# 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 `…` 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). - `.agents/` 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 `` 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 `.agents/phases/complete/` (`.agents/` 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 ```bash 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" ```