carlos / platform Public

spec: fleet status page — public commitments, operator role, bucket-backed serve

Brainstormed with Paul 2026-08-01: fleet/ops scope only, opt-in tenant
naming, console-private with an operator toggle to public, published by a
lease-holding box and served bucket-backed so the page outlives the
deployment it reports on.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 7cfe97c98c45feff460845f9bc4c7ca1c63a964f parent df19892
1 file changed, +185 −0
  • docs/superpowers/specs/2026-08-01-fleet-status-page-design.md +185 −0
diff --git a/docs/superpowers/specs/2026-08-01-fleet-status-page-design.md b/docs/superpowers/specs/2026-08-01-fleet-status-page-design.md
new file mode 100644
index 000000000..2f61eaf3a
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-01-fleet-status-page-design.md
@@ -0,0 +1,185 @@
+# Fleet status page — design
+
+Every CARLOS deployment computes a fleet status view; the operator chooses
+whether it is public. The flagship's is public, at
+`status.carlosframework.com`, holding the same public commitments as
+Eleven's status pages (`status.elevenmessenger.com`): aggregates only,
+band words instead of needles, loud unknowns, and nothing published that
+wasn't deliberately chosen.
+
+Scope decision: **fleet/ops only** — the CARLOS analogue of Eleven's
+`fleet.json`/`index.html` pair. Eleven's other dashboards (books, carbon,
+prompts, contributors, security, SSH ledger, …) are that project's ledgers,
+not platform features; any of them can layer on later as separate specs.
+
+## 1. Access & audiences
+
+Three audiences, two of them existing:
+
+- **Operators** — a new console role. Identity is the console's existing
+ Keymail OAuth (an operator is a person with a keymail address; no second
+ identity system). Authorization is an `operator` flag on the user,
+ granted **only** via the box-side CLI: `console grant -operator
+ <address>`. The root of trust stays what it really is — possession of
+ the box / the AWS account — and the console role is a projection of it
+ into the UI, exactly like owner grants today. There is no self-service
+ path to operator. The role gates: the private fleet-status view, the
+ all-apps/all-routes view, and future operator surfaces. It deliberately
+ does **not** replace the deeper credentials: shipping still requires the
+ release signing key, infra still requires AWS — the role can never do
+ more than the console process itself can.
+- **App owners** — unchanged; they see their own apps' pages only.
+- **The public** — only when the deployment opts in. `STATUS_PUBLIC=1`
+ publishes the static page at `STATUS_HOST`; private is real access
+ control (console auth + operator role), never an unlisted URL.
+
+A self-hosted deployment authenticating its console against keymail.dev is
+trusting a third-party identity provider; that's each operator's choice to
+make, stated plainly in the docs (Keymail itself is self-hostable, which
+keeps the choice sovereign).
+
+## 2. Data model & the privacy boundary
+
+Two bucket artifacts, both written by machinery that never lets a needle
+out. Every commitment below is enforced in the *writer* — ratios, caps,
+hostnames, and machine ids are read internally and die in the process that
+banded or counted them.
+
+### Per-box snapshots — `status/boxes/<box>.json`
+
+Written by each box's agent on a 5-minute timer:
+
+```json
+{
+ "generated_at": "2026-08-01T18:00:00Z",
+ "label": "ada-stargazing",
+ "region": "EU · Ireland",
+ "routes": { "static": 2, "resident": 3, "hibernating": 4,
+ "awake": 1, "asleep": 3 },
+ "load": "comfortable",
+ "capacity": "plenty",
+ "replication": { "healthy": true, "lag_seconds": 42 }
+}
+```
+
+- **Label**: whimsical name-action pair hashed from the machine id —
+ stable per box; never the hostname, IP, or instance id.
+- **Region**: operator-set, coarse by design — continent + country from
+ the deployment's region registry, never a city or datacenter; unset is
+ omitted, never guessed.
+- **`awake`/`asleep`** split the hibernating set by whether each tenant's
+ socket is bound and listening, read side-effect-free from the kernel
+ socket table (`ss -xlH` equivalent) — dialing would wake the tenant.
+ Unreadable → both `null`. Unknowns publish as null, never as guesses.
+- **`load` / `capacity`**: band words only — `comfortable`/`busy`/
+ `critical` and `plenty`/`getting-full`/`at-capacity`, banded at 70% and
+ 90%, computed as the worst of disk-floor proximity and memory
+ utilization. The ratios are never serialized.
+- **`replication`**: seconds since litestream last progressed; `null`
+ when unknown.
+
+### The merged feed — `status/fleet.json` + `status/index.html`
+
+Rendered from the snapshots by whichever box holds the status lease
+(reusing `internal/lease` on `status/lock.json` — no new election
+machinery; any box can render, exactly one does):
+
+```json
+{
+ "generated_at": "2026-08-01T18:05:00Z",
+ "boxes": [ { "label": "ada-stargazing", "region": "EU · Ireland",
+ "load": "comfortable", "stale": false,
+ "routes": { "static": 2, "resident": 3, "hibernating": 4,
+ "awake": 1, "asleep": 3 } } ],
+ "instances": { "total": 9, "static": 2, "resident": 3,
+ "hibernating": 4, "awake": 1, "asleep": 3 },
+ "capacity": { "level": "plenty" },
+ "apps": [ { "name": "kass.training", "instances": 1, "healthy": true,
+ "channel": "stable", "version": "aedcae3" } ],
+ "apps_unlisted": 5,
+ "services": [ { "name": "svc/pdf", "up": true } ],
+ "replication": { "healthy": true, "lag_seconds": 42 }
+}
+```
+
+- **Apps are opt-in by name.** Default is Eleven-strict: no app names, no
+ hostnames. The opt-in is an additive `status_named` column on the route
+ (set via `carlos add -status-named`, and later from the manifest when
+ manifests land) — the app's own choice, recorded where the publisher
+ already reads. An opted-in app appears as name + instance count +
+ health + channel + version-short-sha. Everyone else folds into `apps_unlisted` — one
+ honest count, so the aggregate never lies by omission.
+- **`capacity.level`** is the worst box's band.
+- **`services`**: each `svc/*` route's up/down from its health check.
+- **Staleness is loud**: a box whose snapshot has missed 3 consecutive
+ periods renders as stale, never silently dropped.
+
+### The rule for new fields
+
+A field must be a fleet-wide aggregate, a band word, or behind the app's
+own opt-in — or it doesn't ship. The dry-run self-check and the contract
+test both fail if a fixture hostname or a digit inside the
+capacity/load subtrees reaches the output.
+
+## 3. Publish & serve
+
+The viewer rides the binary, the data rides the bucket, the edge joins
+them:
+
+- **Page assets** (`index.html`, one stylesheet; no JS frameworks, no
+ external requests — same-origin only) are embedded in the carlos binary
+ like the systemd units are. The lease-holding publisher writes them to
+ `status/` beside `fleet.json` each pass. Page changes deploy with the
+ binary; data changes deploy with the timer.
+- **No release churn.** Publishing every 5 minutes must not mint CAS
+ releases. The status route is a small `kind=status` route the edge
+ serves **bucket-backed** from the `status/` prefix with a short (60s)
+ cache, reusing the static handler's plumbing — not channel-pointer
+ tars.
+- **Config**: `STATUS_HOST` + `STATUS_PUBLIC` in the deployment's host
+ config. Public = the route exists (normal ACME via the route table)
+ *and* the `status/` prefix is public-read. Private = neither; the
+ console renders the same `fleet.json` for operators, and that is the
+ only window. A box with no status config publishes nothing (the
+ litestream convention: install everywhere, inert until configured).
+- **Incident fallback** — the reason this architecture won: on a public
+ deployment the raw bucket URL serves the identical page even when every
+ box is down; the page footer links it as "if this page is unreachable".
+ The status page outlives the deployment it reports on.
+- **Honest limit**: a fully-private deployment whose console is down has
+ no status page — the operator falls back to `aws s3 cp` of
+ `fleet.json`; documented in the runbook, not papered over.
+- **Flagship**: `status.carlosframework.com`, public. One A record in the
+ carlosframework.com zone (Paul Campbell DNSimple account), bucket
+ policy for the public prefix in platform-infrastructure, and
+ `console grant -operator paul@keymail.dev`.
+
+## 4. Testing & guard rails
+
+All build-failing, ported from Eleven's pattern:
+
+- **Allowlisted schemas**: a contract test pins every key `fleet.json`
+ and the snapshots may contain — an extra field fails; the
+ capacity/load subtrees additionally fail on any digit.
+- **Leak tests**: the writer runs against a fixture registry full of
+ hostnames and machine ids; none may survive into output except apps
+ explicitly opted in; unknowns must surface as `null` (an `ss` failure
+ must never render as `awake: 0`).
+- **Banding tests** at the 70/90 boundaries; worst-of-boxes capacity;
+ stale-after-3; `apps_unlisted` always equals total minus named.
+- **`carlos status -dry-run`**: builds the whole page from fixtures with
+ no box state, network, or upload — the test surface and the
+ self-hoster's "what would I publish?" preview in one verb.
+- **e2e**: the edge serves a `kind=status` route bucket-backed; the
+ console shows the fleet view to an operator and 404s a mere owner.
+
+## 5. Rollout
+
+1. Platform repo: `internal/status` (writer + publisher), the agent
+ timer, edge `kind=status`, console operator role + fleet view.
+ Code-only, green before any deploy.
+2. Ship/adopt the binary on both deployments — inert until configured.
+3. Flagship: public config, bucket-policy tofu, DNS A record, operator
+ grant, verify page + fallback URL.
+4. Tito: private by default, with the console-less fallback noted; the
+ operator can flip it public or deploy a console later.