feat(modules): publish the installed-module list at /api/v1/public/modules
Phase 2, PR 6 of docs/website/MODULE_SYSTEM.md 2.7 — the first module-system
URL a client can see. The SPA and the Android app feature-detect against the
capabilities a module declares; the shape is settled in MODULE_API.md 2.9.
Four decisions, and what is absent from the payload is most of the design:
* started modules only. A module that is disabled or failed to load is
ABSENT, exactly as 4.4 already leaves its routes and its nav absent, so a
client renders a site without that capability rather than advertising one
that 503s.
* no state, failure_stage or failure_reason. Where a module broke belongs to
the admin Modules screen, and the reason is an exception string from inside
core — not anonymous-visitor business.
* no client chunk URL. htmlShell injects a script tag per started module
(3.1.3), so the browser is handed the tag rather than a URL to fetch. This
endpoint feature-detects; it does not load. MODULE_SYSTEM 2.6 step 4 is
amended to match (API 6.7).
* no siteMode gate and no database — the same class as /public/status and
/public/version, so a client can still feature-detect during maintenance.
It is a capability router of its own rather than a fifth singleton in
site.router.js, and that is load-bearing: the loader's prefix-collision probe
reads the live tier stack and skips root-mounted layers, because a use('/', ...)
matches every path. A route inside the root-mounted site router would be
invisible to it — mounting use('/modules', ...) is what makes "no module may
claim /modules" a rule the loader enforces.
910 tests pass (+9, every one on the boundary — what must NOT appear).
routes.manifest.json gains exactly the one route and routes.guards.json records
it with an empty gates list, which is itself the assertion that it is ungated.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -737,6 +737,31 @@ const doc = {
|
||||
server: { type: 'string', example: '1.0.0', description: 'Server package version (informational).' },
|
||||
},
|
||||
},
|
||||
PublicModules: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Installed modules currently SERVING (GET /public/modules). A disabled or failed module is absent, not listed with a state — its routes and nav are absent too. Database-free and not site-mode gated.',
|
||||
properties: {
|
||||
modules: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/PublicModule' },
|
||||
},
|
||||
},
|
||||
},
|
||||
PublicModule: {
|
||||
type: 'object',
|
||||
description: 'One started module, as published to anonymous clients.',
|
||||
properties: {
|
||||
id: { type: 'string', example: 'uo', description: 'Module id — also the URL segment its routes live under (/api/v1/public/<id-owned prefixes>).' },
|
||||
name: { type: 'string', example: 'Ultima Online', description: 'Human label.' },
|
||||
version: { type: 'string', example: '1.0.0', description: 'The module\'s own version (semver). Unrelated to the API version.' },
|
||||
capabilities: {
|
||||
type: 'array',
|
||||
description: 'Opaque strings the module declares. Feature-detect against them; treat an unknown one as absent.',
|
||||
items: { type: 'string', example: 'shard' },
|
||||
},
|
||||
},
|
||||
},
|
||||
Brand: {
|
||||
type: 'object',
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user