# Task 01 — Three new Settings + `/api/config` keys (+ validator) **Phase:** `62_ui_customization` · **Source:** `TODO.md:3` — "…an environment variable to call it 'Brain of ' along with custom message-input placeholder, custom footer-inner text, custom color themes…" **Story:** n/a (TODO-derived) ## Objective The backend half of the customization layer: three new `BOR_`-prefixed settings with phase-61's neutral strings as DEFAULTS, surfaced through the existing public `GET /api/config` boot fetch — with the existing exact-key-set contract tests moved to the new six-key set so the suite stays green from this task on. ## Work 1. `app/config.py` — in the `# --- App ---` block (after `static_dir`, ~L47), add: ```python # --- UI customization (phase 62, TODO L3) --- # Defaults are the phase-61 neutral copy — UNSET => byte-identical UI. input_placeholder: str = "Ask me anything…" footer_text: str = "Powered by self-hosted models" #: Theme file NAME under frontend/assets/themes/ (e.g. "indigo.css"); #: empty = the built-in dark-tech palette. Validated: bare filename #: only — no paths, no ".." (no-CDN: served from the static dir). theme: str = "" ``` plus a `@field_validator("theme")` (pydantic v2, `mode="after"`): empty string passes; the value must match `^[a-z0-9_-]+\.css$` (ASCII, lowercase, bare filename) or the validator raises `ValueError` naming the offending value and the allowed shape (phase-56 fail-loud house style — a typo in `.env` must kill startup, not silently 404). - `input_placeholder` / `footer_text`: no validation beyond being strings (empty ⇒ the template default stands, handled by brand.js treating empty as "skip"). 2. `app/api/config.py` — the `GET /api/config` response grows to exactly six keys: ```python return { "app_name": settings.app_name, "version": settings.app_version, "docs_repo_configured": settings.docs_configured, "input_placeholder": settings.input_placeholder, "footer_text": settings.footer_text, "theme": settings.theme, } ``` (values passed through verbatim — the frontend brand layer decides "empty = keep the template default"; NO new secrets surface: these are display strings, same public posture as `app_name`). Update the module + endpoint docstrings to cite phase 62. 3. Contract-test updates (MUST land with this task or the suite goes red): - `tests/integration/test_api.py` L21–46: both exact-key-set assertions become `{"app_name", "version", "docs_repo_configured", "input_placeholder", "footer_text", "theme"}`; assert the DEFAULT values (`"Ask me anything…"`, `"Powered by self-hosted models"`, `""`); extend `test_config_follows_overridden_app_name`'s pattern with a `test_config_serves_ui_customization_overrides` test (fresh app with `Settings(input_placeholder=…, footer_text=…, theme="indigo.css")` → the three keys reflect the overrides). - `tests/e2e/test_configurable_brand.py` L150 + L158: the two exact-key-set assertions grow to the six-key set (values untouched — that suite's instance has no phase-62 overrides, so the new keys are their defaults). 4. Unit tests — `tests/unit/test_config.py` (existing file, add cases): `theme` validator — `""` ok; `"indigo.css"` ok; `"Indigo.css"` rejected; `"../evil.css"` rejected; `"a/b.css"` rejected; `"indigo"` (no extension) rejected — each rejection names the value. Plus one test that `GET /api/config` (integration) default response carries the three new keys with the locked defaults. ## Testing & Quality - `uv run pytest` green (unit + integration); `uv run ruff check . && uv run pyright` clean. - Coverage: **>90%** on `app/` (validate.sh gate). ## Completion Criteria - [ ] `Settings` exposes `input_placeholder` / `footer_text` / `theme` with the locked defaults; `BOR_THEME=../evil.css` in the env fails app startup, naming the value (verified once via a unit/validator test — the E2E boots-check lands in task 05). - [ ] `GET /api/config` returns exactly the six keys (defaults + overrides) — integration tests green. - [ ] `tests/e2e/test_configurable_brand.py`'s key-set assertions updated (suite itself re-run in task 05's regression pass, DB up). - [ ] Full suite green, lint + types clean, coverage >90%.