Replace baked-in UOM/MysticMoon/UOMysticmoon branding with a BRAND_* env
scheme so one prebuilt image runs as any shard; UOMysticmoon becomes the
first tenant that sets these vars rather than a special case in the code.
Architecture (chosen because the app ships as a prebuilt image):
- server/src/config/brand.js + bot/src/brand.js read BRAND_* once at boot,
with Runic Gateway defaults.
- Text/colors reach the SPA at RUNTIME through the existing public settings
API (settings.model.getPublic -> SiteContext), so no client rebuild. The
admin-editable site title + contact email still override BRAND_NAME/email.
- SiteContext applies BRAND_ACCENT_COLOR to the --accent CSS var at runtime.
- Express templates the built index.html <title>/description/OG/favicon at
serve time from BRAND_* (renderIndexHtml in app.js).
- Server-side consumers read brand directly: emails, TOTP issuer, API docs,
boot logs, HTML error page. Bot uses it for embed color + logs.
Assets: logo/hero/favicon delivered from a ./brand:/app/brand bind-mount
(BRAND_LOGO/HERO/FAVICON), with neutral defaults baked in; hero falls back
to a built-in image when unset.
Scope: also genericized package.json names (uomysticmoon-* -> runic-gateway-*)
and the DB_NAME/DB_USER/COOKIE_NAME code defaults (runic_gateway/runic/
rg_token). Production keeps its real values by pinning them in .env — see
.env.uomysticmoon.example, which reproduces the exact UOMysticmoon identity
(proof the substitution works). Changing a deployed COOKIE_NAME invalidates
existing sessions, so UOMysticmoon pins uomm_token.
Verified: 193 server tests pass, client builds, app.js loads + templates the
built index.html, brand transform injects title/description/OG/favicon.
Bug 1 — Logged-out visitors saw the coming-soon Maintenance page while
admins saw the real hero. That difference is produced client-side by
MaintenanceGate (site_mode=maintenance && no user). Pull the `/` hero
route out from behind the gate so every visitor always lands on the real
Portal hero; the MaintenanceGate stays on all other public routes, so
content pages remain gated during maintenance and admins still preview
through it.
Bug 2 — The landing hero rendered the site nav because Portal used
PublicLayout with the default header=true. Pass header={false} so the
hero has no top nav (footer retained), using the layout's existing
escape hatch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
- SiteHeader: a single consistent main nav (Home, News, Screenshots, Five on
Friday, Newsletter, Wiki, Shard, About) with active-state highlighting, plus
an auth-aware entry on the right — Sign in when logged out, My Account
(player) or Admin (staff) when logged in.
- Portal (landing) now renders the site header too, so the nav is present
across the entire site, not just interior pages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qPmpmVH1xGCiZoz9m9vW3
Remove the two-card destination row and the below-hero quick-links nav
from the portal so the hero fills the viewport with nothing rendered
after it. The 5 quick links (News, Screenshots, Five on Friday,
Monthly Newsletter, About) move into the hero editor as a third
buttons element in defaultLayout(), reusing the existing buttons
element type so they stay fully editable with no schema changes.
Also drop overflow:hidden on the hero section: on mobile, 100vh can
compute smaller than window.innerHeight, and with overflow hidden the
wrapped quick-links text was getting clipped at the bottom edge.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Second phase of the hero canvas editor (see HERO_EDITOR.md).
- new lib/heroLayout.js: shared defaultLayout/buildOverlay/heroBackground/
parseLayout used by both the portal and the editor (Portal refactored onto it)
- new admin view HeroEditor.jsx at /admin/hero (+ sidebar nav + route):
- live canvas preview (16:9) rendering the draft via HeroElement
- background panel: image upload (/admin/uploads, >1MB warning), 3x3 position
grid, overlay opacity slider — all update the canvas in real time
- debounced (800ms) auto-save to hero_layout_draft
- Publish (writes hero_layout + draft), Preview (opens /?preview=1), Revert
- Portal: ?preview=1 renders the draft via the admin settings endpoint, with a
"showing unpublished draft" banner; normal load renders the published layout
No schema/dep changes. Verified end to end: overlay/position update the canvas,
auto-save writes the draft, publish updates the live portal, preview shows the
draft while the public page shows live. Element drag/properties land in Phase 3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First phase of the hero canvas editor (see HERO_EDITOR.md).
- settings.model: add hero_layout to PUBLIC_KEYS so the portal receives it
(corrects the design doc — public settings is a whitelist, not getAll();
hero_layout_draft stays admin-only)
- new HeroElement.jsx: renders one layout element by type (text_block,
buttons, moon, badge, image); absolute % positioning with anchor; shared
by the portal now and the editor canvas later
- MoonDot: optional color override for the hero moon element
- Portal.jsx: parse hero_layout (version-checked, try/catch), render elements
sorted by z; fall back to a DEFAULT_LAYOUT built from the current hero so the
page is byte-for-byte unchanged until staff publish their own
No schema change. Verified: default render matches the old hero; publishing a
hero_layout re-renders the portal; the draft key is not exposed publicly; client
builds; no console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>