phase: 91_admin_theme_tab
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:
@@ -54,6 +54,12 @@
|
||||
view). No mobile dropdown copy is needed: the link lives
|
||||
in the SAME #app-nav element the hamburger opens. -->
|
||||
<a href="/tokens.html" class="nav-link" id="nav-tokens" hidden>Tokens</a>
|
||||
<!-- Phase 91 (task 04): the Theme link is admin-only — hidden
|
||||
by default, header.js reveals it once whoami says admin,
|
||||
exactly like the Tokens link above (the shell's seventh
|
||||
view). No mobile dropdown copy is needed: the link lives
|
||||
in the SAME #app-nav element the hamburger opens. -->
|
||||
<a href="/theme.html" class="nav-link" id="nav-theme" hidden>Theme</a>
|
||||
<!-- Phase 46 (mobile dropdown copy: sign-in — desktop bar copy is
|
||||
outside the nav; see styles.css .sign-in-mobile rules). -->
|
||||
<a href="/login.html?next=/" class="auth-link sign-in-link sign-in-mobile" id="sign-in-link-mobile" hidden>
|
||||
@@ -943,6 +949,177 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Phase 91 (task 04): the Theme view — the shell's seventh
|
||||
folded view (the phase-76 fold pattern, the phase-79 Tokens
|
||||
view as the most recent precedent): the admin-only palette +
|
||||
branding editor. /theme.html serves THIS document (the shell
|
||||
route in app/main.py); the router shows this section for that
|
||||
pathname. The CSS-file theming (BOR_THEME, phase 62) is
|
||||
retired (task 03): the effective theme is injected into every
|
||||
served page's <head> server-side (app/core/caching.py +
|
||||
app/core/theming.py), so it paints on the FIRST paint — no
|
||||
red flash, no pop-in. SHIPS hidden (anonymous-safe — the gate
|
||||
is what anonymous sees; theme.js reveals the content for
|
||||
admin only). The hidden + inert pair is the WCAG contract:
|
||||
a hidden view must not receive focus or keyboard traversal
|
||||
(AGENTS.md rule 5). Mounted lazily — assets/router.js imports
|
||||
theme.js on first show only (mount-once, hide-forever; the
|
||||
editor lands in task 05). The form is STATIC markup (the
|
||||
E2E-stable-selectors house convention) — the onsubmit
|
||||
binding + live preview + Save/Reset lifecycle land in task 05
|
||||
(theme.js); there is no real submit (every button is
|
||||
type="button"). -->
|
||||
<section class="view" id="view-theme" hidden inert aria-label="Theme" tabindex="-1">
|
||||
<div class="container theme-shell">
|
||||
<!-- Phase 91 (task 04): anonymous sign-in gate — the EXACT
|
||||
#sources-gate pattern (phase 16) and the same .sources-gate
|
||||
visual language: the palette + branding is what the login
|
||||
locks (B5, owner-locked 2026-09-09). Visible for
|
||||
anonymous, hidden for the admin (theme.js). The gate's
|
||||
Sign in returns to the Theme view (the header's ?next=
|
||||
convention; the static href is the no-JS fallback). -->
|
||||
<section class="sources-gate" id="theme-gate" aria-labelledby="theme-gate-title" hidden>
|
||||
<div class="sources-gate-glyph" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="10" width="16" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/><circle cx="12" cy="14.5" r="1.4" fill="currentColor" stroke="none"/><path d="M12 16v2"/></svg>
|
||||
</div>
|
||||
<h2 id="theme-gate-title">Sign in to change the theme</h2>
|
||||
<p class="sources-gate-sub">
|
||||
The palette and branding are admin-only. Chat — and any
|
||||
document an answer cites — stays open to everyone.
|
||||
</p>
|
||||
<a class="sources-gate-link" href="/login.html?next=/theme.html">Sign in</a>
|
||||
</section>
|
||||
|
||||
<!-- Phase 91 (task 04): the editor — SHIPS hidden
|
||||
(anonymous-safe; the gate is what anonymous sees). theme.js
|
||||
reveals it once the cached whoami says admin (the
|
||||
#git-sources-content pattern). -->
|
||||
<div id="theme-content" hidden>
|
||||
<div class="page-head">
|
||||
<h1>Theme</h1>
|
||||
<p class="page-sub">
|
||||
Changes preview live as you pick;
|
||||
<strong>Save theme</strong> bakes the palette into every
|
||||
page it is served on — it applies on the first paint, no
|
||||
pop-in.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Static form skeleton (E2E-stable selectors; task 05 wires
|
||||
the bindings — effective-value populate, the live preview,
|
||||
the §7.4 Save/Reset lifecycle, the WCAG contrast warnings
|
||||
in #theme-contrast). No real submit: both buttons are
|
||||
type="button"; maxlength=300 mirrors the server's 300-char
|
||||
limit (the server re-validates — 422 naming the field).
|
||||
The color inputs ship the BUILT-IN values (app/core/
|
||||
theming.py BUILTIN_COLORS) — task 05 re-populates them
|
||||
with the EFFECTIVE values on mount. -->
|
||||
<form id="theme-form" novalidate>
|
||||
<fieldset class="theme-group">
|
||||
<legend class="theme-group-title">Branding</legend>
|
||||
<!-- B4 (owner-locked): the strings keep the brand.js
|
||||
runtime application — they apply via the /api/config
|
||||
boot fetch on the NEXT page load; the live preview
|
||||
covers the palette only. An empty field restores the
|
||||
default (the env value). -->
|
||||
<p class="theme-note">
|
||||
These strings apply on the next page load — the live
|
||||
preview covers the palette only. Leaving a field empty
|
||||
restores its default.
|
||||
</p>
|
||||
<label for="theme-app-name">App name</label>
|
||||
<input
|
||||
id="theme-app-name"
|
||||
name="app_name"
|
||||
type="text"
|
||||
maxlength="300"
|
||||
autocomplete="off"
|
||||
>
|
||||
<label for="theme-placeholder">Chat input placeholder</label>
|
||||
<input
|
||||
id="theme-placeholder"
|
||||
name="input_placeholder"
|
||||
type="text"
|
||||
maxlength="300"
|
||||
autocomplete="off"
|
||||
>
|
||||
<label for="theme-footer">Footer line</label>
|
||||
<input
|
||||
id="theme-footer"
|
||||
name="footer_text"
|
||||
type="text"
|
||||
maxlength="300"
|
||||
autocomplete="off"
|
||||
>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="theme-group">
|
||||
<!-- The five text/background pairs are checked against
|
||||
WCAG 2.1 AA (4.5:1) as the owner picks (theme.js —
|
||||
the app/core/theming.py docstring is the authoritative
|
||||
pair table); failures list in #theme-contrast as a
|
||||
warning and never block a save. -->
|
||||
<legend class="theme-group-title">
|
||||
Palette — five pairs checked against WCAG 2.1 AA (4.5:1)
|
||||
</legend>
|
||||
<div class="theme-colors">
|
||||
<div class="theme-color">
|
||||
<label for="theme-bg">Background (--bg)</label>
|
||||
<input id="theme-bg" name="bg" type="color" value="#0f0a0a">
|
||||
</div>
|
||||
<div class="theme-color">
|
||||
<label for="theme-surface">Surface (--surface)</label>
|
||||
<input id="theme-surface" name="surface" type="color" value="#1a0f0f">
|
||||
</div>
|
||||
<div class="theme-color">
|
||||
<label for="theme-ink">Text (--ink)</label>
|
||||
<input id="theme-ink" name="ink" type="color" value="#f0e6e6">
|
||||
</div>
|
||||
<div class="theme-color">
|
||||
<label for="theme-ink-soft">Secondary text (--ink-soft)</label>
|
||||
<input id="theme-ink-soft" name="ink_soft" type="color" value="#b8a8a8">
|
||||
</div>
|
||||
<div class="theme-color">
|
||||
<label for="theme-line">Border (--line)</label>
|
||||
<input id="theme-line" name="line" type="color" value="#2d1a1a">
|
||||
</div>
|
||||
<div class="theme-color">
|
||||
<label for="theme-brand">Brand accent (--brand) — buttons, links</label>
|
||||
<input id="theme-brand" name="brand" type="color" value="#f43f5e">
|
||||
</div>
|
||||
<div class="theme-color">
|
||||
<label for="theme-brand-soft">Brand tint (--brand-soft)</label>
|
||||
<input id="theme-brand-soft" name="brand_soft" type="color" value="#2d0a0a">
|
||||
</div>
|
||||
<div class="theme-color">
|
||||
<label for="theme-brand-ink">Brand text (--brand-ink)</label>
|
||||
<input id="theme-brand-ink" name="brand_ink" type="color" value="#fca5a5">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Save (primary) + Reset to defaults (secondary). Both
|
||||
type="button" (no real submit); task 05 runs the §7.4
|
||||
never-stale lifecycle ("Saving…" while the PUT is out,
|
||||
re-enabled on success AND failure). -->
|
||||
<div class="theme-actions">
|
||||
<button type="button" id="theme-save">Save theme</button>
|
||||
<button type="button" class="theme-reset" id="theme-reset">Reset to defaults</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- task 05 owns all three lines: #theme-error (the server's
|
||||
422 detail — the fields are kept on failure), #theme-result
|
||||
("Theme saved." / "Reset to the built-in theme."),
|
||||
#theme-contrast (the WCAG warnings for the five pairs —
|
||||
warning-only, the owner can still save). -->
|
||||
<p class="theme-error" id="theme-error" role="alert" hidden></p>
|
||||
<p class="theme-result" id="theme-result" role="status" aria-live="polite" hidden></p>
|
||||
<p class="theme-contrast" id="theme-contrast" role="alert" hidden></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Phase 79 (task 05): the in-app token gate — a body-level
|
||||
|
||||
Reference in New Issue
Block a user