Files
brain-of-reese/frontend/assets/brand.js
T
ducoterra d22d260b8b
Build and Push Containers / build-and-push-app (push) Successful in 5m43s
Build and Push Containers / build-and-push-db (push) Successful in 12s
phase: 91_admin_theme_tab
All verification is complete — this pass needed no code changes. Final report:

**Phase 91 — Admin Theme tab: final verification pass (all 6 tasks already in `complete/`)**

- Verified pre-paint theming end-to-end: `ui_settings` store + resolver, admin `GET/PUT /api/ui-settings`, `CachingMiddleware` inline-`<style id="bor-theme">` injection before `</head>` (incl. `/shared/<token>` prefix branch, unit-pinned), CSP sha256 exemption for the inline tag, Theme tab shell + `theme.js` editor, CSS-file theming fully retired.
- No defects found; zero changes made — working tree left exactly as the task executors left it.
- Tests: `uv run pytest --cov=app` → 1841 passed, 0 failed (TOTAL coverage **99%**; theming/ui_settings/caching all 100%); `uv run pytest tests/e2e/test_admin_theme_tab.py -v --no-cov` → **5 passed** in isolation.
- Lint/types: `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings.
- Criteria: (1) unset deployment byte-identical, no `#bor-theme` anywhere — ✓ (unit no-op test + E2E reset byte-compare); `rg "BOR_THEME|themes/"` → single hit is the permitted doc-history comment in `frontend/index.html`. (2) admin-only gate + 403s for anonymous and token users — ✓ (E2E test 3). (3) saved theme inline before `</head>` on every page incl. `/shared/<token>`, computed `--brand` on first paint for admin + anonymous — ✓ (E2E test 2 + unit). (4) reset → byte-identical; 5 contrast pairs warn <4.5:1, non-blocking — ✓ (E2E tests 4–5). (5) suite green, >90% coverage, lint clean — ✓. (6) commit deferred to harness per rules.
- Notable: `.agents/PLAN.md` is absent from the repo — the phase overview's Design section was used as the binding spec; no deviation resulted.
- Next pending phase: **none** — 91 is the last phase in `todo/`.
2026-09-09 17:22:24 -04:00

222 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 <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);
* 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 .then, 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).
*
* 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 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";
/* 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]));
}
/* 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;
function applyBrand() {
BOR_CONFIG_PROMISE.then((cfg) => {
const name = typeof cfg?.app_name === "string" ? cfg.app_name.trim() : "";
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 <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));
}
}
}
}
// Phase 62 (owner-locked 2026-09-01, TODO L3): the SAME settled
// config also carries the two customization STRING keys — applied
// here, INDEPENDENT of the app_name block above (they apply even
// when the name is the default/empty). Each empty value is a
// no-op, so an unset deployment stays byte-identical (no attribute
// or element touched, no second network call). Color theming is
// server-side since phase 91 (task 03) — not this layer's job.
const placeholder =
typeof cfg?.input_placeholder === "string" ? cfg.input_placeholder : "";
if (placeholder) {
// 5. The composer placeholder — the chat page only:
// #message-input exists only on index.html, so every other
// page no-ops via the null guard.
document.querySelector("#message-input")?.setAttribute(
"placeholder",
placeholder,
);
}
const footerText =
typeof cfg?.footer_text === "string" ? cfg.footer_text : "";
if (footerText) {
// 6. The footer line on every page (all 9 carry the phase-61
// .footer-text hook). textContent on purpose: an operator
// string can't inject markup.
document.querySelectorAll(".footer-text").forEach((el) => {
el.textContent = footerText;
});
}
});
}
/* The DOM passes run once the document is ready AND the config is
settled (applyBrand awaits the parse-time promise) — the fetch may
resolve before or after DOMContentLoaded; both orderings apply the
brand exactly once. */
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", applyBrand);
} else {
applyBrand();
}