phase: 190_app_name_settings_fix
Build and Push Containers / build-and-push-app (push) Successful in 2m9s
Build and Push Containers / build-and-push-db (push) Successful in 15s

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:
2026-10-18 02:59:19 -04:00
parent 47456a44df
commit c6ffa050a6
59 changed files with 4104 additions and 556 deletions
+77 -38
View File
@@ -3,7 +3,10 @@ pins).
The Theme tab edits ALL 17 palette variables (the 9 identity + the 8
semantic state colors — B3 revised, owner permission 2026-09-10,
TODO.md L3) plus the 3 branding strings: 20 form fields total. The
TODO.md L3) and NOTHING else since phase 190 (task 02, A1: the admin
Settings panel is the single writable owner of the 3 brand strings, so
the tab's "Branding" fieldset, its three string inputs and the string
half of its PUT body are gone). The
live behavior (live preview, the Save/Reset PUT body, the served-theme
sync, the contrast warnings) is E2E-gated (``tests/e2e/
test_admin_theme_tab.py`` + the phase-93 monochrome suite, task 04);
@@ -12,20 +15,25 @@ design depends on, so a silent regression is caught without a browser
(house pattern: ``tests/unit/test_big_read_progress.py`` reads
frontend sources and asserts on their mechanisms):
* ``theme.js`` ``FIELDS`` — exactly 20 entries in the FORM's order
(the 3 branding strings, then the 17 colors in the server's
``theming.COLOR_FIELDS`` order — identity, brand, then state):
everything that iterates FIELDS (live preview, ``collectBody``'s PUT
body, ``clearPreview``, ``applyServedTheme``'s tag content) covers
the 8 semantic pickers automatically only if this order holds;
* ``theme.js`` ``FIELDS`` — exactly 17 entries (the colors ONLY — no
``kind: "string"`` entry may come back) in the FORM's order, the
server's ``theming.COLOR_FIELDS`` order (identity, brand, then
state): everything that iterates FIELDS (live preview,
``collectBody``'s PUT body, ``clearPreview``, ``applyServedTheme``'
tag content) covers the 8 semantic pickers automatically only if this
order holds — and the body ``collectBody`` builds carries no string
key either (the phase-190 pin: an old cached client's save can no
longer re-bake a brand string);
* ``theme.js`` ``PAIRS`` — exactly the EIGHT WCAG 2.1 AA
(4.5:1) pairs: the five identity pairs + the three semantic
ink-on-bg pairs; the two ``*_line`` state variables stay EXCLUDED
(decorative borders, no contrast duty — the same rule as
``--line`` / ``--grid-line``); the docstring mirror is pinned in
``tests/unit/test_theming.py``;
* ``index.html`` ``#view-theme`` — all 20 inputs carry E2E-stable ids
+ visible labels; the 17 color inputs are ``type="color"`` and ship
* ``index.html`` ``#view-theme`` — the 17 color inputs carry E2E-stable
ids + visible labels, and the three removed string inputs stay absent
(``#theme-app-name`` / ``#theme-placeholder`` / ``#theme-footer`` —
the phase-190 A1 strip); the color inputs are ``type="color"`` and ship
the BUILT-IN static values — asserted against
``frontend/assets/styles.css``'s ``:root`` parsed in-test (the
house drift pattern — the same guard ``test_theming.py`` runs on
@@ -88,16 +96,24 @@ def _root_declarations() -> dict[str, str]:
return dict(re.findall(r"(--[a-z-]+)\s*:\s*([^;]+);", block))
# ---------- theme.js: FIELDS — the 20 form fields, in order ----------
# ---------- theme.js: FIELDS — the 17 color form fields, in order ----------
def test_fields_lists_all_20_in_form_order() -> None:
"""``FIELDS`` has EXACTLY 20 entries — the 3 branding strings,
then the 17 color fields — in the form's own order (the palette
fieldset's 9 identity pickers, then the State colors fieldset's 8
semantic pickers), each with its E2E-stable ``theme-*`` id and
kind (the color entries are ``kind: "color"`` — the live preview,
``collectBody``, and the served-theme sync all key on it)."""
def test_fields_lists_the_17_colors_and_no_strings() -> None:
"""``FIELDS`` has EXACTLY 17 entries — the color fields ONLY — in the
form's own order (the palette fieldset's 9 identity pickers, then
the State colors fieldset's 8 semantic pickers), each with its
E2E-stable ``theme-*`` id and kind (the color entries are
``kind: "color"`` — the live preview, ``collectBody``, and the
served-theme sync all key on it).
Phase 190 (task 02, A1) is what makes this a CONTRACT and not a
coincidence: the admin Settings panel is the single writable owner
of ``app_name`` / ``input_placeholder`` / ``footer_text``, so no
entry may carry ``kind: "string"`` — an entry means a populated
input AND a PUT-body key, and a string key is exactly how the
Theme tab used to bake the then-effective name into its row and
shadow every later panel change (``TODO.md`` L3)."""
js = _js()
start = js.index("const FIELDS = [")
end = js.index("];", start)
@@ -105,11 +121,8 @@ def test_fields_lists_all_20_in_form_order() -> None:
r'\{\s*field:\s*"([a-z_]+)",\s*id:\s*"(theme-[a-z-]+)",\s*kind:\s*"([a-z]+)"\s*\}',
js[start:end],
)
assert len(fields) == 20, f"FIELDS must list exactly 20 entries, got {len(fields)}"
assert len(fields) == 17, f"FIELDS must list exactly 17 entries, got {len(fields)}"
assert [(f, i) for f, i, _ in fields] == [
("app_name", "theme-app-name"),
("input_placeholder", "theme-placeholder"),
("footer_text", "theme-footer"),
("bg", "theme-bg"),
("surface", "theme-surface"),
("ink", "theme-ink"),
@@ -127,11 +140,14 @@ def test_fields_lists_all_20_in_form_order() -> None:
("accent_bg", "theme-accent-bg"),
("accent_ink", "theme-accent-ink"),
("accent_line", "theme-accent-line"),
], "FIELDS must list the 20 fields in the form's order"
assert all(kind == "color" for _, _, kind in fields[3:]), (
"the 17 palette entries are all kind color"
], "FIELDS must list the 17 color fields in the form's order"
assert all(kind == "color" for _, _, kind in fields), (
"every palette entry is kind color — no string field is back in the tab"
)
assert all(kind == "string" for _, _, kind in fields[:3])
for name in theming.STRING_FIELDS:
assert f'{{ field: "{name}"' not in js[start:end], (
f"{name} left the Theme tab in phase 190 — the panel owns it"
)
def test_fields_color_order_is_the_server_color_fields_order() -> None:
@@ -153,6 +169,28 @@ def test_fields_color_order_is_the_server_color_fields_order() -> None:
)
def test_collectbody_builds_the_17_colors_and_no_string_keys() -> None:
"""``collectBody`` — the PUT body the Save button sends — is
FIELDS-driven and therefore carries the 17 colors ONLY: no string
key appears anywhere in its source (phase 190, A1), and the old
trimmed-string branch (``value.trim() || null`` — the bake) is gone.
The server ignores body strings defensively too
(``app/api/ui_settings.py``), so this is the client half of "a
Theme-tab save can never shadow the Settings panel again"."""
js = _js()
start = js.index("function collectBody()")
end = js.index("\n }", start)
body_src = js[start:end]
assert body_src, "collectBody must exist"
for name in theming.STRING_FIELDS:
assert name not in body_src, f"{name} must never ride the PUT body"
assert "value.trim()" not in body_src, "the string-trim bake branch must be gone"
assert "for (const f of FIELDS)" in body_src, (
"the body stays FIELDS-driven — one ordering source for the PUT"
)
assert "isHex(value)" in body_src, "the colors keep their hex-or-null rule"
# ---------- theme.js: PAIRS — exactly the eight WCAG pairs ----------
@@ -197,23 +235,24 @@ def test_line_vars_are_excluded_from_pairs() -> None:
)
# ---------- index.html: the #view-theme form (20 inputs) ----------
# ---------- index.html: the #view-theme form (17 color inputs) ----------
def test_theme_view_carries_all_20_labeled_inputs() -> None:
"""All 20 inputs are STATIC markup in the shell (the E2E-stable
selectors convention): each with a visible ``<label for>``; the 17
palette inputs are ``type="color"`` (the 3 branding inputs
``type="text"``)."""
def test_theme_view_carries_the_17_labeled_color_inputs() -> None:
"""The 17 color inputs are STATIC markup in the shell (the
E2E-stable selectors convention), each with a visible
``<label for>`` and ``type="color"`` — and NO string input is in the
view (phase 190, task 02: the "Branding" fieldset and its
``#theme-app-name`` / ``#theme-placeholder`` / ``#theme-footer``
inputs are gone, so the tab can neither show nor send a brand
string; the same absence is asserted in the browser by
``tests/e2e/test_admin_theme_tab.py``)."""
body = _theme_view(_html())
text_ids = ("theme-app-name", "theme-placeholder", "theme-footer")
for field_id in text_ids:
assert re.search(rf'<label[^>]*for="{field_id}"[^>]*>', body), (
f"missing the visible label for #{field_id}"
)
assert re.search(rf'<input[^>]*id="{field_id}"[^>]*type="text"[^>]*>', body), (
f"#{field_id} must be a text input"
for field_id in ("theme-app-name", "theme-placeholder", "theme-footer"):
assert f'id="{field_id}"' not in body, (
f"#{field_id} must be gone from the Theme tab (phase 190 A1)"
)
assert f'for="{field_id}"' not in body, f"#{field_id}'s label must be gone"
for field in theming.COLOR_FIELDS:
field_id = f"theme-{field.replace('_', '-')}"
assert re.search(rf'<label[^>]*for="{field_id}"[^>]*>', body), (