Documentation
¶
Overview ¶
Package multiui manages a registry of cloop projects for the multi-project web UI dashboard. The registry is persisted at <root>/projects.json, where root is $CLOOP_HOME when set and $HOME/.cloop otherwise — see Root.
Index ¶
- Constants
- func AddPaths(paths []string) error
- func AddPathsOwned(paths []string, owner string) error
- func AllCloopRunPIDs() []int
- func CloopRunPIDsInDir(dir string) []int
- func IsCloopRunningInDir(dir string) bool
- func RemovePath(path string) error
- func Root() (string, error)
- func Save(projects []ProjectEntry) error
- func Scan(dir string) ([]string, error)
- func SetHidden(path, viewer string, hidden bool) error
- type AggregateStats
- type Health
- type ProjectEntry
- type ProjectStatus
Constants ¶
const EnvRoot = "CLOOP_HOME"
EnvRoot names the environment variable that relocates the directory this registry lives in, which would otherwise be $HOME/.cloop. It follows the CARGO_HOME convention: the variable holds the directory itself, not the home that contains it.
Scope note: only this package consults it. The other things under ~/.cloop — profiles, plugins, the executor agent credential — still resolve from $HOME directly, so setting this alone does not relocate them. Redirect $HOME for that. docs/reference/configuration.md states the same thing for operators.
It exists because this registry is process-global mutable state living at a fixed path outside the working tree. Without a seam, any test that reached AddPaths wrote into the developer's real ~/.cloop/projects.json and stayed there. That is not hypothetical: a single test in pkg/ui accumulated 99 entries into the real registry on a development host, at which point project index 99 resolved to a deleted /tmp directory and three unrelated authorization tests began failing on every run. Ephemeral CI runners always start from an empty HOME, so the pipeline stayed green while every developer went red — and the failures surfaced as authorization assertions, which invites "fixing" the assertion instead of the leak.
Variables ¶
This section is empty.
Functions ¶
func AddPaths ¶
AddPaths appends new paths to the registry, avoiding duplicates, and saves. The load-mutate-save sequence runs under registryMu so two concurrent in-process callers cannot each load the same baseline and then race to overwrite each other's additions.
func AddPathsOwned ¶
AddPathsOwned is AddPaths with an ownership stamp: entries created by this call record owner (an oidcauth OwnerKey; "" = unowned/shared). Existing entries keep their current owner — registering an already-known path never re-assigns it.
func AllCloopRunPIDs ¶
func AllCloopRunPIDs() []int
AllCloopRunPIDs returns the PIDs of every "cloop run" process visible to the current user via /proc. Linux-only; returns nil otherwise. Used by the Web UI's global Stop control which intentionally signals every cloop run on the host (single-project setups have only one).
func CloopRunPIDsInDir ¶
CloopRunPIDsInDir returns the PIDs of all "cloop run" processes whose working directory is dir or a subdirectory of dir. It reads /proc/*/cwd symlinks (Linux only) and returns nil on non-Linux hosts or when /proc is unreadable.
Used by the Web UI's per-project Stop button so that pressing stop on project A signals only the cloop run for A, not every cloop run on the host. The pre-fix handler shelled out to `pkill -f "cloop run"`, which fired SIGINT at *every* cloop run process — a real correctness bug in multi-project mode where a single user click would terminate unrelated projects' runs.
func IsCloopRunningInDir ¶
IsCloopRunningInDir returns true if a "cloop run" process has its working directory set to dir. It reads /proc/*/cwd symlinks (Linux only).
func RemovePath ¶
RemovePath drops the entry with the given absolute path from the registry and saves. Removal is by path (not by index) so a concurrent re-order or addition does not cause the wrong project to be deleted. Unknown paths are silently ignored — the registry is the source of truth and a successful no-op delete is the desired outcome when the entry is already gone.
func Root ¶
Root returns cloop's per-user state directory: $CLOOP_HOME when set and non-empty, otherwise $HOME/.cloop.
A relative CLOOP_HOME is resolved against the current working directory, so that a caller cannot silently end up addressing two different directories from two different working directories.
func Save ¶
func Save(projects []ProjectEntry) error
Save writes projects to the registry file atomically: it serialises the JSON, writes it to a sibling tmp file, fsyncs, and renames into place. A crash or disk-full mid-write therefore leaves the previous registry intact instead of a half-written / zero-byte projects.json that future loads would refuse.
func Scan ¶
Scan walks a parent directory one level deep and returns all subdirectories (including the directory itself) that contain a .cloop/ folder.
func SetHidden ¶
SetHidden records (hidden=true) or clears (hidden=false) viewer's wish to keep the project at path off their dashboard, and saves the registry.
viewer is an opaque per-user key chosen by the caller — pkg/ui passes the OIDC OwnerKey, or a fixed single-user sentinel when OIDC is disabled. This package deliberately does not know how that key is derived.
Addressing is by path, not by index, for the same reason RemovePath is: an index is only meaningful relative to a list that a concurrent add or re-order can shift underneath the request.
A path that is not yet registered is added rather than rejected. Projects reach the dashboard from three places — the registry, --projects, and the directory the server was launched in — and only the first is persistent. Refusing here would make "hide" fail for the other two with an error about registration that says nothing about what the user asked for.
Types ¶
type AggregateStats ¶
type AggregateStats struct {
TotalProjects int `json:"total_projects"`
ActiveRuns int `json:"active_runs"`
TotalTasks int `json:"total_tasks"`
DoneTasks int `json:"done_tasks"`
FailedTasks int `json:"failed_tasks"`
TotalSteps int `json:"total_steps"`
}
AggregateStats holds aggregated metrics across all projects.
func Aggregate ¶
func Aggregate(statuses []ProjectStatus) AggregateStats
Aggregate computes aggregate stats from a slice of project statuses.
Statuses the recipient has hidden are skipped, so the headline counters agree with the cards actually on screen. A "12 projects" total above a grid showing nine would read as a rendering bug.
type ProjectEntry ¶
type ProjectEntry struct {
Name string `json:"name"`
Path string `json:"path"`
// Owner records which dashboard user created/owns this project when the
// Web UI runs with OIDC authentication enabled. The value is the
// oidcauth.Identity.OwnerKey() — lowercased email, or "sub:<subject>"
// when the IdP supplies no email. Empty means unowned/shared: the
// project is visible to every authenticated user. The field is ignored
// entirely when OIDC is disabled.
Owner string `json:"owner,omitempty"`
// HiddenFor lists the viewers who have hidden this project from their
// dashboard. Values are viewer keys in the same namespace as Owner (see
// SetHidden).
//
// This is a per-viewer set rather than a single bool because the
// registry is one file shared by every user of a hub. A global flag
// would mean one user decluttering their own dashboard blanks the
// project out of everyone else's — the same tenancy mistake a global
// task list would be.
//
// Hiding is presentation only. It is deliberately *not* access control:
// a hidden project stays in the caller's project list (flagged via
// ProjectStatus.Hidden) so index-addressed routes keep resolving to the
// same project, and so the settings panel can offer it back. Owner
// governs who may see a project at all; this governs who wants to.
HiddenFor []string `json:"hidden_for,omitempty"`
}
ProjectEntry is a registered project in the multi-project registry.
func Load ¶
func Load() ([]ProjectEntry, error)
Load reads the registry from disk; returns an empty registry if file is absent.
On parse failure (zero-byte file from a torn pre-atomicfile write, schema drift, manual edit gone wrong) the corrupt projects.json is quarantined aside as projects.json.corrupt-<unix> and (nil, nil) is returned. The multi-project Web UI dashboard is the worst pre-fix case here: a bad save in this global registry bricked every "cloop ui" multi-project listing across all projects on the host. Returning empty is a recoverable state — the user re-adds projects via `cloop ui add` — whereas a hard error disabled the entire dashboard.
func (ProjectEntry) HiddenForViewer ¶
func (e ProjectEntry) HiddenForViewer(viewer string) bool
HiddenForViewer reports whether viewer has hidden this project. Comparison is case-insensitive to match the Owner rule, since both hold email-derived keys that an IdP may not case-normalize.
type ProjectStatus ¶
type ProjectStatus struct {
Name string `json:"name"`
Path string `json:"path"`
Status string `json:"status"` // state.Status field value
Health Health `json:"health"` // computed indicator
Goal string `json:"goal"`
TotalTasks int `json:"total_tasks"`
DoneTasks int `json:"done_tasks"`
FailedTasks int `json:"failed_tasks"`
ActiveTasks int `json:"active_tasks"`
TotalSteps int `json:"total_steps"`
LastActivity time.Time `json:"last_activity"`
Provider string `json:"provider,omitempty"`
Model string `json:"model,omitempty"`
PMMode bool `json:"pm_mode"`
HasProject bool `json:"has_project"` // false if no state file found
Running bool `json:"running"` // true if cloop run is actually executing
// Hidden reports that the recipient of this payload has hidden the
// project from their dashboard. It is per-recipient, so GetStatus
// deliberately leaves it false: the status cache is shared by every
// connected client, and the flag is stamped on a per-recipient copy
// when the list is filtered for delivery.
//
// Hidden projects are still delivered. Dropping them would renumber
// the list, and the whole dashboard addresses projects by their index
// in it — a hidden project at index 2 would silently shift every
// project after it onto a neighbour's tasks, runs and deletes.
Hidden bool `json:"hidden,omitempty"`
}
ProjectStatus is the live status of a project, returned by the /api/projects endpoint.
func GetStatus ¶
func GetStatus(entry ProjectEntry) ProjectStatus
GetStatus loads the state for the project at path and returns a ProjectStatus.