feat(pages): batch 2 — clients, edge, releases ladder, history, live/branches/development, secret locations (U-I3) #2
Loading…
Reference in a new issue
No description provided.
Delete branch "ditdash-u-i3-pages2"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
U-I3 pages + API twins; client-scoped policy narrowing; provenance double-prefix fix. Tests 61→70; clippy clean. Critic approve-with-fixes: FR-34 verdict vocabulary — keep the landed U-R3N2 spellings (ditdash renders what dito emits); spec erratum recorded.
The client list surface becomes two pages: /clients (every declared client, the billing contact rendered with its placeholder flag — a RFC 2606 placeholder is a declared gap, never laundered into a real person) and /clients/{slug} (the same row plus PSA account, anchor day, and the U-G3 usage/statement gap). Both read the collected client list surface; the detail's CLI twin names `dito client show` (the command that will answer it), the host-detail precedent. RBAC rides the policy function: the list is an MSP-level surface, the detail is the client entity (in_client), so a client-scoped grant reaches only the client it names and nothing class-level. A tenant-scoped viewer reaches no client page. The fixture client rows move to the landed U-R3N4 shape (legal name, contact triple, per-row sources/gaps). Tests: render-as-role snapshots (denied roles 404), twin envelope + shape tests with scalar twin→page parity (the parity helper moves to tests/common), and the client-scope boundary test (own client opens; the class list, foreign clients, and every host/edge surface 404). Assisted-by: worker:synthetic/hf:deepseek-ai/DeepSeek-V4.1-FlashThe edge page family reads the collected `edge show` surfaces: /edges summarizes every edge the bundle knows (collected files, the host list's edge column, and the manifest's named-unavailable edges — an edge without a surface renders its reason, never silence) and /edges/{edge} renders the U-R3N2 rows: public addresses with their host/tenant links and the undeclared-tenant-IP exposure finding, hosts behind the edge, and the three port views per address:port — declared, appliance ("not probed" when the cell is absent, never a fabricated closed), outside (the fixed "no outside view" placeholder until the alerting arc's vantage exists) — each with the surface's row verdict (consistent / exposure / drift / unmanaged / unknown) rendered as a badge beside its detail line. The appliance verdict diff renders when the surface carries `live`; without it the page names the missing probe (`edge show --live`). The fixture edge surface moves to the landed U-R3N2 shape (camelCase edge/hosts/addresses/ports/live) with a drift, an exposure, a consistent, and an appliance-unprobed row; a dedicated test strips the live block and the appliance cells and asserts the declared-only rendering. RBAC: edges stay MSP-level — a tenant viewer or client-scoped grant reaches none of it, pinned in tests. Assisted-by: worker:synthetic/hf:deepseek-ai/DeepSeek-V4.1-Flash/releases renders the release list surface: per-channel heads, every indexed release, and the catalog ladder (#ladder) — per service key, the declared version at each channel head from the release index's machine-written catalog. The rungs the surface does not carry are named, never faked: the running rung (and with it the lever) awaits `dito service show <tenant> <key>`; the upstream rung awaits the catalog upstream feed and reads "no evidence" without one. /releases/{tag} renders the release's catalog, a catalog diff derived from the in-channel predecessor in the index (labeled derived; `dito release diff` adds the migration ordering), the per-tenant resolution table joined from each tenant show surface's release block (with unavailable tenants named — coverage is never silent), and the release review fields the surface lacks (changelog, CI state, pull requests) as named gaps. Both pages are MSP-level: a tenant viewer 404s. Assisted-by: worker:synthetic/hf:deepseek-ai/DeepSeek-V4.1-Flash/history is the global change-history feed (FR-17): entity, actor, kind, unattributed-only, and a time range ride the URL; the read model parses the raw query, narrows records, and the twin carries the same filter state plus the clear/toggle hrefs the page renders. The route segment `history` joins the canon test's exempt list — the canon's own rename table names `<noun> history` (a read verb; "the global feed = <msp> history"), deliberately not a noun. The bundle does not yet carry the surface (the collector collects the read surfaces, not the audit-repository read): the page then names the missing surface ("the history feed surface (msp history) is not collected yet"), states what it searched, and asserts no records — and an empty filtered result states its coverage, never a silent blank. The filter mechanics are pinned against a fixture that writes the future msp-history surface: entity/kind/unattributed/since narrowing, the empty-result coverage line, the twin's narrowed records, and the MSP-scope denial for a tenant viewer. Assisted-by: worker:synthetic/hf:deepseek-ai/DeepSeek-V4.1-FlashThe batch-2 panels land on the existing pages: - Every app page carries #live: recorded (per-channel deploy/rollback) against observed (host record + exporter), with the agreement verdict computed in the one read model so page and twin render the same judgment — live when the refs agree, drift naming both ("recorded X — host serves Y"), unknown when a side is missing. A row without live rows renders the recorded ref the list carries, observed null, verdict unknown; never a silent pick. #branches and #development render their panels with the missing input named (U-R3N5 branches, U-F4 development records), and the twin carries the same gaps. - The tenant page and the MSP home gain the What's-live table (design §23.5) over the same rows, tenant/app linked. - The tenant page gains the secret-locations panel from the collected secret list surface: env, key name, path, status, version — metadata only, never a value. The class gate precedes the surface read, so a denied role's twin holds null secrets and its page holds no panel (the msp viewer, the tenant viewer, and the client scope all pinned). The app-list fixture gains the recorded/observed live rows (stable drifts, testing agrees, one app without rows) and the secret-list fixture moves to the landed U-R3N4 shape. Assisted-by: worker:synthetic/hf:deepseek-ai/DeepSeek-V4.1-Flash