phase: 190_app_name_settings_fix
All gates re-verified green. Report: - **Verified (all 4 tasks already complete; this was the final pass):** `effective_settings` resolves the 3 brand strings panel-row → tab-row → env (`applied_override_keys`, presence-not-value); `PUT /api/ui-settings` ignores string body fields; Theme-tab form is colors-only (17 pickers, no `#theme-app-name`/`#theme-placeholder`/`#theme-footer`); the ONE shared `applyBrandFromConfig` re-applies title/`.brand-text`/footer/placeholder after a panel save-reset and a tab save-reset; boot path byte-identical (single `/api/config`). - **Completion criteria:** - strings order panel→tab→env, both branches — ✅ `tests/unit/test_theming.py::test_effective_app_name_prefers_settings_panel_row_over_ui_settings_row`, `..._fall_back_to_ui_settings_row_without_panel_row`, `..._per_field_panel_then_tab_then_env`, `..._ignore_panel_rows_when_the_overlay_is_off` - PUT ignores string fields + no 3 inputs in the tab — ✅ `tests/integration/test_ui_settings_api.py::test_put_ignores_string_fields_in_body` (+2) and the E2E `test_theme_tab_dom_is_colors_only` - live apply without reload + incident regression — ✅ `tests/e2e/test_app_name_settings_fix.py` tests 1–4 - byte-identical boot, no double-apply — ✅ `tests/unit/test_brand_live.py` markers + E2E "exactly one /api/config" - suite green, coverage >90%, ruff+pyright clean — ✅ - no behavior change in prior phases — ✅ (palette, phase-168 panel, env fallback suites all green) - atomic commit — **left to the harness** (pipeline rule overrides per-phase committing; no git write run) - **Gates:** `uv run pytest --cov=app --cov-report=term-missing` → **4630 passed, TOTAL 6941 16 99%**, exit 0 · `uv run pytest tests/e2e/test_app_name_settings_fix.py -v --no-cov` → **6 passed** in isolation · regression E2E (isolated, one process each, all exit 0): `test_smoke` `test_configurable_brand` `test_admin_theme_tab` `test_theme_save_and_coverage` `test_theme_semantic_completion` `test_themed_favicon` `test_dark_tech_theme` `test_admin_settings_panel` `test_admin_settings_layout` `test_settings_mobile_value_width` · `uv run ruff check .` → All checks passed · `uv run pyright` → 0 errors. - **Documentation:** README — fixed "saving one repaints *every* open page" (only the open document repaints; other pages on next load) and a broken paragraph wrap in *Customizing the look*; AGENTS.md rule 16 — `test_chunks_hnsw_recall.py` now described accurately (index paths forced with `enable_seqscan`/`indexonlyscan`/`bitmapscan` off, unforced plan asserted only on a pristine relation); `tests/unit/test_theme_frontend.py` — two stale section headers ("20 form fields"/"20 inputs" → 17 color). No AGENTS.md settings-overlay/resolution-order clause exists, so per the phase none was added. Full env-var cross-check (`BOR_*` in `app/config.py` vs README) → 0 gaps; 0 missing file references. - **Notable:** the working tree also carries this phase's flake hardening of the phase-188 `tests/integration/test_chunks_hnsw_recall.py` (verified legit and green). Live infra untouched; shared DB left clean (0 `app_settings`, 0 `ui_settings`). **Flagged, not changed:** `settings_overlay.applied_override_keys` lets a `ProgrammingError` (missing `app_settings`) propagate from public `GET /api/config`, where `load_overrides` deliberately fails soft — only reachable on an un-migrated (0027) DB, outside this phase's pinned A1 semantics, so left for the owner to rule on rather than silently deviating. - **Next pending phase:** `191_source_name_labels_tags`.
This commit is contained in:
+107
-50
@@ -1,5 +1,7 @@
|
||||
/* Brain of Reese — Theme view module (phase 91, task 05; phase 92,
|
||||
* task 04): the admin palette + branding editor.
|
||||
* task 04; COLORS ONLY since phase 190, task 02): the admin palette
|
||||
* editor — the three brand strings moved to the admin Settings panel
|
||||
* (A1: that panel is their single writable owner).
|
||||
*
|
||||
* The phase-76 shell-view-module contract (the tuning.js / tokens.js
|
||||
* shape): the router (assets/router.js) lazy-imports this module on
|
||||
@@ -17,19 +19,22 @@
|
||||
* gate (the #nav-theme link is already hidden by header.js — the
|
||||
* gate is the DIRECT-URL case, the #tokens-gate pattern). No
|
||||
* /api/ui-settings request is ever made outside the admin branch.
|
||||
* • load — GET /api/ui-settings → populate the 20 inputs with the
|
||||
* EFFECTIVE values (the resolver's DB-over-env / DB-over-built-in
|
||||
* merge): the tab always shows the live theme — env defaults when
|
||||
* the row is empty. A failed fetch keeps the static form (the
|
||||
* built-in values ship in the inputs) and shows #theme-error with
|
||||
* a retry (the loadHealth house style — never a blanked panel).
|
||||
* • live preview (colors only, B4) — on `input` of any of the 17
|
||||
* color pickers the value is written straight onto <html> as an
|
||||
* inline custom property, so the WHOLE page repaints (every view,
|
||||
* the header) while the owner is picking. Text fields have NO page
|
||||
* effect: the 3 strings keep the brand.js runtime application
|
||||
* (owner-locked B4) — they apply via the /api/config boot fetch on
|
||||
* the NEXT page load, and the sub-copy says so.
|
||||
* • load — GET /api/ui-settings → populate the 17 color inputs with
|
||||
* the EFFECTIVE values (the resolver's DB-over-env /
|
||||
* DB-over-built-in merge): the tab always shows the live theme —
|
||||
* the built-in palette when the row is empty. A failed fetch keeps
|
||||
* the static form (the built-in values ship in the inputs) and
|
||||
* shows #theme-error with a retry (the loadHealth house style —
|
||||
* never a blanked panel). The response still carries the 3 brand
|
||||
* strings (read-only, old-client compat) — populate() simply has no
|
||||
* field to write them into.
|
||||
* • live preview (colors only) — on `input` of any of the 17 color
|
||||
* pickers the value is written straight onto <html> as an inline
|
||||
* custom property, so the WHOLE page repaints (every view, the
|
||||
* header) while the owner is picking. The brand strings are not
|
||||
* this tab's surface at all since phase 190 (A1) — the Settings
|
||||
* panel edits them (and, from phase 190 task 03, applies them to
|
||||
* the open document on save).
|
||||
* • favicon live preview (phase 177, A4 — owner-confirmed
|
||||
* 2026-10-13) — a pick of one of the THREE driving fields
|
||||
* (``surface`` / ``brand`` / ``brand_ink`` — the artwork's
|
||||
@@ -95,10 +100,10 @@
|
||||
* the admin opens the tab (a normal load is a no-op — the served
|
||||
* tag and the overrides agree).
|
||||
* • Save — the §7.4 never-stale lifecycle: disable + "Saving…" →
|
||||
* PUT /api/ui-settings with the 20 form values (a cleared/empty
|
||||
* text field → null; colors always their current hex — the
|
||||
* server's built-in→NULL normalization keeps the row empty when
|
||||
* the owner saves the defaults) → 200: #theme-result "Theme
|
||||
* PUT /api/ui-settings with the 17 form colors (always their
|
||||
* current hex — the server's built-in→NULL normalization keeps the
|
||||
* row empty when the owner saves the defaults; NO string key in
|
||||
* the body, phase 190 A1) → 200: #theme-result "Theme
|
||||
* saved." (role=status), refetch + re-populate (canonical state),
|
||||
* reconcile the open document to the settled values (#bor-theme
|
||||
* text + the <html> overrides — the OPEN page paints the saved
|
||||
@@ -111,13 +116,26 @@
|
||||
* field), the form is KEPT (the owner fixes + retries); any other
|
||||
* non-2xx: the fixed error line; a network error: the "is the app
|
||||
* reachable?" line.
|
||||
* • Reset — the same lifecycle ("Resetting…") with all 20 values
|
||||
* null (the API's documented "defaults" operation) → #theme-result
|
||||
* • Reset — the same lifecycle ("Resetting…") with all 17 colors
|
||||
* null (the API's documented "defaults" operation for the palette;
|
||||
* a brand string is reset from the Settings panel, phase 190 A1) →
|
||||
* #theme-result
|
||||
* "Reset to the built-in theme." → refetch + re-populate (the
|
||||
* env/built-in defaults) → reconcile the open document: the tag
|
||||
* is REMOVED (effective = the built-ins → content null) and the
|
||||
* <html> overrides are dropped (a failed refetch still drops
|
||||
* them — the picks are stale once the reset landed).
|
||||
* • the live brand re-apply (phase 190, task 03 — A2) — after BOTH
|
||||
* settle paths (Save success and Reset success) the open document
|
||||
* re-reads `GET /api/config` through `window.applyBrandFromConfig`,
|
||||
* the ONE brand pass brand.js owns and the boot runs (this file
|
||||
* never writes the title / brand / footer / composer itself). The tab
|
||||
* no longer EDITS the strings (A1) and its PUT never writes those
|
||||
* columns — so the re-read is the SELF-HEAL half: whatever the server
|
||||
* resolves (panel row → the legacy tab row → env) is what the open
|
||||
* page shows the moment the settle lands, including a name the panel
|
||||
* changed while this tab was open. One request at a time; a failed
|
||||
* re-apply leaves the document as it was (never a blanked brand).
|
||||
* • WCAG contrast (the phase-93 design's EIGHT pairs — the pairs
|
||||
* the layout actually pairs, see app/core/theming.py's docstring —
|
||||
* this file's PAIRS is its client-side mirror, and the two must
|
||||
@@ -168,21 +186,22 @@ export async function mount(root) {
|
||||
const SAVE_LABEL = "Save theme";
|
||||
const RESET_LABEL = "Reset to defaults";
|
||||
|
||||
/* The 20 form fields, in the form's order: `field` is the API key
|
||||
(the input's name attribute), `id` the E2E-stable element id,
|
||||
`kind` how the value is read for a PUT — a string field that is
|
||||
empty after the trim sends null (the server stores NULL = "use
|
||||
the default"); a color field always sends its current #rrggbb
|
||||
(the server's built-in→NULL normalization keeps the row empty
|
||||
when the owner saves the defaults). The 17 color fields follow
|
||||
the server's theming.COLOR_FIELDS order (the 9 identity
|
||||
variables, then the 8 semantic state variables — phase 93), so
|
||||
everything FIELDS-driven (live preview, collectBody,
|
||||
/* The 17 color form fields, in the form's order: `field` is the API
|
||||
key (the input's name attribute), `id` the E2E-stable element id,
|
||||
`kind` how the value is read for a PUT — a color field always
|
||||
sends its current #rrggbb (the server's built-in→NULL
|
||||
normalization keeps the row empty when the owner saves the
|
||||
defaults). THE 3 BRAND STRINGS ARE NOT HERE (phase 190, A1 —
|
||||
owner-confirmed 2026-10-18): the admin Settings panel is the
|
||||
single writable owner of app_name / input_placeholder /
|
||||
footer_text, so the tab renders no string input and its PUT body
|
||||
carries no string key — a Theme-tab save can never re-bake a name
|
||||
and shadow the panel (the TODO.md L3 incident). The 17 color
|
||||
fields follow the server's theming.COLOR_FIELDS order (the 9
|
||||
identity variables, then the 8 semantic state variables — phase
|
||||
93), so everything FIELDS-driven (live preview, collectBody,
|
||||
applyServedTheme's tag content) stays in COLOR_FIELDS order. */
|
||||
const FIELDS = [
|
||||
{ field: "app_name", id: "theme-app-name", kind: "string" },
|
||||
{ field: "input_placeholder", id: "theme-placeholder", kind: "string" },
|
||||
{ field: "footer_text", id: "theme-footer", kind: "string" },
|
||||
{ field: "bg", id: "theme-bg", kind: "color" },
|
||||
{ field: "surface", id: "theme-surface", kind: "color" },
|
||||
{ field: "ink", id: "theme-ink", kind: "color" },
|
||||
@@ -324,8 +343,8 @@ export async function mount(root) {
|
||||
* inline style beats the served <style id="bor-theme"> :root, so
|
||||
* the whole page repaints live while the owner is picking. An empty
|
||||
* value removes the override (the page falls back to the served
|
||||
* theme). Text fields write NOTHING here — the strings apply via
|
||||
* brand.js on the next page load (the sub-copy says so). */
|
||||
* theme). Only the 17 color pickers bind here — the tab has no
|
||||
* string input at all since phase 190 (A1). */
|
||||
function previewColor(field, value) {
|
||||
if (value) {
|
||||
document.documentElement.style.setProperty(cssVar(field), value);
|
||||
@@ -488,9 +507,10 @@ export async function mount(root) {
|
||||
}
|
||||
}
|
||||
|
||||
/* GET /api/ui-settings → populate the 20 inputs with the EFFECTIVE
|
||||
values (the tab always shows the live theme — env defaults when
|
||||
the row is empty) and re-check the eight pairs (a SAVED palette
|
||||
/* GET /api/ui-settings → populate the 17 color inputs with the
|
||||
EFFECTIVE values (the tab always shows the live theme — the
|
||||
built-in palette when the row is empty) and re-check the eight
|
||||
pairs (a SAVED palette
|
||||
can itself fail AA — the warning then tracks it). A failed fetch
|
||||
keeps the static form + shows #theme-error with a retry (the
|
||||
loadHealth house style — never a blanked panel). Returns the
|
||||
@@ -524,17 +544,16 @@ export async function mount(root) {
|
||||
/* ---------- the PUT (Save + Reset share it) ---------- */
|
||||
|
||||
function collectBody() {
|
||||
/* All 20 fields — the 3 strings + the 17 colors (the 9 identity
|
||||
+ the 8 semantic state, phase 93). */
|
||||
/* The 17 colors ONLY (the 9 identity + the 8 semantic state) — no
|
||||
string key ever rides the wire (phase 190, A1: the Settings
|
||||
panel owns the brand strings, and the server ignores them in
|
||||
this body anyway — an old cached client can no longer re-bake
|
||||
one). */
|
||||
const body = {};
|
||||
for (const f of FIELDS) {
|
||||
const input = inputs[f.field];
|
||||
const value = input ? input.value : "";
|
||||
if (f.kind === "string") {
|
||||
body[f.field] = value.trim() || null; // cleared field → null
|
||||
} else {
|
||||
body[f.field] = isHex(value) ? value : null; // colors: their hex
|
||||
}
|
||||
body[f.field] = isHex(value) ? value : null; // colors: their hex
|
||||
}
|
||||
return body;
|
||||
}
|
||||
@@ -577,6 +596,39 @@ export async function mount(root) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- the live brand re-apply (phase 190, task 03 — A2) ----------
|
||||
* The reconcile above owns the COLORS; the three brand STRINGS are the
|
||||
* Settings panel's (A1), and this tab's PUT never writes those columns.
|
||||
* The re-read is still owed: the effective string can move under an
|
||||
* open tab (the panel saved or reset one, another browser changed the
|
||||
* row), and every settle is the moment the document is re-branded to
|
||||
* what the server actually resolves. Both settle paths therefore run the
|
||||
* SAME shared
|
||||
* pass brand.js owns and the boot runs:
|
||||
* fetch("/api/config") → window.applyBrandFromConfig(cfg). One code
|
||||
* path for Save and Reset (the PUT body difference is the colors), one
|
||||
* request at a time (§7.4 — a second settle never stacks a second
|
||||
* fetch), and NEVER a blanked brand: a non-2xx or an unreachable app
|
||||
* returns without touching the DOM, leaving the last applied name,
|
||||
* footer and placeholder (the save's own status / error line is the
|
||||
* visibility). */
|
||||
let brandReapplyInFlight = false;
|
||||
|
||||
async function reapplyBrand() {
|
||||
if (brandReapplyInFlight) return;
|
||||
if (typeof window.applyBrandFromConfig !== "function") return; // brand.js is the owner
|
||||
brandReapplyInFlight = true;
|
||||
try {
|
||||
const r = await fetch("/api/config", { cache: "no-store" });
|
||||
if (!r.ok) return; // the brand keeps its last applied state
|
||||
window.applyBrandFromConfig(await r.json());
|
||||
} catch {
|
||||
/* unreachable app or a non-JSON body — nothing is written */
|
||||
} finally {
|
||||
brandReapplyInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTheme() {
|
||||
const outcome = await putSettings(collectBody(), "Saving…", saveBtn);
|
||||
if (!outcome.ok) {
|
||||
@@ -592,11 +644,12 @@ export async function mount(root) {
|
||||
/* A failed refetch keeps the current <html> overrides on purpose:
|
||||
the PUT body came from these very inputs, so they ARE the saved
|
||||
palette — never revert onto the stale tag (the phase-91 defect). */
|
||||
await reapplyBrand(); // the strings ride the same settle (task 03)
|
||||
}
|
||||
|
||||
async function resetTheme() {
|
||||
const body = {};
|
||||
for (const f of FIELDS) body[f.field] = null; // all null = the defaults
|
||||
for (const f of FIELDS) body[f.field] = null; // 17 nulls = the defaults
|
||||
const outcome = await putSettings(body, "Resetting…", resetBtn);
|
||||
if (!outcome.ok) {
|
||||
showError(outcome.detail);
|
||||
@@ -611,6 +664,11 @@ export async function mount(root) {
|
||||
clearPreview(); // the reset landed — the picks are stale
|
||||
restoreFavicon(); // stale picks — back to the route URL
|
||||
}
|
||||
/* The palette row is back to the defaults — and the open document
|
||||
re-reads the effective brand too, so a string the panel moved
|
||||
while this tab was open paints now, not on the next load
|
||||
(phase 190, task 03). */
|
||||
await reapplyBrand();
|
||||
}
|
||||
|
||||
/* ---------- view boot (phase 91 task 05) ----------
|
||||
@@ -642,10 +700,9 @@ export async function mount(root) {
|
||||
|
||||
/* Bindings — armed BEFORE the first load: a fast owner can start
|
||||
picking while the GET is still out; the preview writes are
|
||||
idempotent and the settled load re-populates afterwards. Color
|
||||
inputs drive the live preview + the contrast re-check; text
|
||||
inputs drive neither (B4 — the strings apply on the next page
|
||||
load, the sub-copy says so). */
|
||||
idempotent and the settled load re-populates afterwards. The 17
|
||||
color inputs drive the live preview + the contrast re-check (the
|
||||
brand strings are the Settings panel's surface, phase 190 A1). */
|
||||
for (const f of FIELDS) {
|
||||
const input = inputs[f.field];
|
||||
if (!input || f.kind !== "color") continue;
|
||||
|
||||
Reference in New Issue
Block a user