/* 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;
* • Phase 59: window.BOR_DOCS_REPO_CONFIGURED = false synchronously
* (inert until proven) and window.BOR_CONFIG_PROMISE — the SAME
* fetch's promise, exposed at parse time so the chat page's boot
* (app.js) can await it BEFORE rendering any bubble; the "Save as
* doc" gating flag is then final, and a restored conversation of a
* configured admin never misses (or flashes) the button. The
* promise NEVER rejects — the error arm warns and resolves null;
* • fetch("/api/config", { cache: "no-store" }) — on success the
* docs flag is set from cfg.docs_repo_configured, and on 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 rest), 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);
* 5–6. Phase 62 (owner-locked 2026-09-01, TODO L3) — the SAME
* settled config also carries the two UI-customization
* string keys, applied in this same pass, AFTER the app_name
* passes and INDEPENDENT of them (they apply even when the
* name is the default/empty). Each empty value is a no-op —
* an unset deployment stays byte-identical:
* 5. input_placeholder — non-empty → the #message-input
* placeholder (the chat page only; every other page
* no-ops via the null guard);
* 6. footer_text — non-empty → every .footer-text node's
* textContent (all 9 pages, the phase-61 hook; an
* operator string can't inject markup via textContent).
* Phase 91 (task 03): color theming is no longer a brand.js
* job — the retired CSS-file theme link (the old step 7) is
* deleted with its env var; the SERVER now injects the
* effective palette inline before first paint
* (app/core/theming.py), so this layer keeps the text swaps
* only.
* • fetch failure / empty name → the default stays + console.warn
* (the loadHealth house style: progressive enhancement, the page
* never breaks).
*
* ONE pass, TWO callers (phase 190, task 03 — A2). Steps 1–6 live in
* the named function applyBrandFromConfig(cfg), the single owner of the
* brand DOM. It runs (a) at BOOT, through applyBrand() awaiting the
* parse-time fetch — byte-identical to the pre-phase single pass — and
* (b) on a LIVE re-apply, when the admin saves or resets a brand string
* in the Settings panel (assets/settings.js) or saves/resets the Theme
* tab (assets/theme.js): those surfaces fetch a fresh /api/config and
* hand it to the SAME function, so "saved" is visible without a reload
* (the PLAN §7.4 never-stale contract, extended from the phase-92
* palette to the strings). Because a live re-apply runs against a
* document whose previous pass already replaced the default literal,
* step 7 re-runs the three literal passes over the name the page
* CURRENTLY shows (window.BOR_BRAND_APPLIED) — at boot that is "" so
* step 7 never runs and the boot path stays exactly the phase-39
* sequence. The function is on this layer's existing export surface —
* the window globals (window.BOR_BRAND, window.BOR_CONFIG_PROMISE) —
* as window.applyBrandFromConfig; brand.js stays the ONE owner of the
* brand DOM and no surface copies a step.
*
* No-op property: with the customization env vars (BOR_APP_NAME,
* BOR_INPUT_PLACEHOLDER, BOR_FOOTER_TEXT) unset, /api/config answers
* with the template defaults themselves — the name IS the literal, the
* placeholder and footer are the phase-61 copy (re-setting them is
* invisible) — so an unset deployment renders byte-identical. The
* "empty writes nothing" rule for the two phase-62 keys holds on a live
* re-apply too (the page keeps what it shows): /api/config cannot
* distinguish "cleared" from "unset", and both env defaults are
* non-empty, so an empty value only ever comes from an operator saving
* an empty string (and then the page keeps what it shows). A panel
* RESET deletes the row, so the resolver hands back the legacy tab row
* or the env default (both non-empty) and that name re-applies live.
*/
/* 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";
/* Phase 59 (owner-locked 2026-08-31, TODO.md L3): the docs-push flag —
surfaced the way app_name is (a window global, inert until the boot
fetch proves otherwise). false = the "Save as doc" action is hidden
for everyone (BOR_DOCS_REPO empty — the feature is off). */
window.BOR_DOCS_REPO_CONFIGURED = false;
/* 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";
/* Phase 190 (task 03): the name the DOM passes last applied — "" until
the first apply (the boot pass). A live re-apply needs it: the page
no longer carries the default literal where the brand shows, so the
re-apply replaces THIS name too (step 7 below). */
window.BOR_BRAND_APPLIED = "";
/* 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) => ({
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
}[c]));
}
/* The /api/config fetch — started at TOP LEVEL (parse time) so
window.BOR_CONFIG_PROMISE exists before the page's module scripts
evaluate (app.js's boot awaits it, above). Phase 59: the flag lands
here, the moment the answer arrives — before any DOM pass. The
promise NEVER rejects: the error arm warns (the loadHealth house
style — the page never breaks) and resolves to null, so the default
name + false flag stand. */
const BOR_CONFIG_PROMISE = fetch("/api/config", { cache: "no-store" })
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
.then(
(cfg) => {
window.BOR_DOCS_REPO_CONFIGURED = cfg?.docs_repo_configured === true;
return cfg;
},
(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);
return null;
},
);
window.BOR_CONFIG_PROMISE = BOR_CONFIG_PROMISE;
/* THE shared brand pass (phase 190, task 03) — the ONE owner of the
brand DOM. Synchronous, idempotent, and safe to run against an
already-branded document: boot (applyBrand below, via the settled
fetch) and the live re-apply (settings.js after a brand-string
save/reset, theme.js after a save/reset) run THIS function and
nothing else. `cfg` is a /api/config payload; a null/missing payload
touches nothing (the loadHealth house style — never a blanked brand). */
function applyBrandFromConfig(cfg) {
const name = typeof cfg?.app_name === "string" ? cfg.app_name.trim() : "";
const previous =
typeof window.BOR_BRAND_APPLIED === "string" ? window.BOR_BRAND_APPLIED : "";
if (name) {
// A configured name: apply it (empty / missing: the default
// stands — and the phase-62 keys below are INDEPENDENT of the
// name, so they still apply).
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 ${escapeHTML(rest)}`;
} 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