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
+134
View File
@@ -0,0 +1,134 @@
/* Brain of Reese — brand layer (phase 39).
*
* One env var (BOR_APP_NAME) drives the display name everywhere. This
* small CLASSIC script is the single owner of the resolution — it is not
* a module, so its top level runs at parse time: window.BOR_BRAND is
* readable from the first line of the page's module scripts (modules
* execute after parsing, so a module could not guarantee this).
*
* Contract (phase 39 locked decisions — A11 no CDN, runtime fetch):
* • window.BOR_BRAND = "Brain of Reese" synchronously — the default
* name renders immediately, no blank flash;
* • fetch("/api/config", { cache: "no-store" }) — on success with a
* non-empty app_name, window.BOR_BRAND is updated and the name is
* applied to the DOM:
* 1. document.title — global replace of the literal;
* 2. every .brand-text node — a name starting "Brain of " keeps
* the current look (Brain of <strong>rest</strong>), any other
* name renders plain (no bold); the name is HTML-escaped (an
* operator-controlled string must not inject markup);
* 3. a TreeWalker over the document's text nodes — the literal is
* replaced (the index empty-state h1 "Hey! I'm Brain of
* Reese." and any other prose); script/style text nodes are
* skipped so page source is never mutated;
* 4. an attribute pass — the aria-label / placeholder / meta
* content attributes containing the literal (the #messages
* aria-label, the input label, the meta descriptions).
* • fetch failure / empty name → the default stays + console.warn
* (the loadHealth house style: progressive enhancement, the page
* never breaks).
*
* No-op property: with BOR_APP_NAME unset the /api/config answer IS the
* literal, so every replacement below is a byte-identical no-op.
*/
/* The synchronous default — set BEFORE any fetch, so module scripts
reading window.BOR_BRAND at evaluation time always find a value. */
window.BOR_BRAND = "Brain of Reese";
/* The literal the DOM passes replace — the default name. The page
scripts' own `window.BOR_BRAND || "Brain of Reese"` fallbacks stay in
sync with it. */
const BRAND_LITERAL = "Brain of Reese";
/* The name is operator-controlled: HTML-escape it before it touches
innerHTML (the markdown.js escape pattern — local on purpose, no
cross-module import for a 5-line helper). */
function escapeHTML(s) {
return String(s).replace(/[&<>"']/g, (c) => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
}[c]));
}
function applyBrand() {
fetch("/api/config", { cache: "no-store" })
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
.then((cfg) => {
const name = typeof cfg?.app_name === "string" ? cfg.app_name.trim() : "";
if (!name) return; // empty / missing: the default stands
window.BOR_BRAND = name;
// 1. The document title (global replace of the literal — covers
// every page's static "<…> · Brain of Reese" titles).
document.title = document.title.replaceAll(BRAND_LITERAL, name);
// 2. The header brand on every page: a name starting "Brain of "
// keeps the bold split (the current look), anything else
// renders plain — the name is always escaped.
for (const el of document.querySelectorAll(".brand-text")) {
if (name.startsWith("Brain of ")) {
const rest = name.slice("Brain of ".length);
el.innerHTML = `Brain of <strong>${escapeHTML(rest)}</strong>`;
} else {
el.textContent = name;
}
}
// 3. Prose: a TreeWalker over the body's text nodes replaces the
// literal (the empty-state h1, any other copy). Text nodes
// inside <script>/<style> are rejected — the page source must
// never be rewritten.
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
const tag = node.parentElement ? node.parentElement.tagName : "";
return tag === "SCRIPT" || tag === "STYLE"
? NodeFilter.FILTER_REJECT
: NodeFilter.FILTER_ACCEPT;
},
},
);
const nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
for (const node of nodes) {
if (node.nodeValue && node.nodeValue.includes(BRAND_LITERAL)) {
node.nodeValue = node.nodeValue.replaceAll(BRAND_LITERAL, name);
}
}
// 4. Attributes: the #messages aria-label, the composer input
// label, the meta descriptions — aria-label / placeholder /
// meta content only, each replaced in place.
for (const el of document.querySelectorAll(
"[aria-label], [placeholder], meta[content]",
)) {
for (const attr of ["aria-label", "placeholder"]) {
const v = el.getAttribute(attr);
if (v && v.includes(BRAND_LITERAL)) {
el.setAttribute(attr, v.replaceAll(BRAND_LITERAL, name));
}
}
if (el.tagName === "META") {
const v = el.getAttribute("content");
if (v && v.includes(BRAND_LITERAL)) {
el.setAttribute("content", v.replaceAll(BRAND_LITERAL, name));
}
}
}
})
.catch((err) => {
// Fetch failure (or a non-JSON body): the default name stays —
// the page never breaks (the loadHealth house style).
console.warn("brand: /api/config did not answer — keeping the default name.", err);
});
}
/* The top level only sets the global (synchronously, at parse time);
the DOM passes run once the document is ready. */
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", applyBrand);
} else {
applyBrand();
}