Files
brain-of-reese/.agent/phases/complete/62_ui_customization/00_phase.md
T

7.8 KiB
Raw Blame History

Phase 62 — UI Customization (deploy with your own look)

Source: TODO.md L3 — "Allow UI customization. This is brain of reese, but I want anyone to be able to deploy it with their name. The project should support an environment variable to call it 'Brain of ' along with custom message-input placeholder, custom footer-inner text, custom color themes, and anything else that an end-user might want to customize." Story: n/a (TODO-derived — owner roadmap confirmation 2026-09-01; builds on the COMPLETED phase-39 capability, whose dir was never filed under complete/ — the code + tests/e2e/test_configurable_brand.py exist, commit fe55be0) Context: The app-name half of this TODO item is DONE (phase 39): BOR_APP_NAME → Settings.app_name (app/config.py L43) → GET /api/config (app/api/config.py, public, returns {app_name, version, docs_repo_configured}) → the classic frontend/assets/brand.js layer (parse-time window.BOR_BRAND, one cache: "no-store" fetch whose never-rejecting promise is window.BOR_CONFIG_PROMISE, DOM passes for title / .brand-text / text nodes / aria-label+placeholder+meta attributes; fetch failure ⇒ default stands + console.warn, the page never breaks). This phase extends that EXACT mechanism to three more settings — no new network surface, no new script, no templating engine (the frontend stays StaticFiles(html=True), app/main.py L101):

  • Composer placeholder: #message-input exists only on frontend/index.html (the chat page).
  • Footer text: after phase 61, ALL 9 pages carry <span class="footer-text">…</span> inside .footer-inner.
  • Theme: the entire palette is :root custom properties in frontend/assets/styles.css L7–25 (--bg, --surface, --ink, --ink-soft, --line, --brand, --brand-soft, --brand-ink, --accent-bg, --accent-ink, --accent-line, --ok-bg, --ok-ink, --err-bg, --err-ink, --err-line) — a theme file overriding those variables is a drop-in; no component CSS references literal colors except via the variables (spot-verify while authoring the example theme).
  • No-CDN rule (AGENTS.md rule 6): theme CSS is served from the FastAPI static dir (frontend/ in dev via BOR_STATIC_DIR; /app/static in the container).
  • Containerfile stage 1 esbuild/minify chain + per-page HTML copy does NOT ship a new frontend/assets/themes/ dir — one line is added (task 03, A7).
  • Contract tests that must be updated when /api/config grows: tests/integration/test_api.py L21–46 (exact key set, two tests) and tests/e2e/test_configurable_brand.py L150 + L158 (exact key set); tests/unit/test_frontend_brand.py pins the brand layer.
  • The phase-39 E2E boots its own second instance (testy_server, APP_PORT + 1, BOR_APP_NAME override) — phase 62's E2E reuses that pattern on a distinct port (task 05).
  • .agent/ is UNTRACKED (owner instruction, commit 281f355) — the phase commit stages app/ frontend/ tests/ .env.example README.md Containerfile.

Objective

Anyone can deploy BOR under their own identity with env vars alone: the app name (already shipped) plus a custom composer placeholder (BOR_INPUT_PLACEHOLDER), custom footer line (BOR_FOOTER_TEXT), and a custom color theme (BOR_THEME=indigo.css → frontend/assets/themes/indigo.css). Unset ⇒ byte-identical to the phase-61 neutral defaults.

Dependencies

  • 61_stale_ui_copy (todo, preceding — FUNCTIONAL: phase 61's neutral strings become these Settings' DEFAULTS, and its .footer-text class is the footer selector this phase drives; the "unset ⇒ byte-identical" contract is measured against phase-61 copy)

