Standardize on the .agents/ directory (shared with project skills): phases/, user_stories/, reports/, screenshots/, validate.sh, and phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves history; runtime artifacts move alongside). Updates every reference in AGENTS.md, README.md, .gitignore, app docstrings, and test story headers. Historical KB content in data/ and the runtime pipeline.log transcript are left untouched.
7.8 KiB
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-inputexists only onfrontend/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
:rootcustom properties infrontend/assets/styles.cssL7–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 viaBOR_STATIC_DIR;/app/staticin the container). Containerfilestage 1 esbuild/minify chain + per-page HTML copy does NOT ship a newfrontend/assets/themes/dir — one line is added (task 03, A7).- Contract tests that must be updated when
/api/configgrows:tests/integration/test_api.pyL21–46 (exact key set, two tests) andtests/e2e/test_configurable_brand.pyL150 + L158 (exact key set);tests/unit/test_frontend_brand.pypins the brand layer. - The phase-39 E2E boots its own second instance (
testy_server,APP_PORT + 1,BOR_APP_NAMEoverride) — phase 62's E2E reuses that pattern on a distinct port (task 05). .agents/is UNTRACKED (owner instruction, commit281f355) — the phase commit stagesapp/ 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-textclass is the footer selector this phase drives; the "unset ⇒ byte-identical" contract is measured against phase-61 copy)
Tasks
01_config_settings.md— three newSettingsfields (+ fail-loudthemefilename validator) + three newGET /api/configkeys; update the exact-key-set contract tests (integration + the phase-39 E2E).02_brand_layer_customization.md—brand.jsapplies placeholder / footer text / theme link from the same boot fetch; unit pins.03_theme_assets.md—frontend/assets/themes/indigo.cssexample theme (WCAG-AA palette) +themes/README.mdauthoring guide + the oneContainerfileline.04_docs.md—.env.example+ README env table + "Customizing the look" section.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:
themevalidator (empty ok; bare filename ok; path/../non-.cssrejected naming the value);GET /api/configreturns 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/configreturns 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.cssset: the chat composer placeholder, the footer line on multiple pages, and the computed:root --brandall reflect the overrides; a<link href="/assets/themes/indigo.css">is present. - With nothing set: placeholder
Ask me anything…, footerPowered 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 pytestgreen; coverage TOTAL >90%;uv run ruff check . && uv run pyrightclean.uv run pytest tests/e2e/test_ui_customization.py -v --no-covgreen 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-signcommit; 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_THEMEnames a file underfrontend/assets/themes/served statically by FastAPI (no-CDN intact); the file overrides the:rootpalette variables; the example theme isindigo.css(dark indigo/slate, every text/background pair ≥4.5:1); a MALFORMEDBOR_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.jspath 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/themesin 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"