docs(website): theming phases 3-4 as built
Records where the build diverged from the design and why. - The presets do not live in theme.css as [data-theme] blocks. Section 6.2 is marked superseded and a "Phases 3-4 as landed" section explains the inline --accent precedence problem that forced server-side resolution. - Section 4.5's accentInt fix is struck through: getPublic() never exposed accentInt, and Discord embeds are colored by a separate process reading env, so there was nothing per-request to recompute. Replaced with what was actually done -- the bot fetching the effective accent. - Phase 9 re-scoped. Applying Fantasy on a live instance showed the section 4.8 rgba literals carry a hue, not just a light/dark assumption, so the dark presets need that promotion too. - BACKEND_DESIGN.md: the new /settings/theme/options route, the `theme` block on /public/settings, effective values in the brand block, theme_visual validation on PUT /admin/settings, route count 225 -> 226. - api-route-inventory.json regenerated from the manifest. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -129,7 +129,10 @@ server/
|
||||
nav.router.js (1) /settings/nav — the nav_admin and
|
||||
nav_player overrides, read by the
|
||||
layouts that render them
|
||||
nav.controller.js
|
||||
theme.router.js (1) /settings/theme/options — the closed
|
||||
sets the admin appearance form is
|
||||
built from. Static; no DB read
|
||||
nav.controller.js + theme.controller.js
|
||||
admin/ index.js mounts the capability routers below at their
|
||||
own prefixes; owns the shared
|
||||
`noindex, isLoggedIn, staffOnly` gate and
|
||||
@@ -279,6 +282,17 @@ consumer parses it. Server side that is `utils/settingsJson.js`
|
||||
malformed or wrong-shaped value as **absent** rather than as an error, so a
|
||||
hand-edited row degrades to the default instead of rendering something broken.
|
||||
|
||||
**`theme_visual` is resolved server-side, not shipped raw to the browser.**
|
||||
`utils/themeResolve.js` layers `:root` ← preset ← custom, field by field, into
|
||||
the CSS custom properties `getPublic()` returns as `theme`; the SPA's only job
|
||||
is to write them onto `<html>` and take back what it wrote last time
|
||||
(`client/src/lib/themeVars.js`). One authority for the merge means the effective
|
||||
accent in `brand.accent` — the cross-repo contract the Android app and the
|
||||
Discord bot theme themselves from — always agrees with what the website paints.
|
||||
Values reaching a CSS variable are checked against closed sets on both paths:
|
||||
strictly on write (400, naming the field) and forgivingly on read (drop the bad
|
||||
field, keep its neighbours).
|
||||
|
||||
### activity_log — append-only
|
||||
| col | type | notes |
|
||||
|---|---|---|
|
||||
@@ -633,7 +647,7 @@ are authoritative, and they answer different questions:
|
||||
|
||||
| Artifact | Source of truth for | Generated by |
|
||||
|---|---|---|
|
||||
| `server/routes.manifest.json` — mirrored as [api-route-inventory.json](./api-route-inventory.json) | **What URLs exist.** 225 public routes + 2 on the internal listener, sorted, method + path only. | `npm run routes:manifest`, by walking the live Express stack |
|
||||
| `server/routes.manifest.json` — mirrored as [api-route-inventory.json](./api-route-inventory.json) | **What URLs exist.** 226 public routes + 2 on the internal listener, sorted, method + path only. | `npm run routes:manifest`, by walking the live Express stack |
|
||||
| `server/swagger/swagger-output.json` — served at `/api/docs` | **What each route means.** Parameters, bodies, response codes, security. | `npm run swagger`, from `#swagger.*` annotations |
|
||||
|
||||
The split is deliberate: Swagger is annotation-derived, so an unannotated route is invisible in it and
|
||||
@@ -809,7 +823,7 @@ from the per-route **siteMode** middleware (§5), never from an auth gate.
|
||||
|
||||
| Method | Path | Notes |
|
||||
|---|---|---|
|
||||
| GET | `/settings` | whitelisted public keys, derived `registration`/`gameAccountSignup` flags, the per-shard **`brand`** block (name, `accent` color, logo/hero/favicon) a client themes itself from — one image runs as any shard, asset fields may be site-relative paths (resolve against the base URL) — and a **`push`** block `{ ntfyUrl }` (M7): the client-facing ntfy relay URL the app's embedded distributor registers its device topic against, from `NTFY_PUBLIC_URL` / first `NTFY_ALLOWED_ORIGINS` (never the internal `NTFY_BASE_URL`); `null` when push isn't configured for the shard. |
|
||||
| GET | `/settings` | whitelisted public keys, derived `registration`/`gameAccountSignup` flags, the per-shard **`brand`** block (name, `accent` color, logo/hero/favicon) a client themes itself from — one image runs as any shard, asset fields may be site-relative paths (resolve against the base URL); these are **effective** values, so an admin theme (`theme_visual`) beats `BRAND_ACCENT_COLOR` and an uploaded `brand_assets` asset beats its `BRAND_*` path — an optional **`theme`** block, the resolved CSS custom properties for that admin theme (absent when the instance was never themed, which is what makes it render from the shipped stylesheet unchanged) — and a **`push`** block `{ ntfyUrl }` (M7): the client-facing ntfy relay URL the app's embedded distributor registers its device topic against, from `NTFY_PUBLIC_URL` / first `NTFY_ALLOWED_ORIGINS` (never the internal `NTFY_BASE_URL`); `null` when push isn't configured for the shard. |
|
||||
| GET | `/status` | status message + current mode, **plus a `version` block** (`{ service:'runic-gateway', api, server }`) so a client first-run probe recognizes the backend and can run a version-mismatch guard |
|
||||
| GET | `/version` | lightweight, **DB-free** backend identity/version (`{ service, api, server }`) — the canonical target for the version guard and a cheap liveness check |
|
||||
| GET | `/posts/:category` | published only; `category` ∈ news\|five-on-friday\|newsletter\|screenshots |
|
||||
@@ -843,6 +857,7 @@ These rows are configuration that happens to need a login.
|
||||
| Method | Path | Purpose |
|
||||
|---|---|---|
|
||||
| GET | `/settings/nav` | `{ nav_admin, nav_player }` — the stored nav overrides as raw JSON strings (or `null`), for the two authenticated layouts that render them. Deliberately not public: an anonymous visitor has no use for either, and the admin nav's labels describe the shape of the admin surface. Open to **any** role because `AdminLayout` renders for editors and moderators and `PlayerPortalLayout` for players, none of whom can read `GET /admin/settings`. Presentation-only — the role/feature filters in those layouts still decide what is shown, and an override can never un-hide a gated item (see [THEMING_AND_NAV.md](THEMING_AND_NAV.md) §7) |
|
||||
| GET | `/settings/theme/options` | The closed sets an admin may pick from when theming the site: the presets (each with its **full token map**, so a form can show what an unset field currently resolves to), the curated Google Fonts shortlist per role, the shadow depths, the editable color/radius field names paired with the CSS variable each drives, and `shippedTokens` (what `theme.css`'s `:root` declares). Static — derived from `config/themePresets.js`, no DB read. Served rather than duplicated in client code so the options the form **offers** can never drift from the ones `PUT /admin/settings` **accepts** |
|
||||
|
||||
### /admin (admin/index.js → the capability routers in §2) — all behind `isLoggedIn` + `noindex` + `staffOnly`
|
||||
|
||||
@@ -878,7 +893,7 @@ file a route sits in — that is the property the route manifest freezes.
|
||||
| POST | `/posts/upload` | multipart image upload (multer) → `{image_url}` for screenshots |
|
||||
| GET | `/wiki` · GET `/wiki/:slug` | read incl. unpublished |
|
||||
| POST | `/wiki` · PUT `/wiki/:slug` · DELETE `/wiki/:slug` | manage pages |
|
||||
| GET | `/settings` · PUT `/settings` | read all / update `{key:value,...}` |
|
||||
| GET | `/settings` · PUT `/settings` | read all / update `{key:value,...}`. Enum-constrained keys are validated on the way in; `theme_visual` additionally has every value checked against the closed sets in `config/themePresets.js` (hex color, shortlisted font stack, bounded px radius, listed shadow) and is stored stringified. The read path drops bad fields anyway, so the `400` is about **feedback** — a save that appears to succeed and then does nothing is worse than a rejection |
|
||||
| DELETE | `/settings/:key` | reset one setting to its default by deleting the row. Allowlisted to the keys whose default lives outside the store (`theme_visual`, `brand_assets`, `nav_public`, `nav_admin`, `nav_player`, `hero_layout_draft`) — anything else is `400`. Idempotent: resetting a key that was never set succeeds |
|
||||
| GET | `/activity?limit=&offset=` | paginated activity log |
|
||||
| GET | `/users` · POST `/users` · PUT `/users/:id` · DELETE `/users/:id` | user mgmt (can't delete self / last admin; password hashed on write) |
|
||||
|
||||
Reference in New Issue
Block a user