phase: 91_admin_theme_tab
Build and Push Containers / build-and-push-app (push) Successful in 5m43s
Build and Push Containers / build-and-push-db (push) Successful in 12s

All verification is complete — this pass needed no code changes. Final report:

**Phase 91 — Admin Theme tab: final verification pass (all 6 tasks already in `complete/`)**

- Verified pre-paint theming end-to-end: `ui_settings` store + resolver, admin `GET/PUT /api/ui-settings`, `CachingMiddleware` inline-`<style id="bor-theme">` injection before `</head>` (incl. `/shared/<token>` prefix branch, unit-pinned), CSP sha256 exemption for the inline tag, Theme tab shell + `theme.js` editor, CSS-file theming fully retired.
- No defects found; zero changes made — working tree left exactly as the task executors left it.
- Tests: `uv run pytest --cov=app` → 1841 passed, 0 failed (TOTAL coverage **99%**; theming/ui_settings/caching all 100%); `uv run pytest tests/e2e/test_admin_theme_tab.py -v --no-cov` → **5 passed** in isolation.
- Lint/types: `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings.
- Criteria: (1) unset deployment byte-identical, no `#bor-theme` anywhere — ✓ (unit no-op test + E2E reset byte-compare); `rg "BOR_THEME|themes/"` → single hit is the permitted doc-history comment in `frontend/index.html`. (2) admin-only gate + 403s for anonymous and token users — ✓ (E2E test 3). (3) saved theme inline before `</head>` on every page incl. `/shared/<token>`, computed `--brand` on first paint for admin + anonymous — ✓ (E2E test 2 + unit). (4) reset → byte-identical; 5 contrast pairs warn <4.5:1, non-blocking — ✓ (E2E tests 4–5). (5) suite green, >90% coverage, lint clean — ✓. (6) commit deferred to harness per rules.
- Notable: `.agents/PLAN.md` is absent from the repo — the phase overview's Design section was used as the binding spec; no deviation resulted.
- Next pending phase: **none** — 91 is the last phase in `todo/`.
This commit is contained in:
2026-09-09 17:22:24 -04:00
parent 3095c4c577
commit d22d260b8b
74 changed files with 4448 additions and 675 deletions
+173 -3
View File
@@ -104,8 +104,8 @@ def test_view_map_covers_the_shell_paths() -> None:
"""The VIEW map is pathname → view name: the shell's own two URLs
("/" and "/index.html") are the chat view, plus one entry per
folded view (tasks 01–03: tuning, rag, git-sources, history;
phase 79 task 06: tokens — all five non-chat navbar views are
in)."""
phase 79 task 06: tokens; phase 91 task 04: theme — all six
non-chat navbar views are in)."""
js = _js()
view_start = js.find("const VIEW = {")
assert view_start != -1, "the VIEW map must exist"
@@ -123,8 +123,11 @@ def test_view_map_covers_the_shell_paths() -> None:
assert '"/tokens.html": "tokens"' in view_body, (
"phase 79 task 06 folds the Tokens view into the shell"
)
assert '"/theme.html": "theme"' in view_body, (
"phase 91 task 04 folds the Theme view into the shell"
)
# The view names are the #view-<name> section slugs in index.html.
for name in ("chat", "tuning", "history", "tokens"):
for name in ("chat", "tuning", "history", "tokens", "theme"):
assert f'id="view-{name}"' in _html(), f"missing the #view-{name} section"
@@ -218,6 +221,9 @@ def test_only_non_chat_views_have_lazy_modules() -> None:
assert 'tokens: () => import("./tokens.js")' in mods_body, (
"the Tokens view module is lazy-imported on first show"
)
assert 'theme: () => import("./theme.js")' in mods_body, (
"the Theme view module is lazy-imported on first show (phase 91)"
)
assert '"chat"' not in mods_body, "the chat view has no lazy module"
assert 'import("./app.js")' not in js, "app.js must never be lazy-imported"
@@ -275,6 +281,11 @@ def test_router_writes_active_state_title_and_meta() -> None:
assert "Saved chats — every conversation is saved automatically, one click back." in js
assert 'tokens: "Access tokens · Brain of Reese"' in js
assert "Generate and revoke the API tokens that let people use the app." in js
assert 'theme: "Theme · Brain of Reese"' in js
assert (
"Set the palette and branding — the theme is baked into every served page, "
"live on the first paint."
) in js
# The brand composition (phase 39's window.BOR_BRAND, read at
# write time — never a hardcoded stamp).
assert 'window.BOR_BRAND || "Brain of Reese"' in js
@@ -351,6 +362,15 @@ def test_shell_markup_has_one_main_two_views_and_chat_only_active() -> None:
tokens_link = tokens_match.group(0)
assert "hidden" in tokens_link, "#nav-tokens ships hidden (admin-only)"
assert "is-active" not in tokens_link, "no static active stamp on the Tokens link"
# The Theme nav link (phase 91 task 04) ships hidden (admin-only)
# and UNstamped too — the router is the single writer of the active
# state, and a token user (role "user") must never see the link
# (header.js reveals it for admin only).
theme_match = re.search(r'<a[^>]*id="nav-theme"[^>]*>', html)
assert theme_match, "the shell must carry the #nav-theme nav link"
theme_link = theme_match.group(0)
assert "hidden" in theme_link, "#nav-theme ships hidden (admin-only)"
assert "is-active" not in theme_link, "no static active stamp on the Theme link"
# ---------- phase 76 task 04: the header is shell-owned ----------
@@ -869,3 +889,153 @@ def test_tokens_view_scaffold_in_the_shell() -> None:
# The Actions column header is visually-hidden (the row buttons
# carry their own aria-labels — the history-table convention).
assert '<th scope="col"><span class="visually-hidden">Actions</span></th>' in body
# ---------- phase 91 task 04: the Theme view (skeleton) ----------
def test_theme_view_scaffold_in_the_shell() -> None:
"""Phase 91 task 04: the shell carries the #view-theme section —
hidden AND inert + focusable (the WCAG pair, AGENTS.md rule 5) —
with the #theme-gate (the EXACT #sources-gate pattern, ship-hidden,
its Sign in returning to the Theme view via ?next=/theme.html) and
the ship-hidden #theme-content (the #git-sources-content pattern)
holding the STATIC form skeleton: the page-head (h1 "Theme"), the
#theme-form with the 3 labeled branding text inputs (maxlength=300
— the server re-validates) + the 8 labeled type=color palette inputs
(the 8 identity variables, in the theming.COLOR_FIELDS order), the
#theme-save (primary) + #theme-reset (secondary) — BOTH type="button"
(no real submit), and the three task-05 feedback lines: #theme-error
(role=alert), #theme-result (role=status), #theme-contrast
(role=alert) — all ship hidden. The editor behavior (populate,
live preview, Save/Reset, the contrast warnings) lands in task 05;
this pin keeps the E2E-stable skeleton from drifting."""
html = _html()
view = html.find('<section class="view" id="view-theme"')
assert view != -1, "the #view-theme section must be in the shell"
tag_end = html.find(">", view)
tag = html[view:tag_end]
assert "hidden" in tag and "inert" in tag, (
"the folded view ships hidden AND inert"
)
assert 'tabindex="-1"' in tag, "the target view is focusable"
main_end = html.find("</main>", view)
assert view < main_end, "the view section lives inside the single main"
body = html[view:main_end]
# The gate: the exact #sources-gate pattern (class + ship-hidden +
# its ?next= returning to the Theme view — the no-JS fallback).
gate = re.search(r'<section[^>]*id="theme-gate"[^>]*>', body)
assert gate and "hidden" in gate.group(0), "#theme-gate must ship hidden"
assert 'class="sources-gate"' in gate.group(0), (
"the gate reuses the .sources-gate visual language"
)
assert "<h2 id=\"theme-gate-title\">Sign in to change the theme</h2>" in body
assert 'href="/login.html?next=/theme.html"' in body, (
"the gate's Sign in returns to the Theme view (no-JS fallback)"
)
# The content ships hidden (theme.js reveals it for admin only —
# the #git-sources-content pattern).
content = re.search(r'<div[^>]*id="theme-content"[^>]*>', body)
assert content and "hidden" in content.group(0), (
"#theme-content must ship hidden (anonymous-safe)"
)
# The static form skeleton (the E2E-stable-selectors house
# convention): the 3 labeled branding text inputs (maxlength=300)
# and the 8 labeled type=color palette inputs (the 8 identity
# variables — one per theming.COLOR_FIELDS field).
assert re.search(r'<form[^>]*id="theme-form"[^>]*>', body), (
"the #theme-form must be STATIC markup in the shell"
)
for field_id in ("theme-app-name", "theme-placeholder", "theme-footer"):
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}"[^>]*maxlength="300"[^>]*>', body
), f"#{field_id} must be a text input with maxlength=300"
for field_id in (
"theme-bg",
"theme-surface",
"theme-ink",
"theme-ink-soft",
"theme-line",
"theme-brand",
"theme-brand-soft",
"theme-brand-ink",
):
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="color"[^>]*>', body
), f"#{field_id} must be a type=color input"
# Save (primary) + Reset (secondary) — BOTH type="button" (no real
# submit; theme.js owns the onsubmit handling + the §7.4 lifecycle).
save = re.search(r'<button[^>]*id="theme-save"[^>]*>', body)
assert save and 'type="button"' in save.group(0), (
"#theme-save must be a type=button (no real submit)"
)
reset = re.search(r'<button[^>]*id="theme-reset"[^>]*>', body)
assert reset and 'type="button"' in reset.group(0), (
"#theme-reset must be a type=button (no real submit)"
)
assert "Save theme" in body, "the Save button's label"
assert "Reset to defaults" in body, "the Reset button's label"
# The three task-05 feedback lines, all ship hidden.
assert re.search(r'<[^>]*id="theme-error"[^>]*role="alert"[^>]*hidden', body)
assert re.search(r'<[^>]*id="theme-result"[^>]*role="status"[^>]*hidden', body)
assert re.search(r'<[^>]*id="theme-contrast"[^>]*role="alert"[^>]*hidden', body)
def test_theme_nav_link_ships_on_every_page_header() -> None:
"""Phase 91 task 04: the phase-34 one-bar contract — the SAME nav
ships on every page (test_nav_consistency pins the header inventory
PARITY across the shell pages, the document viewer, and the login
page), so #nav-theme (ship-hidden, admin-only) must be in the
#app-nav of EVERY header-bearing page: the shell + document.html +
login.html + shared.html. The doc-edit flow page ships the reduced
header (no admin links at all) and is out of the contract."""
for page in (
FRONTEND / "index.html",
FRONTEND / "document.html",
FRONTEND / "login.html",
FRONTEND / "shared.html",
):
text = page.read_text(encoding="utf-8")
match = re.search(r'<a[^>]*id="nav-theme"[^>]*>', text)
assert match, f"{page.name} must carry the #nav-theme nav link (one-bar)"
link = match.group(0)
assert 'href="/theme.html"' in link, f"{page.name}: the Theme link's href"
assert "hidden" in link, (
f"{page.name}: #nav-theme ships hidden (admin-only)"
)
assert "is-active" not in link, (
f"{page.name}: no static active stamp on the Theme link"
)
def test_header_js_reveals_the_theme_link_for_admin_only() -> None:
"""Phase 91 task 04: header.js reveals #nav-theme for role admin —
the same ship-hidden / reveal-for-admin contract as the other
admin-only links: the two-line reveal (`hidden = !admin`) sits in
initSharedHeader, null-safe (a page without the link is a no-op),
and the gate is the `admin` flag (role === "admin") — a token user
(role "user") never sees the link."""
header_js = (ASSETS / "header.js").read_text(encoding="utf-8")
fn = header_js.find("export async function initSharedHeader")
assert fn != -1, "initSharedHeader must exist"
body = header_js[fn:]
lookup = body.find('document.querySelector("#nav-theme")')
assert lookup != -1, "header.js must look up #nav-theme"
reveal = body.find("navTheme.hidden = !admin")
assert 0 <= lookup < reveal, (
"the reveal must be the two-line pattern: null-safe lookup, "
"then hidden = !admin (the admin flag — role === \"admin\")"
)
# The lookup + reveal sit AFTER the whoami resolution (the admin
# flag exists only once fetchWhoami has settled).
whoami = body.find("const whoami = await fetchWhoami()")
admin_flag = body.find('const admin = whoami.role === "admin"')
assert 0 <= whoami < admin_flag < lookup, (
"the reveal keys off the resolved admin flag"
)