ditdash U-I7/I8/I9: preferences, the wipe-and-compare harness, usage + statements views #8
Loading…
Reference in a new issue
No description provided.
Delete branch "ditdash-u-i789"
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?
I7: the PREF_KEYS-whitelisted per-user store, PUT /api/v1/me, the /me panel. I8: the wipe-and-compare CI harness (all_routes + /me x 3 profiles, byte-identical modulo preferences; the falsifiability instrument) — the live SC-1 run stays deferred. I9: the usage/statement views (matrix extension, drill-through, shadow operator-only, no charge math, the U-G4 format gap). Carry-overs landed: the dark-leg contrast test, the forge-mode fail-closed test, Page::Config in all_routes. Critic: PASS (0C/0W). Dispatcher-run suite + clippy green.
The store: src/prefs.rs — one JSON document per forward-auth identity under state_root()/preferences/<username>.json (0600, ≤64 KiB), the frozen PREF_KEYS whitelist (saved_views, pinned, default_range, default_msp), the identity→path guard, and a corrupt-file path that renders defaults with a note (a preference file never breaks a page). The write path: PUT /api/v1/me accepts {preferences: {...}} only — any other top-level key refuses with the key named and the store key derives from the verified headers identity, never the body; the /me panel's form posts through the same prefs::save routine (post/redirect/get). Saved views validate against U-I5a's view-state vocabulary at save time (an unparseable view refuses with the reason) and render as links only when they still parse. The /me resource: the page gains the preferences panel (saved views, pinned rows, defaults, the form) between prefs compare markers; the envelope carries data.preferences, so /me?view=json and /api/v1/me twin the page exactly. The openapi document gains the additive put operation. tests/preferences.rs: whitelist refusal, cross-user isolation (a body can never redirect the write), corrupt-file resilience, form round-trip, unsafe-username refusal, twin parity, and the FR-27 probe at unit scale (clearing the store resets only preferences — /hosts and /clients byte-identical across it). Assisted-by: worker:synthetic/hf:deepseek-ai/DeepSeek-V4.1-FlashThe pages: `/usage` (the msp-level per-client index — current-period summary + period-close status, each row linking its statement page) and `/clients/{c}/statements/{period}` (`current` or a `YYYY-MM` tag: the statement facts, the priced lines VERBATIM, each line's drill to its usage records with their windows + `source` refs, the close state, the v1 not-metered row for llm.tokens, and the export linkage). Render-only: no charge math runs in ditdash — the envelope's amounts render as carried, the shadow filter drops rows for roles that cannot read BillingShadow and never re-rates anything. The data: the collected `client show` surface (U-G3's collector line — the statement content inside the show envelope: lines carrying `usage_record_ids` plus the referenced usage records, freeze/close state). Until it lands, every page renders its named data-absence state; fixture tests pin BOTH states (acme's full world, globex's usage-without-statements variant). Readers are additive-tolerant (no deny_unknown_fields; quantity/consumed_quantity, ref/source and the period naming fallbacks) — an unknown future field parses without refusal (rings alignment). The policy: viewer@client(c) gains Class::Statement + Class::Usage within its client scope (the design §23.3 client-viewer halves) — the golden matrix and policy_matrix.rs extend in the same commit; the msp-level index stays out of a client grant's reach, cross-client probes answer the indistinguishable 404, and shadow lines never reach any viewer (page or twin). The panels: the client detail's usage/statements fill from the collected surface (period rows linking their pages); the app + tenant pages gain their #usage sections from the rows' summaries (no new surfaces). The statement twin's `?format=focus|csv` answers the named U-G4 gap — the wiring point the serializers replace (a probe, never a panic); an unknown format refuses 400 naming the accepted set; the openapi document declares the parameter. tests/billing_views.rs (13 tests): render-as-role, FR-29's drill walk, shadow absence on page + twin for both viewer classes, frozen + provisional close fixtures, the format branch, both data-absence states, the panel fills, the matrix extension, openapi, and the usage/statement digests. tests/pages2.rs's client assertion moves to the reworded data-absence line; the wipe world (tests/wipe.rs) carries the client-show surfaces so the U-I8 sweep walks the new pages; the page digests refresh via the suites' mechanism (the nav gains Usage; app/tenant/client pages gain their sections). Assisted-by: worker:synthetic/hf:deepseek-ai/DeepSeek-V4.1-Flash