Tasks

  1. 01_config_settings.md — three new Settings fields (+ fail-loud theme filename validator) + three new GET /api/config keys; update the exact-key-set contract tests (integration + the phase-39 E2E).
  2. 02_brand_layer_customization.md — brand.js applies placeholder / footer text / theme link from the same boot fetch; unit pins.
  3. 03_theme_assets.md — frontend/assets/themes/indigo.css example theme (WCAG-AA palette) + themes/README.md authoring guide + the one Containerfile line.
  4. 04_docs.md — .env.example + README env table + "Customizing the look" section.
  5. 05_e2e_customization.md — tests/e2e/test_ui_customization.py (own instance, all three vars set; default instance byte-identical; malformed-theme fail-loud), regression suites, one atomic commit.

Testing & Quality

  • Unit: theme validator (empty ok; bare filename ok; path/../non-.css rejected naming the value); GET /api/config returns the six keys with defaults AND with overrides; brand-layer JS pins (house style).
  • Integration: the config contract tests updated to the six-key set (task 01).
  • E2E (mandatory, house rule): tests/e2e/test_ui_customization.py, run in isolation — overridden instance shows custom placeholder/footer/computed theme color on the pages; the shared default server is byte-identical (default placeholder/footer, NO theme link, built-in --brand).
  • Coverage: >90% on app/ (validate.sh gate).

Completion Criteria

  • GET /api/config returns exactly {app_name, version, docs_repo_configured, input_placeholder, footer_text, theme} — proven by integration + E2E.
  • With BOR_INPUT_PLACEHOLDER / BOR_FOOTER_TEXT / BOR_THEME=indigo.css set: the chat composer placeholder, the footer line on multiple pages, and the computed :root --brand all reflect the overrides; a <link href="/assets/themes/indigo.css"> is present.
  • With nothing set: placeholder Ask me anything…, footer Powered by self-hosted models, no theme link, --brand = built-in #f43f5e — the phase-39/61 no-op contract holds.
  • BOR_THEME=../evil.css (or any malformed value) refuses startup, naming the value (phase-56 house style).
  • uv run pytest green; coverage TOTAL >90%; uv run ruff check . && uv run pyright clean.
  • uv run pytest tests/e2e/test_ui_customization.py -v --no-cov green in isolation (DB up).
  • Regression E2E suites green in isolation: test_configurable_brand.py, test_stale_ui_copy.py, test_smoke.py, test_dark_tech_theme.py.
  • frontend/assets/themes/ ships in the container image (Containerfile line) and serves in dev (static dir).
  • One --no-gpg-sign commit; phase dir moved to .agent/phases/complete/ (.agent/ stays untracked).

Locked decisions

  • Owner-locked (2026-09-01, roadmap confirmation, A4): env var names — BOR_INPUT_PLACEHOLDER, BOR_FOOTER_TEXT, BOR_THEME (theme value = a FILENAME, e.g. indigo.css).
  • Owner-locked (2026-09-01, roadmap confirmation, A5): the theme mechanism — BOR_THEME names a file under frontend/assets/themes/ served statically by FastAPI (no-CDN intact); the file overrides the :root palette variables; the example theme is indigo.css (dark indigo/slate, every text/background pair ≥4.5:1); a MALFORMED BOR_THEME (anything not matching ^[a-z0-9_-]+\.css$) fails startup loudly, naming the value (phase-56 house style); a theme file MISSING at runtime degrades to the built-in theme (console.warn, the page never breaks — the loadHealth/brand.js house style).
  • Owner-locked (2026-09-01, roadmap confirmation, A6): the TODO's open-ended "anything else that an end-user might want to customize" is scoped to these three vars + the existing BOR_APP_NAME / BOR_SUGGESTIONS; the settings → /api/config → brand.js path is the extension point for future items (no further surface invented now).
  • Owner-locked (2026-09-01, roadmap confirmation, A7): the Containerfile ships themes via cp -r ./assets/themes /out/assets/themes in stage 1 (no per-file esbuild minify — a future theme file needs no Containerfile edit).

Commit

git add app/ frontend/ tests/ .env.example README.md Containerfile && git commit --no-gpg-sign -m "feat(web): customizable placeholder, footer text, and color theme via BOR_* env vars"