feat(pages): batch 2 — clients, edge, releases ladder, history, live/branches/development, secret locations (U-I3) #2

Merged
jsutter merged 7 commits from ditdash-u-i3-pages2 into main 2026-10-08 13:09:48 +00:00
Owner

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.

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 manifest records `dito --version` output, which already begins with
"dito " — the provenance line prefixed again and every page read
"dito dito 6.1.2-rc.7". Render the collector's version string verbatim
and pin the regression with a render test; regenerate the recorded
snapshot digests (the provenance line appears on every page).

Also clears the clippy findings that kept `cargo clippy --all-targets --
-D warnings` red: the views.rs needless lifetime and three pre-existing
test-side lints (policy_matrix clone-ref, canon_surface needless
allocation, http len-zero).

Assisted-by: worker:synthetic/hf:deepseek-ai/DeepSeek-V4.1-Flash
The Entity gains an optional client slug and the in_client constructor:
a client-scoped grant (operator or viewer) now reaches the Client class
only for the client it names, replacing the class-wide match that let any
client-scoped binding read every client row. The membership map
(client → its tenants) still does not exist, so nothing wider resolves —
fail-closed, stated in the policy comment and pinned by the golden matrix.

The golden role matrix extends its entity cells with the client dimension
(3 tenant options × 3 client options × 21 classes × 3 scopes × 4 verbs ×
2 roles = 4536 cells) and gains the boundary test the batch-2 acceptance
names: a tenant-scoped viewer reaches its own tenant's rows and nothing
across the tenant boundary — never another tenant's rows, never host
data; a client-scoped grant reaches only its named client.

Assisted-by: worker:synthetic/hf:deepseek-ai/DeepSeek-V4.1-Flash
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-Flash
The 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-Flash
The 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
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
deepnet/ditdash!2
No description provided.