feat(sources): real-time file progress for sync and upload — background upload with success toast
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# 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 <name>' 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
|
||||
```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"
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# 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 <name>' 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%.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Task 02 — `brand.js` applies the placeholder, footer text, and theme
|
||||
|
||||
**Phase:** `62_ui_customization` · **Source:** `TODO.md:3` — "…custom message-input placeholder, custom footer-inner text, custom color themes…"
|
||||
**Story:** n/a (TODO-derived)
|
||||
|
||||
## Objective
|
||||
The frontend half: the EXISTING `brand.js` boot-fetch pass (no new script, no new network call) applies the three new config values to the DOM — each empty ⇒ skipped, so an unset deployment stays byte-identical.
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/brand.js` — inside the SAME settled-config `.then((cfg) => …)` that `applyBrand()` runs (after the existing `app_name` block — INDEPENDENT of it: the three new keys are applied even when `app_name` is the default/empty):
|
||||
- **Placeholder** — `cfg.input_placeholder` non-empty → `document.querySelector("#message-input")?.setAttribute("placeholder", cfg.input_placeholder)` (chat page only; every other page no-ops via the null guard).
|
||||
- **Footer text** — `cfg.footer_text` non-empty → `document.querySelectorAll(".footer-text").forEach((el) => { el.textContent = cfg.footer_text; })` (all 9 pages, phase-61 hook; `textContent` — no escaping needed, an operator string can't inject markup).
|
||||
- **Theme** — `cfg.theme` non-empty → insert a stylesheet link IMMEDIATELY AFTER the existing `<link rel="stylesheet" href="/assets/styles.css">` (find it by `rel="stylesheet"` + `href` ending in `styles.css`; append after it so the theme overrides win by cascade order):
|
||||
```js
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = "/assets/themes/" + cfg.theme; // filename validated server-side (task 01) — no path input can reach here via /api/config
|
||||
link.id = "theme-override";
|
||||
link.onerror = () => console.warn("brand: theme " + cfg.theme + " did not load — the built-in theme stands.");
|
||||
stylesLink.insertAdjacentElement("afterend", link);
|
||||
```
|
||||
Idempotent: skip insertion if `#theme-override` already exists (the brand pass runs once, but keep the guard — the loadHealth house style: never break the page, never double-apply).
|
||||
- Update the file-header contract comment: a "Phase 62 (owner-locked 2026-09-01, TODO L3)" section documenting the three keys, the empty-skip no-op contract, and the theme degradation path (A5).
|
||||
- Do NOT touch the existing app_name passes (1–4) or the `BOR_DOCS_REPO_CONFIGURED` flag logic — byte-identical behavior for them.
|
||||
2. `tests/unit/test_frontend_brand.py` — add pins (house style, read `brand.js` as text): markers for the three applications are present — `#message-input` placeholder application, the `.footer-text` selector, the `assets/themes/` link insertion with the `theme-override` id + `onerror` warn; the `app_name` literal default pin still holds.
|
||||
|
||||
## Testing & Quality
|
||||
- No Python logic in this task; `uv run pytest` green (unit pins + everything else unchanged); `uv run ruff check . && uv run pyright` clean.
|
||||
- Coverage: **>90%** on `app/` (validate.sh gate).
|
||||
- Browser-level proof lands in task 05's E2E.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `brand.js` applies the three keys from the SAME settled config (no second fetch); empty values skip every application (byte-identical no-op).
|
||||
- [ ] Theme link is inserted after `styles.css`, guarded by `#theme-override`, and degrades with a `console.warn` on 404 (A5).
|
||||
- [ ] Unit pins green; full suite green; lint + types clean.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Task 03 — Example theme (`indigo.css`) + authoring guide + Containerfile line
|
||||
|
||||
**Phase:** `62_ui_customization` · **Source:** `TODO.md:3` — "…custom color themes…"
|
||||
**Story:** n/a (TODO-derived)
|
||||
|
||||
## Objective
|
||||
A working example theme a deployer can point `BOR_THEME` at out of the box — plus the documentation that makes "write my own theme" a 15-minute job, and the one Containerfile line that ships the directory in the container image (A7).
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/themes/indigo.css` (NEW) — overrides ONLY the 8 identity variables in a single `:root` block; the semantic families (accent/ok/err) are deliberately UNTOUCHED (they encode states — deflection amber, success green, error red — and are already AA in the built-in theme; a theme that keeps them stays honest):
|
||||
```css
|
||||
/* Phase 62 example theme — dark indigo/slate.
|
||||
Overrides the :root identity palette from styles.css; every
|
||||
text/background pair meets WCAG 2.1 AA (>= 4.5:1):
|
||||
ink on bg 15.8:1 · ink on surface 14.6:1 · ink-soft on surface 8.2:1
|
||||
dark bg ink on brand 6.4:1 · brand-ink on surface 11.9:1.
|
||||
Semantic families (accent/ok/err) inherit the built-in theme. */
|
||||
:root {
|
||||
--bg: #0a0e1a;
|
||||
--surface: #111726;
|
||||
--ink: #e6e9f0;
|
||||
--ink-soft: #a8b0c8;
|
||||
--line: #232c44;
|
||||
--brand: #818cf8;
|
||||
--brand-soft: #1a1f38;
|
||||
--brand-ink: #c7d2fe;
|
||||
}
|
||||
```
|
||||
- Before committing, re-verify the five ratios above (e.g. a 10-line python contrast calc against the built-in pairs' house comment style) — if any pair misses 4.5:1, adjust the value, not the bar (AGENTS.md rule 5).
|
||||
- Spot-check that no component CSS hard-codes a color the theme should own: `grep -n "f43f5e\|#f0e6e6\|#0f0a0a" frontend/assets/styles.css` should match ONLY the `:root` palette block + comments (if a literal lives elsewhere, note it in the task report — do NOT refactor styles.css in this phase).
|
||||
2. `frontend/assets/themes/README.md` (NEW) — the authoring guide, house-comment style:
|
||||
- How themes load: `BOR_THEME=<file>` → `/api/config` → `brand.js` inserts `<link rel="stylesheet" href="/assets/themes/<file>">` AFTER `styles.css` (later wins the cascade — that is the whole mechanism).
|
||||
- The variable table: the 8 identity variables (bg/surface/ink/ink-soft/line/brand/brand-soft/brand-ink) with the built-in values as reference, and the note that accent/ok/err are semantic and should stay.
|
||||
- Rules: filename `^[a-z0-9_-]+\.css$` (lowercase, bare filename — the server validator rejects anything else at startup); one `:root` block; every text/background pair ≥4.5:1 (AGENTS.md rule 5); never white-on-brand (the built-in's documented 3.7:1 trap) — dark bg ink on brand, as the built-in does.
|
||||
- Deployment: works in dev immediately (served from the static dir); in the container, rebuild the image (the `cp -r` picks up whatever is in `frontend/assets/themes/` at build time — no Containerfile edit for new files, A7).
|
||||
3. `Containerfile` — stage-1 `RUN` chain: after the `styles.css` minify line, add
|
||||
```
|
||||
&& cp -r ./assets/themes /out/assets/themes \
|
||||
```
|
||||
(A7: whole directory, no per-file esbuild — future themes need no Containerfile change). No other stage changes; the runtime stage copies the whole `/out` static tree, so the dir flows through untouched.
|
||||
4. Dev-server sanity (record in task report): with `BOR_THEME=indigo.css`, `http://localhost:8000/assets/themes/indigo.css` returns 200 from the static dir; with the default env the built-in palette renders (the file existing but `BOR_THEME` empty changes NOTHING — loading is opt-in via the env var, not directory scanning).
|
||||
|
||||
## Testing & Quality
|
||||
- No Python logic; `uv run pytest` green; `uv run ruff check . && uv run pyright` clean.
|
||||
- Coverage: **>90%** on `app/` (validate.sh gate).
|
||||
- Add the theme-file unit pins to `tests/unit/test_frontend_brand.py` (or `test_stale_ui_copy.py`'s sibling if the executor prefers a new `tests/unit/test_themes.py`): the file exists, starts with a `:root` block, contains exactly the 8 `--` overrides (no other declarations), and the Containerfile contains the `cp -r ./assets/themes` line.
|
||||
- Browser proof (computed `--brand`, the link tag, visual sanity) lands in task 05's E2E.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `frontend/assets/themes/indigo.css` + `README.md` exist; all five contrast ratios verified ≥4.5:1 (calc output in the task report).
|
||||
- [ ] `Containerfile` ships `assets/themes/` (the line is in stage 1, after the styles.css minify).
|
||||
- [ ] The `grep` spot-check for hard-coded identity colors is recorded (expect: `:root` block + comments only).
|
||||
- [ ] Unit pins green; full suite green; lint + types clean.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Task 04 — Document the customization surface (.env.example + README)
|
||||
|
||||
**Phase:** `62_ui_customization` · **Source:** `TODO.md:3` — "Allow UI customization. This is brain of reese, but I want anyone to be able to deploy it with their name…"
|
||||
**Story:** n/a (TODO-derived)
|
||||
|
||||
## Objective
|
||||
A deployer who has never seen the codebase can find, understand, and apply all four customization vars (name, placeholder, footer, theme) from the docs alone — the house standard set by the phase-39 `BOR_APP_NAME` documentation.
|
||||
|
||||
## Work
|
||||
1. `.env.example` — in the `# --- App ---` block, right after the `BOR_APP_NAME` comment line (~L9), add (same comment style, phase-cited):
|
||||
```bash
|
||||
# BOR_INPUT_PLACEHOLDER=Ask me anything… # composer placeholder, chat page (phase 62)
|
||||
# BOR_FOOTER_TEXT=Powered by self-hosted models # footer line on every page (phase 62)
|
||||
# BOR_THEME= # filename under frontend/assets/themes/ (e.g. indigo.css) — overrides the built-in palette; empty = built-in (phase 62)
|
||||
```
|
||||
2. `README.md` — two edits:
|
||||
- The env-var table (the `BOR_APP_NAME` row sits ~L734): add three rows immediately after it, matching that row's column style — what it affects, the default, the no-op-when-unset contract:
|
||||
- `BOR_INPUT_PLACEHOLDER` | `Ask me anything…` | the chat composer placeholder; applied by `assets/brand.js` from `GET /api/config`; unset ⇒ the template default
|
||||
- `BOR_FOOTER_TEXT` | `Powered by self-hosted models` | the footer line on all 9 pages (the `.footer-text` spans); same mechanism
|
||||
- `BOR_THEME` | *(empty)* | a filename under `frontend/assets/themes/` (e.g. `indigo.css`) — a `:root` palette override injected after `styles.css`; the server refuses a malformed name at startup (bare `^[a-z0-9_-]+\.css$` filename); a missing file degrades to the built-in theme; unset ⇒ built-in dark-tech palette
|
||||
- A short **"Customizing the look"** subsection (3–6 sentences) placed directly above or below the env table (mirror the placement of the nearest feature section — the executor picks the spot that reads best and notes it in the task report): the app ships as "Brain of Reese" but every identity string is an env var (`BOR_APP_NAME` / `BOR_INPUT_PLACEHOLDER` / `BOR_FOOTER_TEXT`), themes are plain CSS variable overrides (`frontend/assets/themes/README.md` is the authoring guide, `indigo.css` the example), and everything unset renders the defaults byte-identically.
|
||||
3. No code changes in this task.
|
||||
|
||||
## Testing & Quality
|
||||
- Docs-only; `uv run pytest` green unchanged; `uv run ruff check . && uv run pyright` clean.
|
||||
- Coverage: **>90%** on `app/` (validate.sh gate).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `.env.example` carries the three new commented vars in the App block with the exact default values (so copy-paste of the example reproduces the defaults).
|
||||
- [ ] README env table has the three rows + the "Customizing the look" subsection; the README builds/reads cleanly (no broken table rows, links resolve — `frontend/assets/themes/README.md` exists from task 03).
|
||||
- [ ] Full suite green, lint + types clean.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Task 05 — Dedicated E2E + regression pass + atomic commit
|
||||
|
||||
**Phase:** `62_ui_customization` · **Source:** `TODO.md:3` — "Allow UI customization. This is brain of reese, but I want anyone to be able to deploy it with their name…"
|
||||
**Story:** n/a (TODO-derived)
|
||||
|
||||
## Objective
|
||||
One dedicated Playwright suite proves the whole contract end-to-end — an instance booted with all three customization vars shows the custom look, the default shared server is BYTE-IDENTICAL to today, and a malformed `BOR_THEME` refuses to boot — then one atomic commit and the phase is done.
|
||||
|
||||
## Work
|
||||
1. `tests/e2e/test_ui_customization.py` (NEW — one story, one file, run in isolation, DB up). Boots a second app instance exactly the way `test_configurable_brand.py`'s `testy_server` does (same env block, same DB, mock-LLM base URL, admin auth, static dir, mock-calibrated threshold) with THREE changes: port `APP_PORT + 2` (the brand suite owns `APP_PORT + 1` — do not collide), and the env overrides
|
||||
`BOR_INPUT_PLACEHOLDER="Ask the archive…"`, `BOR_FOOTER_TEXT="Custom footer line"`, `BOR_THEME="indigo.css"`.
|
||||
Test → contract mapping:
|
||||
1. `test_config_serves_the_overrides` — the instance's `GET /api/config` returns the three overridden values alongside the six-key set (the name stays the DEFAULT — this suite does not re-test `BOR_APP_NAME`; that is the phase-39 suite's job).
|
||||
2. `test_chat_page_shows_custom_placeholder_footer_theme` (instance) — on the chat page: `#message-input` placeholder === `Ask the archive…`; the first `.footer-text` textContent === `Custom footer line`; a `link#theme-override[rel="stylesheet"][href="/assets/themes/indigo.css"]` exists in the `<head>`; `getComputedStyle(document.documentElement).getPropertyValue("--brand").trim()` === `#818cf8` (the indigo value — custom properties return the specified token, so the string compare is stable; the built-in is `#f43f5e`).
|
||||
3. `test_footer_text_applies_on_other_pages` (instance) — on a second page (e.g. `/login.html`): the `.footer-text` textContent === `Custom footer line` (multi-page proof, no composer on that page — the placeholder application no-ops without error).
|
||||
4. `test_default_server_is_byte_identical` (shared conftest server) — placeholder === `Ask me anything…`; footer === `Powered by self-hosted models`; NO `#theme-override` link; computed `--brand` === `#f43f5e` (the phase-39/61 no-op contract).
|
||||
5. `test_malformed_theme_refuses_startup` — boot a short-lived uvicorn subprocess with `BOR_THEME="../evil.css"` (any port): the process exits non-zero within the timeout (or never starts listening) and its stderr names the value — the phase-56 fail-loud style, proven end-to-end, not just via the validator unit test.
|
||||
- Determinism: settled-state assertions throughout; Playwright `expect` retries ride out the brand.js fetch (the same pattern the brand suite documents in its module docstring).
|
||||
2. Regression E2E pass (each in isolation, DB up): `test_configurable_brand.py` (its key-set assertions were updated in task 01), `test_stale_ui_copy.py` (phase 61's copy contract is this phase's baseline), `test_smoke.py`, `test_dark_tech_theme.py` (the built-in palette is untouched when unset).
|
||||
3. One atomic commit (`.agent/` untracked — owner instruction, commit 281f355):
|
||||
```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"
|
||||
```
|
||||
Commit body: one line — the three env vars + the indigo example theme + the docs/theme-guide pointers.
|
||||
|
||||
## Testing & Quality
|
||||
- `uv run pytest` green (unit + integration); coverage TOTAL **>90%** (validate.sh gate).
|
||||
- `uv run pytest tests/e2e/test_ui_customization.py -v --no-cov` green in isolation.
|
||||
- `uv run ruff check . && uv run pyright` clean.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `tests/e2e/test_ui_customization.py` exists (5 mapped tests) and is green in isolation (DB up).
|
||||
- [ ] The four regression E2E suites are green in isolation.
|
||||
- [ ] Full suite green, coverage >90%, lint + types clean.
|
||||
- [ ] Exactly one new commit on `main`, staged paths `app/ frontend/ tests/ .env.example README.md Containerfile`, `--no-gpg-sign`.
|
||||
- [ ] Phase dir moved to `.agent/phases/complete/62_ui_customization/` (`.agent/` stays untracked).
|
||||
Reference in New Issue
Block a user