Phase 2 PR 4 of MODULE_SYSTEM.md §2.7. Records what §1.8's three entangled files
and §1.9's extension slot actually became, and four decisions taken with them.
MODULE_API.md §2.4:
- registerNotificationStreams takes the catalog ALONE. `mapEvent` was a leftover
from before §1.8's push inversion was settled — a module owns fromShardEvent
and calls publish() with an id it resolved, so core never needs a second route
to the same place. It follows that the public-safety filter is module-internal,
which is the right home: the kinds, the streams and the filter become one file
that moves together.
- the entry shape is two booleans, not a single `scope` — that object is the body
of GET /auth/me/notifications/streams and a shipped Android client reads both.
- registerAnnounceLeg gains `label`, so a module's leg renders in the admin panel
with no client change; and legs became `announce_job_legs` ROWS, because a
module cannot ALTER a core table and a registered leg had nowhere to live.
- every call STAGES; nothing commits until the module as a whole is known good.
New §6.5 — the eight grandfathered names (seven stream ids, one leg id), allowed
to `uo` alone by explicit allowlist, the same shape as the loader's legacy table
prefixes. Grandfathering rather than dropping the rule is what keeps it real for
every module written after this one.
New §6.6 — an extension slot is invisible to static analysis, so core needs §6.1's
fragment merge for its OWN slot fills, a phase earlier than the plan expected.
Moving the six users-detail routes behind the slot deleted 407 lines from
swagger-output.json while printing `Success`. Which slots and where each hangs are
both derived — from the registry and from the live express stack — because a
written-down copy drifts.
MODULE_SYSTEM.md §1.8 / §1.9 record the outcome per file, and §2.7 the progress.
BACKEND_DESIGN.md and website-README.md follow the moved files; the PUBLIC_KINDS
reference at §"visibility" named the wrong file and is corrected to
utils/shardBroadcast.js.
Co-Authored-By: Claude <noreply@anthropic.com>
Docs half of website PR 3 (phase 2). MODULE_SYSTEM.md 2.7 gains the PR 3 entry;
MODULE_API.md 2.6 gains the decisions the section did not previously answer.
The one that shapes the code: a fragment is VALIDATED at load time and EXECUTED
later, split on whether a database is needed to know the answer. Everything 2.6
states about the SQL is knowable by reading the file, so breaking one of those
rules costs a module its mount entirely rather than mounting and 503ing with its
tables half created. What is left for the replay is the class of failure only
the server can report, and that is post-mount and answers 503.
Also recorded: the rules are enforced as a leading-verb allowlist (CREATE,
ALTER, INSERT, UPDATE -- the four core's own schema.sql uses) rather than the
DROP denylist 2.6 words them as, because the file is replayed on every boot and
a denylist only ever bans what somebody thought of; the replay sits outside
ensureSchema's wait-for-the-database retry loop, so one module's bad SQL cannot
cost the boot two minutes; partial application is accepted, since MariaDB
self-commits DDL and no transaction could undo it; and `npm run seed` is the one
sanctioned caller that replays nothing, because it never requires app.js and so
has no scan to read.
Co-Authored-By: Claude <noreply@anthropic.com>
Records what phase 2 PR 2 decided against the two things MODULE_API.md
left open for it.
7.6 is settled as an explicit modules.load(tierRouters) call in app.js
rather than a require-time scan: the loader needs the tier routers handed
to it for the 4.3 check, which a require-time side effect cannot receive,
and a require's position enforces an ordering constraint invisibly.
4.1 and 4.3 gain the mechanics that fall out -- where the call must sit
in app.js and why in both directions, that mounting is a second pass
after validation, and that core's prefix ownership is probed on the live
tier routers with express's layer.match() rather than declared in a table
that was already stale in the spike.
2.7 records PR 2 as done.
Co-Authored-By: Claude <noreply@anthropic.com>
RunicGateway/Integration-kit -- empty today, exactly where Module-uo was at the
start of Phase 0 -- becomes the book someone reads to build a module for a game
that is not UO, starting from nothing. It is the only document in the project
written for an audience outside the org.
One book rather than a page in each repo, because the job spans all three layers
of the data path: the website module (the bulk of it), the sidecar and WHY it
exists -- the shard is never network-reachable, it dials out, the wire is a
versioned contract -- and the game-side plugin that feeds it without ever letting
a wedged sidecar stall the game.
The rule that keeps it from rotting: it never re-specifies a contract. MODULE_API.md
stays normative for the module surface and link/PLAN.md + INTEGRATION.md for the
wire protocol; the kit teaches and links out for the authority. A guide that
restates a contract diverges from it silently, and the reader who follows the
divergent copy gets a module that fails validation for reasons the guide cannot
explain.
It trails the implementation rather than leading it -- scaffolded when Phase 2
lands, written against Phase 3's extraction with Module-uo as the worked example,
finished alongside Phase 4 -- and it is a draft until someone builds a working
module for a second game from it alone, without reading core's source.
Adds 2.11, Phase 5 in 2.7, and settled decision 13.
Co-Authored-By: Claude <noreply@anthropic.com>
Phase 2 PR 1 of the module system records two things 2.4 left open: the
states are stored in one `state` column rather than a policy flag beside a
runtime one, and every boot recomputes the outcome states while leaving
`disabled` alone.
That second rule is the one with consequences worth writing down -- a
startup_failed module is retried on every restart, so an operator who fixes
the cause needs no admin-panel visit; a running module can never display a
stale failure reason; and disabling, the one operator decision rather than
outcome, survives restarts. Also states what the row does NOT decide: the
loader scans the filesystem before the database is reachable, so the URL
surface is a property of the volume, which is what keeps
routes.manifest.json generatable against a dead database.
BACKEND_DESIGN.md 3 gains the installed_modules columns alongside the other
tables.
Co-Authored-By: Claude <noreply@anthropic.com>
Part 7 of MODULE_API.md: the three exit criteria and their results, the two
contract changes the spike forced (ctx.express/ctx.validator and
window.__rg.jsxRuntime), the empirical confirmation of the OpenAPI split, the
loader's tested failure guarantees, and the three artifacts in the branch that
are consequences of stopping at six routes rather than intended shape.
Phase 1 is complete: docs/website/MODULE_API.md is written and the spike met
every exit criterion on website branch spike/module-atlas (cut from edge, never
merged).
Co-Authored-By: Claude <noreply@anthropic.com>
Both approved by the org lead 2026-08-10. §6.1 moves from open question to
decision A with the per-side obligations spelled out in a new §6.1a: modules
ship a swagger-fragment.json with fully-qualified paths and namespaced schema
keys, core merges started modules' fragments into /api/docs.json at request
time and always wins a key collision, and swagger-output.json stays exactly
what core's own routes generate.
Co-Authored-By: Claude <noreply@anthropic.com>
The normative contract between core and an installed module: the `ctx` handed
to a module's entry point, the `register*` calls, the client-side registry and
shared-dependency global, the schema-fragment rules, and the loader's
validation and failure obligations. Every member is derived from what the UO
code actually imports today, re-read against the working tree.
Part 6 records four places the survey contradicted MODULE_SYSTEM.md:
• OpenAPI generation is STATIC analysis (swagger-autogen parses app.js as
text), unlike routeManifest.js which walks the live Express stack. A
filesystem-scanning loader is invisible to it, so module routes would be
silently absent from swagger-output.json. Three options, one recommended;
needs a decision before Phase 2.
• The client contract is much larger than §2.1 implies — the atlas pages
import five core modules beyond React, so the plan needs a curated UI kit
and a request primitive on window.__rg.
• shardVisibility is module-owned and the atlas depends on it, so the spike
carries it; two copies coexist during the spike by design.
• Two counts corrected: 27 UO tables (not 25), 6 atlas routes (not 5).
MODULE_SYSTEM.md gains a pointer to the contract and the corrected table count.
Co-Authored-By: Claude <noreply@anthropic.com>
Website work lands on an `edge` branch and reaches `main` as a single cutover
at the end -- the same shape used for protocol v3 and the Android theming
workstream. A core that has grown a module loader but not yet lost its UO
code is a coherent state; a core mid-extraction is not.
Adds a Phase 0, because the strategy is blocked on one line of CI config:
pr-checks.yml is `on: pull_request: branches: [main]`, so every PR into edge
would run no checks at all -- the same trap that let all nine Android M12
phase PRs merge with zero CI. It matters more here, since Phase 2's exit
criterion IS a CI result (zero-line routes.manifest.json diff, passing
tests). The fix must land on website main before the first module PR.
Also records the real module repo: RunicGateway/Module-uo (capital M, as
Android-app), currently empty -- no branches, no initial commit -- and
disambiguates the repo name from the module id `uo`.
Co-Authored-By: Claude <noreply@anthropic.com>
Turns Runic Gateway from a UO-specific platform into a game-agnostic one:
game-specific routes, tables, screens and nav leave the core website and
become an installable module. Operators install the base site, install the
module for their game, and restart -- they never build anything.
Verified against the working trees rather than written from the draft. The
draft's load-bearing assumptions that did not survive:
* one flat module route prefix cannot coexist with URL preservation, since
UO routes span three access tiers -- modules own a named slot per tier
* there is no server-side nav list, and navOverrides.js refuses to have one,
so nav registration is client-side
* the nav feature-gate mechanism is itself the shard visibility system
* there is no migration system to model a module runner on -- schema.sql is
replayed idempotently every boot, so modules ship fragments
* boot/shutdown holds eight UO call sites with no hook to receive them
* notificationStreams, pushDispatch and announceWorker are entangled, not moves
* six UO routes are nested under the core users resource
* the Discord bot has no UO logic at all
* the installer never contacts the website and its Bundle is hardcoded to
two components, so delivery is website-side
* routeManifest and swagger walk app.js with no DB, so modules mount
synchronously from the filesystem
* production is a prebuilt pull-only image and CSP forbids inline script,
which together decide how the client half loads
Ten decisions are recorded as settled in Part 3.
Co-Authored-By: Claude <noreply@anthropic.com>
Phase 8 was scoped as docs, coverage and the cutover. The AC-5 walk turned it
into a phase with code in it, which is the outcome the walk existed to produce -
so the record is longer than a phase-8 note would normally be.
THEMING_AND_NAV.md gains "Phase 8 as landed": what the walk confirmed, the two
defects it found and why they are phase 2's trap repeating, the role sweep that
says which Material roles are still unmapped and which of them have a reader, the
coverage-glob narrowing, and the adb-reverse rig note for the next walk. The
phase 2 note is amended where it claimed the card shadow was the milestone's only
visible change to an untouched instance - it is now one of three, and the third
(an untouched instance drawing a hero, because brand.hero carries the env
default) is a correction to the record with no code behind it.
PLAN.md's M12 entry stopped at phase 2; it now runs through 6, records phase 8
including the defects, and drops the same "one deliberate change" claim.
COVERAGE_PLAN.md gains an amendment section explaining why the ui/theme/**
directory glob became wrong the moment M12 put three pure resolvers behind it,
and the general rule it suggests: prefer file globs to directory globs once a
directory is mixed. Its own §5 already forbade excluding testable code.
Pairs with Android-app phase 8.
Co-Authored-By: Claude <noreply@anthropic.com>
Corrects §6.3's link-resolution table, which named three website paths the site
does not serve (/site/news/<idOrSlug>, /page/<slug>, /contact) and missed two it
does (/site/atlas/:slug, /site/market/vendors/:serial), and records the four
drawer decisions taken before code: the /<slug> CMS catch-all with the site's
reserved segments excluded, the static section header, the hand-off icon, and
reusing LocalAssetResolver for the Custom Tab's absolute URL.
Adds "Phase 6 as landed" and ticks the phase in §8.
Co-Authored-By: Claude <noreply@anthropic.com>
§6.2 corrected and amended, §6.1 given the Home decision, and a "Phase 5 as
landed" section added with the ✅ in §8.
**§6.2's table was in the wrong order, and the order is now load-bearing.** The
three news categories were listed Five on Friday / Newsletter / Screenshots; the
website's array has them Screenshots / Five on Friday / Newsletter. That did not
matter while the table was only a mapping, but a stored `order` is an index into
the site's nav, so a row the admin never moved takes its sort key from this list —
and a key from the wrong list scrambles a partially-overridden nav. The rows are
now numbered 0-15 and the order is called out as verbatim.
Also written into §6.2: the `feature` values are deliberately **not** mirrored
into the table (`APP_MENU` stays the app's own source of truth for gating), and
the "no `APP_MENU` row" rule covers **seven** entries rather than four — the
three news categories as well as the hub boards.
Two decisions the org lead settled before code, both recorded where the rule
lives rather than only in the phase notes:
- **The news categories are ignored for the drawer**, on the same rule as the hub
four. The mapping still exists for phase 6's added links, where a category tab
is a perfectly good destination because the admin named it by path. The
alternative — surfacing such a row only when overridden — keeps AC-1 but lets an
override introduce navigation after all, so it was rejected.
- **An admin may hide the Home row**, mirroring the website, where `/` is hideable
from the public header. Home stays the start destination and stays reachable by
back-press. The contrast that makes it safe is with `/admin/navigation`, which
has three guards because hiding it would strip the only way to undo an override;
nothing about a hidden Home row is unrecoverable.
"Phase 5 as landed" records the sort-key finding above as the thing the spec did
not settle, that an untouched instance gets `APP_MENU` back by **identity** so
AC-1's drawer claim is an `assertSame`, that the app's own rows are partitioned
off rather than sorted (and what that would mean for a future interleaved row),
that `group`/`section` are read and dropped, and why `Routes.NEWS_ROUTE` is
declared beside `Routes.NEWS` rather than replacing it — with the consequence that
`destination.route` now carries a query and is compared on `substringBefore('?')`.
Android-app: RunicGateway/Android-app#38.
Co-Authored-By: Claude <noreply@anthropic.com>
§5.6 gains the three decisions the drafted section left open — the top bar's
text fallback, the hero's fixed cropped band, and the accessibility split
between a decorative logo and a named one — each with the reasoning that
settled it, so none is re-litigated from scratch. Adds the 32dp/24dp logo
heights, the six-times-height width cap, and the note that the app takes no
fallback hero image where the website substitutes its own emblem.
"Phase 4 as landed" records how the empty-slot rule is enforced by layout
rather than by a conditional at each call site, why the blank check runs on
both sides of the resolver, and the honest limit of the phase's tests: the
drawing cannot be tested here at all, since the app has no Robolectric and no
androidTest source set, so only the decision of whether to draw is pure.
Phase 4 ticked in §8.
Co-Authored-By: Claude <noreply@anthropic.com>
§5.3 rewritten against what the build measured rather than what the plan
estimated, plus a "Phase 3 as landed" section and the ✅ in §8.
Three corrections to the drafted section:
- The APK estimate was wrong by more than 3×. "Roughly 1.5-2.5 MB" becomes a
measured before/after table: 5,031,411 B → 13,574,703 B, +8.15 MiB. The
section now records that Merriweather is 6.08 MiB of that on its own, and the
two cheaper options the org lead costed and declined, so the number is not
re-litigated from scratch later.
- Family lookup must be global, never scoped to the role's option list. Two of
the three presets publish a font their own role's dropdown does not offer,
because resolveThemeTokens copies preset tokens verbatim — the same bypass
§5.4 already records for --shadow-card.
- Italics mirror the website's four rather than being skipped, and every
bundled family supplies the four weights the scale asks for (with IM Fell
English's single weight and Cinzel's untouched 500/600/700 as the two
documented exceptions), because a family is not confined to the role its
dropdown lives in.
Android-app: RunicGateway/Android-app#TBD
Co-Authored-By: Claude <noreply@anthropic.com>
The radii-and-shadow phase, shipped as Android-app#35. Seven notes, of which
three are amendments to the spec rather than records of it.
Section 5.4's elevation map is not a no-op and now says so, with section 2 and
AC-1 amended to match. Material3's filled Card is Level0 and FeatureCard drew
none of the shadow its own KDoc claimed, so the app has been flat since M5 -
while the runic-gateway preset it was drawn from selects the "Default" shadow.
The org lead chose to apply the map as written rather than rebase it on the flat
baseline, because rebasing would have collapsed none/Soft/Default onto 0dp and
left only Deep doing anything on the phone. The radius half is untouched by this
and remains a provable no-op.
Section 5.4's exact-string match is corrected to a nearest-blur match. The
fantasy preset's own --shadow-card is not one of SHADOW_OPTIONS' four values,
because resolveThemeTokens copies a preset's tokens verbatim and they never pass
through the admin form's dropdown.
Section 5.2 gains the rule the pill needed: CircleShape is a percentage, so it
has no shipped dp for a ratio to scale and the resolved px is taken as dp below
the 500px floor. It is a literal because there is no app scale to preserve, not
as an exception to the ratio rule.
Also recorded: why all 24 Card( call sites became ShardCard( (Material takes
elevation as a default argument, not from the theme), that --radius-pill reaches
only StatusPill, and that Shapes - unlike ColorScheme - does implement equals,
so the structural no-op proof is one assertion rather than a reflection walk.
PLAN.md 9's M12 entry records phase 2 landed.
Co-Authored-By: Claude <noreply@anthropic.com>
The colors phase, shipped as Android-app#34. Seven notes, of which three are
departures the build forced rather than choices it made:
ColorScheme has no equals in material3 1.3.0, so AC-1's "full equality, not a
spot check" is a field-by-field compare by reflection over all 36 color roles,
against a verbatim copy of the pre-M12 scheme held in the test.
ShardPillFg is derived from --accent-bright rather than being a sixteenth
token, under the rule 5.1 already states for ShardOnCta.
An instance with an env accent and no theme_visual row loses its accented CTA
buttons, because brand.accent now seeds --accent alone. That is 5.1's "wrong
twice over" being corrected, and it is the one thing on screen that moves.
PLAN.md 9's M12 entry records phase 1 landed.
Co-Authored-By: Claude <noreply@anthropic.com>
Two records, one milestone.
Phase 7 (reading nav_admin / nav_player) is cancelled. Section 6.4 already
made the case against it and scheduled it last so the call could be taken on
its merits with the rest working: the two authenticated navs reach four app
rows between them, which does not pay for a new authenticated fetch, a
session-keyed cache and its teardown. The app therefore makes no authenticated
settings call at all, and the player and staff drawer rows keep their coded --
and so localized -- labels, which is the one thing given up. Section 4's locked
decision, section 3's endpoint row and section 5.5's lifecycle paragraph are
amended to match so the doc does not contradict itself, and section 8 gains
the standing "Phase 7, cancelled" note.
Phase 0 (the contract and the appearance store) landed as Android-app#33, and
its "as landed" notes record two departures from what section 8 specified:
- `theme` is modeled as a raw JsonElement rather than Map<String,String>?.
kotlinx fails the decode of the whole object on a value of an unexpected
kind, and `theme` shares its payload with `brand` and `push` -- so one odd
token would have blanked the branding and dropped the push relay URL, the
opposite of section 2. It is coerced field-by-field instead.
- A failed refresh keeps the last good appearance rather than falling back to
NONE. Section 5.5 said best-effort; the distinction it did not draw is that
a moment of no connectivity on resume must not repaint a themed shard back
to the shipped defaults.
Co-Authored-By: Claude <noreply@anthropic.com>
The website merged runtime admin theming, brand assets and nav overrides to
main (website#126 / docs#109). The app reads exactly one field of it --
brand.accent -- and renders a hardcoded APP_MENU, so an admin who re-skins the
site and restructures the header sees none of it on the phone.
Adds docs/android/THEMING_AND_NAV.md as the design of record for M12, and the
PLAN.md §9 entry that anchors it. Plan only: no app code, no backend work.
Everything consumed is already live on website/main.
The points that shaped it:
- The app's ui/theme/Color.kt palette is already, value for value, the
runic-gateway preset -- M5 was drawn from the same theme.css the preset was
later extracted from. So "an untouched instance is unchanged" carries over as
a testable ColorScheme equality assertion, not an approximation.
- Radii apply as a ratio against that baseline, not as literal dp. The app's
Shapes came from the M5 mockup and genuinely differ (medium 12dp vs
--radius-card 10px); a literal mapping would restyle the untouched app the
day this ships, and copying the app's scale into the server would be a second
source of truth.
- Fonts are bundled, not downloadable: the Play Store font provider makes a
de-Googled device fall back silently. Seven families join the bundled Cinzel.
- Nav overrides are keyed by website paths, so the app needs a path -> route
table -- the one new cross-repo coupling here. An override for a path the app
does not surface in its menu is ignored: a nav override may never introduce
navigation.
- The gates are untouched. MenuAccess and MenuEntry.feature still run after the
merge, so hidden:false cannot un-hide what a role or the shard's visibility
config withholds.
- Read the resolved theme/brand fields, never the raw theme_visual/brand_assets
rows that ride along in the same payload -- re-deriving a palette from them
would be a second resolveThemeTokens in Kotlin, guaranteed to drift.
Nine phases into a fresh edge in both repos, reaching main as one edge -> main
merge, the same shape the website side used. Phase 0 must change nothing on
screen. Phase 7 (the authenticated navs) is marked optional: nav_player reaches
two app rows and nav_admin two, which is a thin return for a new authenticated
fetch and its cache teardown.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgfKv5cz5pbY3dPeofSE5a
Records the capability asked for before the edge -> main cutover: dropdown
sections in the public header, with the coded entries organised into them and
admin-authored links alongside.
§7 is amended rather than quietly contradicted. It said the override layer
"cannot introduce a `to` that is not already in the hardcoded NAV array"; that
remains true of every CODED entry, and the restated constraint spells out what an
added link may be — a same-origin path, carrying no gate of its own, advertising
a route rather than granting one — plus why the property is structural: coded
entries live in a map keyed by routes the base array declares, and everything
that can name an arbitrary path lives in `links`, where the rule is applied.
§6.4 gains the { items, sections, links } wrapper, including the two properties
worth knowing: a bare map still reads as the items map, and a nav with no
sections still stores one.
§9 gains three acceptance criteria — the empty dropdown does not render, an added
link cannot leave the origin, and deleting a section returns its entries to the
top level rather than removing them.
"Phase 10 as landed" records why the Public tab needed its own tree editor, why
moving between containers stayed a dropdown rather than a cross-container drag,
why the menu opens on click and its trigger is not a link, and the palette-vs-full-nav
bug this surfaced in the phase 6-8 save path.
Co-Authored-By: Claude <noreply@anthropic.com>
Marks the nav wiring and the builder UI landed, and records the five places the
build differed from the design:
- The server had no way to store a nav row. The design scoped 6-8 as client
work, but updateSettings would have written a nav object as "[object Object]"
— a save that 200s and does nothing, for ever.
- The server deliberately cannot check that a `to` exists: the base NAV arrays
are client constants, and a server-side copy would be a second source of truth
for navigation. Shape is the server's question, membership the client's.
- `hidden: false` is accepted and never stored, so hiding stays subtractive.
- The nav editor cannot be hidden, enforced in three places.
- Orders are written only when something actually moved, compared against the
base restricted to the rows the editing admin can see.
Phase 9 (hue-carrying rgba literals + Parchment) is cancelled rather than
deferred. The finding that motivated it is kept as the record: those literals
carry a hue, so they are a rough edge in the three dark presets and not only a
blocker for a hypothetical light one.
Co-Authored-By: Claude <noreply@anthropic.com>
Records Phase 5 of THEMING_AND_NAV.md as landed and documents the new route
and the shell lifecycle in BACKEND_DESIGN.md.
Where the build differed from the design: the upload is one admin-only call
that writes the settings row too (rather than the generic staff upload plus a
PUT, which would leave unreferenced files and let editors change the site's
identity); brand_assets needed a validator of its own because these are the
only settings values written straight into HTML as URLs; the shell cache
carries a TTL as well as explicit invalidation because it is per process; and
the logo went into all six MoonDot surfaces rather than three.
Also notes what was deliberately left alone: the shell's title and description
still come from BRAND_NAME rather than the admin-set site_title, and fixing
that would change the served shell for instances with no brand_assets row —
which is exactly what the phase's acceptance criterion forbids.
Co-Authored-By: Claude <noreply@anthropic.com>
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>