carlos / platform Public

spec: dashboard truthful status + scale to 100s of instances

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 0c8f3e764b8379887bbfe59b0bd9185a53ed4e78 parent 79f6fc2
1 file changed, +145 −0
  • docs/superpowers/specs/2026-08-10-dashboard-truth-and-scale-design.md +145 −0
diff --git a/docs/superpowers/specs/2026-08-10-dashboard-truth-and-scale-design.md b/docs/superpowers/specs/2026-08-10-dashboard-truth-and-scale-design.md
new file mode 100644
index 000000000..d34b186e0
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-10-dashboard-truth-and-scale-design.md
@@ -0,0 +1,145 @@
+# App dashboard: truthful status + scale to 100s of instances
+
+2026-08-10. Prompted by the titogo Overview on the tito console showing 18
+false "Not responding" flags, a false app-wide "This app is not deploying"
+banner, and a "2 of 20 responding" tile beside "20 all working" — while every
+flagged address served 2xx/3xx in under a second with the correct
+`X-Carlos-Version`. The page must also anticipate Tito's model: one app
+minting per-customer/per-event instances into the hundreds.
+
+Scope decision (Paul): both halves in one design, truth first — search and
+filter over lying statuses just scales the lie. Growth shape: 100s of
+*dynamic instances* on one app; channels and named routes stay dozens-at-most.
+
+## Diagnosis (verified live + in code)
+
+- The health word comes from a 48h metrics sweep matched to routes only by
+ the managed unit conventions — `carlos-app@<host>.service` or the
+ delegated-exec cgroup name (`internal/console/metricsview.go`,
+ `OwnerAppMetrics`). Routes running under custom units (tito's `titogo@*`,
+ `titogo-canary@*`) never match, so `Reporting()` is false and `healthWord`
+ (`internal/console/overview.go`) falls through to "Not responding" for
+ instance-kind routes. The route rows can carry their real unit name since
+ #110 (`unit` column, used only for restart binding); the sweep ignores it.
+- `AppView.AccountMismatch` collapses route-account stamps to one app-wide
+ boolean (last-writer-wins per app name, `internal/console/view.go`) and
+ renders "this app is not deploying". On tito, live hosts serve `bae`'s
+ edge version — promotes made on the page demonstrably reach the fleet, so
+ the app-wide claim is false; some subset of route rows is stamped `bab`.
+- The Web addresses tile measures DNS-pointing + TLS issuance per domain
+ (`domainNeedsAttention`), not liveness; its "all working" copy invites the
+ liveness reading, colliding with the Running tile.
+- Real defect found alongside: `titogo.bae.platform.tito.io` serves 404 with
+ no version header — operational, tracked separately (see Rollout).
+- The console has zero search/filter/pagination machinery anywhere; every
+ list is unbounded or hard-capped. This design introduces the pattern.
+
+## Design
+
+### 1. Real readings (box) + honest vocabulary (console)
+
+Box metrics sweep: in addition to `carlos-app@*` and exec cgroups, sample
+the cgroup of any registry route whose `unit` column is stamped. Emit into
+the existing readings shape keyed by host, exactly like managed units —
+additive; nothing that parses readings today changes. A stamped unit that
+does not exist or has no cgroup emits no reading and never fails the sweep
+(`ValidUnitName` already rejects malformed names at stamp time).
+
+Console `healthWord`, for a silent, non-hibernating instance-kind route:
+
+- Route is expected to report — the box itself runs its process (managed
+ `carlos-app@` unit or delegated exec child) or the route has a stamped
+ unit — → **"Not responding"**, bad tone, as today. Now true.
+- Otherwise → **"No readings"**, neutral tone (like "Asleep"), with
+ instance-page guidance: stamp the unit (`carlos route … -unit`) to get
+ health here.
+
+Tiles:
+
+- Running: no-readings routes stop counting against the app —
+ "2 of 2 reporting · 18 no readings" (the "· N no readings" clause only
+ when non-zero), not "2 of 20".
+- Web addresses: "20 — DNS & certs OK" instead of "all working".
+- Resources: unchanged (already sums reporting copies only; gains real
+ numbers as routes are stamped).
+
+### 2. Account-mismatch banner at route granularity
+
+Replace the app-wide boolean with the actual list of mismatched routes
+(host + stamped account).
+
+- **All** non-platform routes mismatched → keep the strong "not deploying"
+ banner, now naming the account and the hosts.
+- **Some** mismatched → scoped warning, no "not deploying" claim:
+ "N addresses are stamped to account `X` and won't follow promotes made
+ here: <hosts> — fix on Settings."
+
+On tito this converts a false alarm into a precise pointer at the stale
+`bab`-stamped rows, which is also the diagnostic for the cleanup.
+
+### 3. Scale: rollup + exceptions on Overview, workbench on Running
+
+Overview flow diagram — per column (EDGE, BETA, LIVE, each canary):
+
+- ≤8 addresses: list all, exactly as today. Small apps are unchanged.
+- >8: rollup line "142 addresses · 3 need attention", then only the
+ needs-attention addresses (Not responding / Failed / stuck Setting up),
+ capped at 5, then "+K more → Running". Healthy and Asleep are counted,
+ not listed.
+
+Running tab — server-rendered, GET-param driven (bookmarkable, no JS
+dependency):
+
+- `q=` substring match on host; `health=` filter by health word;
+- cursor pagination ordered by host, page size 50 (`cursor=` opaque,
+ offset-free);
+- header shows per-health-word totals, which double as filter links.
+
+API parity (the page is a view of the API): the instances API gains the
+same `q`, `health`, `limit`, `cursor` params; the response additively gains
+`total`, per-health counts, and `next_cursor` — additive per the wire-shape
+rule: everything that parses today's shape keeps parsing (new fields only,
+no field renamed, moved, or re-typed).
+
+CLI parity: `carlos instances list -match <substr> -health <word>` with the
+same server-side semantics, printing totals — no local-disk registry walk,
+works with zero box access.
+
+Channels grid: structurally unchanged (bounded by team practice); its
+per-column address lists get the same ≥8 rollup for consistency.
+
+### 4. Rollout
+
+1. Branch off origin/main `db76025`; land box sweep + console + API + CLI.
+2. Deploy boxes first (sampling stamped units is harmless while nothing is
+ stamped), then consoles.
+3. Stamp tito's ~18 custom-unit routes (`carlos route … -unit
+ titogo@….service` / `titogo-canary@….service`, run as carlos on the
+ box, one-time). Route records (PR #137) makes stamps durable once
+ merged; not a gate.
+4. Verify against the thing changed: titogo Overview shows canaries
+ Running with real CPU/memory; Running tile reads "N of N reporting";
+ verify with the deployed console, not a cached page.
+5. Follow-ups, precisely named by the new banner/tiles: re-stamp or migrate
+ the `bab`-stamped route rows; investigate the
+ `titogo.bae.platform.tito.io` 404.
+
+Filed as a gap, not built: edge-observed liveness (router-reported
+per-route last-success as a truth source) — the truest signal, deferred for
+its status wire-shape cost.
+
+### 5. Testing
+
+- `healthWord` table tests gain the "No readings" state and the
+ expected-to-report distinction.
+- Overview-stats tests pin the new Running-tile arithmetic and the
+ Web-addresses copy semantics.
+- Banner tests: none / some / all routes mismatched, hosts named in output.
+- API handler tests: `q`/`health`/`cursor` combinations, additive-response
+ compatibility (no new params → prior shape).
+- Box sweep test: stamped unit with fake cgroup dir; stamped-but-absent
+ unit emits nothing and does not fail the sweep.
+- CLI list tests build env through `carlosTestEnv` with `cmd.Dir` in a
+ scratch dir.
+- Gate: `go build ./... && go vet ./... && go test ./... -count=1`, gofmt
+ on touched files, darwin cross-compile.