feat(brand): configurable app name — BOR_APP_NAME drives /api/config + the frontend brand layer
Build and Push Containers / build-and-push (push) Successful in 1m50s

One env var (BOR_APP_NAME, default "Brain of Reese") now drives the app's
display name everywhere (TODO.md L12 — owner ask: "a way to customize the
name for 'Brain of'. Should be an env var."). The existing app_name setting
is the source of truth (phase locked decision — no new variable, no rename);
with the variable unset the app is byte-identical to before.

Endpoint (A10 public/stateless, no secrets):
  GET /api/config → exactly {app_name, version} (app/api/config.py, the
  health.py pattern; registered before the static mount). Integration tests:
  anonymous 200, default values, a Settings override follows, key set is
  exactly two keys — no other setting may leak in later.

Frontend brand layer (A11 — runtime fetch, static templates stay static):
  assets/brand.js — a CLASSIC script, first on all six pages, so its top
  level runs at parse time: window.BOR_BRAND = "Brain of Reese"
  synchronously (the default renders immediately, no blank flash), then a
  no-store fetch of /api/config applies the name — document.title (global
  replace), every .brand-text (a name starting "Brain of " keeps the bold
  split Brain of <strong>rest</strong>, any other name renders plain; the
  operator-controlled name is HTML-escaped before innerHTML), a TreeWalker
  over text nodes (script/style rejected — page source never rewritten),
  and the aria-label/placeholder/meta-content attributes. Fetch failure
  keeps the default + console.warn (the loadHealth house style).
  app.js (status labels, typing label, elapsed-hint aria, tool labels) and
  document.js (viewer titles) read window.BOR_BRAND at CALL time via
  brand() — a label set after the fetch lands carries the configured name.
  Containerfile: esbuild minify line for brand.js (classic, like markdown.js);
  the phase-33 ?v= cache-busting picks the new asset ref up automatically.

E2E (A16 — one story, one file, isolated): test_configurable_brand.py boots
a SECOND app instance (same DB/mock-LLM/admin-auth env block, port APP_PORT+1,
BOR_APP_NAME="Brain of Testy") — the shared conftest server keeps the
default name so every other suite's title/label assertions stay untouched —
and asserts /api/config on both instances, the index title/brand/greeting/
#messages aria-label, the sources + login page titles, and one pre-token
chat turn (think out loud marker) whose #send-status reads "Brain of Testy
is thinking"; the no-op regression pins the shared server's default bytes.

Docs: .env.example App section + README configuration reference — what it
affects (titles, header brand, status labels, aria text), the default, the
bold-split rendering rule.

Gates: 695 unit+integration passed, app/ coverage 99% (>90%), story E2E
green in isolation (two consecutive runs), brand-string suites (smoke,
shared header, header consistency, chat persistence) green, ruff + pyright
clean.
This commit is contained in:
2026-08-27 02:24:16 -04:00
parent 94d7228510
commit fe55be0c35
23 changed files with 788 additions and 20 deletions
+27 -12
View File
@@ -47,8 +47,9 @@
* "thinking": the UI state itself stays "thinking" (button stays
* disabled — never stale, PLAN §7.4) while the LABELS change — the
* button says "Calling tool…", the #send-status + typing-indicator
* labels say what Brain is doing ("Brain of Reese is listing documents"
* / "Brain of Reese is reading source/path"), and a visible `.tool-call`
* labels say what Brain is doing ("…is listing documents" /
* "…is reading source/path" — the name prefix resolves from
* window.BOR_BRAND at call time, phase 39), and a visible `.tool-call`
* line (own icon + accent color, distinct from the brand-ink Thinking
* block) is appended above the answer, one per call, in order.
* Append-only like thinking: frames are tolerated in any interleaving
@@ -111,6 +112,14 @@ const banner = document.querySelector("#kb-banner");
const bannerText = document.querySelector("#kb-banner-text");
const versionEl = document.querySelector("#app-version");
/* Phase 39: the display name resolves from one place — window.BOR_BRAND
* (the classic assets/brand.js sets it at parse time; its /api/config
* fetch refreshes it). Read LAZILY (a function, not a const string):
* a label set after the fetch lands carries the configured name; the
* literal below is only the no-config fallback — with the default name
* every label renders the pre-phase-39 bytes. */
const brand = () => window.BOR_BRAND || "Brain of Reese";
/* ---------- loading-feedback contract (PLAN §7.4) ----------
* Client-side guard: a pre-token stream that produces no delta within
* TURN_TIMEOUT_MS is treated as hung → error state + banner. It is
@@ -126,14 +135,20 @@ const UI_STATE = Object.freeze({
error: "error",
});
/* The #send-status live-region text per UI state (PLAN §7.4). The
* values are builders (phase 39): the brand entries resolve brand() at
* call time, never at module evaluation, so a label set after the
* /api/config fetch lands carries the configured name. */
const SEND_STATUS = Object.freeze({
[UI_STATE.idle]: "",
[UI_STATE.thinking]: "Brain of Reese is thinking",
[UI_STATE.streaming]: "Brain of Reese is answering",
[UI_STATE.error]: "The last question failed — try again",
[UI_STATE.idle]: () => "",
[UI_STATE.thinking]: () => `${brand()} is thinking`,
[UI_STATE.streaming]: () => `${brand()} is answering`,
[UI_STATE.error]: () => "The last question failed — try again",
});
const TYPING_LABEL = "Brain of Reese is thinking";
/* The typing indicator's accessible label (the 10s elapsed-seconds
* hint updates it from this base) — built at call time (phase 39). */
const TYPING_LABEL = () => `${brand()} is thinking`;
const ERROR_HINT = "Try again — if this persists, check the LLM is reachable.";
/* A turn with no answer content (an empty stream, or reasoning that
exhausted max_tokens — phase 17) still renders a bubble, and this exact
@@ -350,7 +365,7 @@ function addTyping() {
wrap.innerHTML = `
<span class="avatar" aria-hidden="true">${BRAIN_AVATAR}</span>
<div class="msg-body">
<div class="bubble typing" role="status" aria-label="${TYPING_LABEL}">
<div class="bubble typing" role="status" aria-label="${TYPING_LABEL()}">
<span></span><span></span><span></span>
</div>
</div>`;
@@ -528,7 +543,7 @@ function startThinkingClock() {
if (secs < 10) return; // hint only after 10s of pre-token silence
const bubble = document.querySelector("#typing-indicator .bubble");
if (bubble) {
bubble.setAttribute("aria-label", `Brain of Reese is still thinking (${secs}s)`);
bubble.setAttribute("aria-label", `${brand()} is still thinking (${secs}s)`);
}
}, 1000);
}
@@ -556,7 +571,7 @@ export function setUiState(state, errorDetail = "") {
sendBtn.disabled = inFlight;
sendBtn.querySelector(".spinner").hidden = !inFlight;
sendLabel.textContent = inFlight ? "Thinking…" : "Send";
sendStatus.textContent = SEND_STATUS[state] ?? "";
sendStatus.textContent = SEND_STATUS[state]?.() ?? "";
if (state === UI_STATE.thinking) {
addTyping();
@@ -963,8 +978,8 @@ async function handleSend(e) {
if (!wrap) wrap = addMessage("brain", "");
const toolStatus =
name === "read_document" && argument
? `Brain of Reese is reading ${argument}`
: "Brain of Reese is listing documents";
? `${brand()} is reading ${argument}`
: `${brand()} is listing documents`;
if (uiState === UI_STATE.thinking) {
sendLabel.textContent = "Calling tool…";
sendStatus.textContent = toolStatus;