ditdash U-I6a/b: the config page + the save path (branding, tokens, contrast, price-book editor) #7
Loading…
Reference in a new issue
No description provided.
Delete branch "ditdash-u-i6"
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?
I6a: the config-repo read client, schema-on-load + last-good banner, the /config page shell, the state root. I6b (sensitive): the save path (fresh-HEAD CAS, author/committer split), branding, Design Tokens, the contrast solver, the schema-staged price-book editor. PREC-I6-2 unlanded: the write token fails closed, no manifest ref (the WS-0 lesson), the editor renders read-only. Critic: PASS-with-findings (3 coverage warnings — dark-mode contrast test, forge-mode fail-closed test, /config in all_routes for I8 — carried to the I7/I8 batch). Dispatcher-run suite + clippy green.
The config document of record lives in deepnet/config (the U-F1 converge phase provisions it). This unit lands the read half of FR-25: - src/state_dir.rs: the one state root (/data/apps/ditdash/data in production, DITDASH_STATE_DIR overrides, temp fallback) — the root U-I8 wipes; collector.rs's bundle_cache_dir() now resolves state_root()/bundle-cache (path-identical), and state_dir::config_cache_dir() joins it for the config cache. - src/configrepo.rs: the contents-API client (Mode::Fixture for tests, Mode::Forge for the live repo via the existing DITDASH_FORGE_TOKEN once PREC-I6-1 widens its reach, Unconfigured honest fallback), on-load validation against the vendored config-document schema, in-memory last-good retention, the invalid_head{commit, errors[]} banner state, size-capped asset fetch (logo/favicon, ≤256 KB, names validated before any join), the disposable cache under the state root, and the one read model the page and its twin share. - docs/schemas/config-document.schema.json: the versioned contract (NFR-6): product name, brand token overrides against the theme's closed --dn-* set, display defaults, known MSPs, the price-book section (structural here; U-I6b refs the vendored G3 schema), and secret_references as LOCATIONS only (NFR-2). - /config + /api/v1/config (Page::Config, PATH_TEMPLATES): the operator-only read surface (the Config class denies every viewer and tenant-scoped operator — 404, existence never leaks) rendering role bindings (msp show, source-named), client declarations (client list), branding as served (asset state included), secret references joined against deepnet's secret list metadata (resolved/absent/unknown + reason + the exact `ditc secret set` command), display defaults, known partner MSPs, and the price-book versions with their pricing lines. The footer names the repo and the honest "no dito command serves config yet" line instead of a fictional invocation. - Tests: valid-document render + twin parity (every twin scalar reaches the page), invalid-HEAD last-good + banner naming the commit, boot-with-only-invalid (no fabricated default), viewer/tenant-operator/unauthenticated denials, the schema round trip + defect classes, the disposable-cache wipe-rebuild identity, and the missing-asset honest state. Assisted-by: worker:synthetic/hf:deepseek-ai/DeepSeek-V4.1-FlashThe write half of FR-25 (sensitive: a standing write credential enters the app by design, with exactly one repository's reach): - The save path: POST /config (form) + POST /api/v1/config (JSON) through ONE commit routine — validate the config-document schema, the vendored price-book schema plus its pure data checks (overlap, duplicate meters, ascending tiers, overrides that replace nothing), the 4.5:1 contrast floor, and the frozen-statement-period check (fail-closed while the collector carries no statement state) BEFORE any write; then a compare-and-swap on HEAD (a save prepared against a moved commit re-reads once and refuses 409, nothing written); then one contents-API PUT, author = the verified operator, committer = ditdash-config (PREC-I6-2's one-repo service identity; the write credential arrives as DITDASH_CONFIG_WRITE_TOKEN and every save fails closed without it). Assets commit each their own commit (validated all-or-nothing first: name, ref, base64, ≤256 KB cap, format signature). A save on an invalid HEAD builds from last-good and says so in the report and the commit message. The served snapshot rebuilds from the repository right after each save. - Branding application: page() consumes the served document's brand — token overrides as a <style> over the theme's own --dn-* names (light + dark layering; the theme keeps loading), the product name in title + header, the logo inside <img>, the favicon link, the body font (one consumer rule), the support link, footer text, and the sign-in hint. - The contrast solver: pure, WCAG relative luminance, 4.5:1 over ink-on-canvas / surface / brand-accent / tint in both modes; a failing palette refuses with the pair named, and CI pins the shipped theme palette green (NFR-5). - W3C Design Tokens import/export for the brand subset (color by mode, fontFamily, the asset refs): export → import → export is byte-equal after the canonicalization pass; a key outside the subset refuses by name. - Brand assets: /config/{asset} served ONLY from the config cache, with Content-Security-Policy: default-src 'none' + X-Content-Type-Options: nosniff — the SVG renders nowhere but inside <img>. - The document editor (one form): branding fields, the light/dark token inputs, the tokens export block + import field, display defaults, known-MSPs and secret-reference rows, the price-book document rows, and the base64 asset fields — every section saved through the one write path. - U-I4a's grafana_base() seam is fulfilled: the document's display_defaults.grafana_base overrides the env default on the traffic panel's explore link (the read model takes the override). - The openapi document names the save operation (request body + 400 / 404 / 409 / 503) — the observable element the route table alone could not describe. - docs/schemas/pricebook.schema.json: staged from the billing plan §U-G3a step 1 (G3 has not landed) with its provenance digest (pricebook.provenance.json names G3a canonical); the config schema now $refs it and tightens body_font to a font-stack alphabet. Deviations from the plan text, deliberate: the asset route rides /config/{asset} (the canon's registered-noun gate refuses a config-assets segment); the asset type set follows the schema's svg/png/webp/ico names; the openapi edit sits outside the plan's file list but the canon requires the observable save surface documented. Tests: save→one-commit with the author/committer split from the commit records, the form path's PRG, moved-HEAD 409, invalid-HEAD self-heal, fail-closed price-book edit + frozen-period refusal (collected-state fixture) + a clear window saving, the price-book defect classes, the contrast refusal with the pair named, tokens round-trip + refusals, asset commit + header set + traversal refusal, Set-gate denials, the grafana override precedence, the openapi save operation, and post-save twin parity. All suites green; no snapshot digest moved. Assisted-by: worker:synthetic/hf:deepseek-ai/DeepSeek-V4.1-Flash