ditdash U-I6a/b: the config page + the save path (branding, tokens, contrast, price-book editor) #7

Merged
jsutter merged 2 commits from ditdash-u-i6 into main 2026-10-11 17:40:39 +00:00
Owner

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.

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-Flash
The 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
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!7
No description provided.