The module's half of the org lead's correction: Teams is the contract, guilds are the presentation, and the presentation is this module's. Adds `/uo/guilds/:id` — the detail view the board never had — with the roster from this module's OWN board, which is the same data it answers core's Team provider from. Reading core's projection of our own answer back would be a round trip through a staler copy of it. The page declares `uo.guild.detail` and core fills it with the Team activity feed. That is the one part of this page core cannot hand over: only core can resolve whether the viewer is inside the Team, and the public/members split on that feed is a security boundary. The guild is named in OUR terms — core maps its own Team from the module id and the external id — so this module never holds core's row id or slug. `TeamOverviewStrip` is deleted with the core Team page it filled. `team.member.row` is not declared here either: the useful thing to put in a roster row is a link to the character behind it, and nothing core could supply identifies one. `GET /public/shard/guilds/:id` backs the page, gated and projected through the same `guilds` feature as the board — so an operator who raises that audience raises this too, and the locked acct/webId fields never survive below admin. A roster is where those appear in bulk, which makes this the endpoint where getting the projection wrong would matter most. Co-Authored-By: Claude <noreply@anthropic.com>
203 lines
11 KiB
JavaScript
203 lines
11 KiB
JavaScript
// ── module-uo's client entry point ─────────────────────────────────────────
|
|
//
|
|
// Core injects `dist/entry.js` as a `<script type="module" src>` before
|
|
// `</body>`, this file registers what the module has, and core renders it. The
|
|
// normative contract is MODULE_API.md §3.3.
|
|
//
|
|
// **Registration is synchronous and happens at evaluation time.** Module scripts
|
|
// are deferred, so this runs after core's bundle — which is where `window.__rg`
|
|
// is published — and before DOMContentLoaded, which is what core waits for
|
|
// before its first render. There is no subscription and no late registration: a
|
|
// module that registered asynchronously would register after the routes had been
|
|
// read, and the symptom is a page that redirects home with nothing logged. That
|
|
// bug cost the Phase 2 client PR an afternoon and no unit test in either repo
|
|
// can see it, which is why §7.7's browser smoke exists.
|
|
//
|
|
// So everything below is a plain top-level call, and every page is a static
|
|
// import. Lazy-loading the routes would be the natural instinct for a chunk this
|
|
// size and it is the one thing this seam cannot have.
|
|
|
|
import { registry, coreApiVersion } from './core.js'
|
|
import { IconShard, IconUser } from './icons.jsx'
|
|
import { useShardFlags } from './lib/useShardFeatures.js'
|
|
|
|
// Public pages — the twelve that used to live at /site/*.
|
|
import Shard from './routes/public/Shard.jsx'
|
|
import ShardActivity from './routes/public/ShardActivity.jsx'
|
|
import ChampSpawns from './routes/public/ChampSpawns.jsx'
|
|
import Guilds from './routes/public/Guilds.jsx'
|
|
import Guild from './routes/public/Guild.jsx'
|
|
import Governors from './routes/public/Governors.jsx'
|
|
import Houses from './routes/public/Houses.jsx'
|
|
import Rules from './routes/public/Rules.jsx'
|
|
import Atlas from './routes/public/Atlas.jsx'
|
|
import AtlasCreature from './routes/public/AtlasCreature.jsx'
|
|
import Leaderboards from './routes/public/Leaderboards.jsx'
|
|
import Market from './routes/public/Market.jsx'
|
|
import MarketVendor from './routes/public/MarketVendor.jsx'
|
|
|
|
// Admin views.
|
|
import ShardAdmin from './routes/admin/ShardAdmin.jsx'
|
|
import ShardOps from './routes/admin/ShardOps.jsx'
|
|
import ShardVisibility from './routes/admin/ShardVisibility.jsx'
|
|
import SpawnAtlas from './routes/admin/SpawnAtlas.jsx'
|
|
import HousesAdmin from './routes/admin/HousesAdmin.jsx'
|
|
import AdminCharacters from './routes/admin/AdminCharacters.jsx'
|
|
import AdminCharacter from './routes/admin/AdminCharacter.jsx'
|
|
|
|
// Player-portal views.
|
|
import PlayerCharacters from './routes/player/PlayerCharacters.jsx'
|
|
import PlayerCharacter from './routes/player/PlayerCharacter.jsx'
|
|
|
|
// Extension-slot fills (§3.7) — module content inside a core page.
|
|
import ShardStatusLink from './components/ShardStatusLink.jsx'
|
|
import UserShardSections from './routes/admin/UserShardSections.jsx'
|
|
import InviteGameAccountStep from './components/InviteGameAccountStep.jsx'
|
|
|
|
const ID = 'uo'
|
|
|
|
// ── Routes ─────────────────────────────────────────────────────────────────
|
|
//
|
|
// Paths are relative to this module's namespace and core prefixes them:
|
|
// `/uo/…`, `/admin/uo/…`, `/player/uo/…`. A module cannot write the segment its
|
|
// routes hang under however it spells `path`, which is the point.
|
|
//
|
|
// **These SPA paths changed and the API paths did not.** `/site/shard` is now
|
|
// `/uo/shard` and `/admin/shard-ops` is now `/admin/uo/ops` — a clean break with
|
|
// no redirects, settled in MODULE_SYSTEM.md §2.7. Every URL in `api.js` is
|
|
// byte-identical to the one core called, because §1.2 freezes the API surface
|
|
// and the shipped Android app calls seven of these routes.
|
|
//
|
|
// The admin paths lost their `shard-` prefixes on the way through: under a `/uo/`
|
|
// namespace `/admin/uo/shard-visibility` says "shard" twice, and a clean break is
|
|
// the only moment that tidy-up is free.
|
|
//
|
|
// `gate` is core's own RoleGate, applied by core. A module cannot supply an auth
|
|
// wrapper — the sidebar and the route table have to agree about who may see what.
|
|
const STAFF = { roles: ['admin', 'moderator'] }
|
|
|
|
registry.registerRoutes(ID, {
|
|
public: [
|
|
{ path: 'shard', element: <Shard /> },
|
|
{ path: 'shard/activity', element: <ShardActivity /> },
|
|
{ path: 'champs', element: <ChampSpawns /> },
|
|
{ path: 'guilds', element: <Guilds /> },
|
|
{ path: 'guilds/:id', element: <Guild /> },
|
|
{ path: 'governors', element: <Governors /> },
|
|
{ path: 'houses', element: <Houses /> },
|
|
{ path: 'rules', element: <Rules /> },
|
|
{ path: 'atlas', element: <Atlas /> },
|
|
{ path: 'atlas/:slug', element: <AtlasCreature /> },
|
|
{ path: 'leaderboards', element: <Leaderboards /> },
|
|
{ path: 'market', element: <Market /> },
|
|
{ path: 'market/vendors/:serial', element: <MarketVendor /> },
|
|
],
|
|
admin: [
|
|
// Admin-only: the sidecar's configuration, who may see which surface, and
|
|
// the atlas import. No `gate` on the other three because AdminLayout already
|
|
// requires staff and these carry their own role rows below.
|
|
{ path: 'link', element: <ShardAdmin /> },
|
|
{ path: 'visibility', element: <ShardVisibility /> },
|
|
{ path: 'atlas', element: <SpawnAtlas /> },
|
|
{ path: 'ops', element: <ShardOps />, gate: STAFF },
|
|
{ path: 'houses', element: <HousesAdmin />, gate: STAFF },
|
|
// Self-service, and deliberately ungated: a staff member's own characters
|
|
// are theirs to read whatever their role. Staff are a superset of players.
|
|
{ path: 'characters', element: <AdminCharacters /> },
|
|
{ path: 'characters/:serial', element: <AdminCharacter /> },
|
|
],
|
|
player: [
|
|
{ path: 'characters', element: <PlayerCharacters /> },
|
|
{ path: 'characters/:serial', element: <PlayerCharacter /> },
|
|
],
|
|
})
|
|
|
|
// ── Nav ────────────────────────────────────────────────────────────────────
|
|
//
|
|
// Rows interleave into CORE groups rather than appending as a "UO" block, which
|
|
// is what keeps the extraction invisible in the sidebar (MODULE_SYSTEM.md §1.4).
|
|
//
|
|
// `feature` names a flag resolved by the provider registered below — by THIS
|
|
// module, so the strings are the bare names they have always been and nothing
|
|
// parses a namespace out of them.
|
|
registry.registerNav(ID, {
|
|
area: 'public',
|
|
items: [
|
|
{ label: 'Shard', to: '/uo/shard', feature: 'status' },
|
|
{ label: 'Champions', to: '/uo/champs', feature: 'champs' },
|
|
{ label: 'Guilds', to: '/uo/guilds', feature: 'guilds' },
|
|
{ label: 'Governors', to: '/uo/governors', feature: 'governors' },
|
|
{ label: 'Houses', to: '/uo/houses', feature: 'houses' },
|
|
{ label: 'Rules', to: '/uo/rules', feature: 'ruleset' },
|
|
{ label: 'Atlas', to: '/uo/atlas', feature: 'atlas' },
|
|
{ label: 'Leaderboards', to: '/uo/leaderboards', feature: 'leaderboards' },
|
|
{ label: 'Market', to: '/uo/market', feature: 'market' },
|
|
],
|
|
})
|
|
|
|
registry.registerNav(ID, {
|
|
area: 'admin',
|
|
items: [
|
|
// Moderation: no `order`, because these two are last in that group today and
|
|
// "append after core's rows" is exactly that — and stays that way if core
|
|
// adds a moderation row later, which an explicit index would not.
|
|
{ label: 'In-Game Ops', to: '/admin/uo/ops', icon: IconShard, group: 'Moderation', roles: ['admin', 'moderator'] },
|
|
{ label: 'Houses', to: '/admin/uo/houses', icon: IconShard, group: 'Moderation', roles: ['admin', 'moderator'] },
|
|
// System: these three sit MID-list, between Discord Bot and Web Bot Activity.
|
|
// Core's rows are keyed by their index and an explicit `order` beats a
|
|
// coincidental one at a tie, so all three asking for 8 — Web Bot Activity's
|
|
// index once the UO rows are gone — lands them ahead of it, in this order.
|
|
{ label: 'Shard (uo-link)', to: '/admin/uo/link', icon: IconShard, group: 'System', order: 8, roles: ['admin'] },
|
|
{ label: 'Shard Visibility', to: '/admin/uo/visibility', icon: IconShard, group: 'System', order: 8, roles: ['admin'] },
|
|
{ label: 'Spawn Atlas', to: '/admin/uo/atlas', icon: IconShard, group: 'System', order: 8, roles: ['admin'] },
|
|
// No group: a trailing untitled group of its own, below core's Account row
|
|
// rather than beside it (§3.3). One position lower than it sits today, and
|
|
// the alternative — letting a module into core's furniture groups — is worse.
|
|
{ label: 'My Characters', to: '/admin/uo/characters', icon: IconShard },
|
|
],
|
|
})
|
|
|
|
registry.registerNav(ID, {
|
|
area: 'player',
|
|
// Order 0: Characters is the portal's first row today, and with the module
|
|
// installed it is also what core's `/player` index resolves to.
|
|
items: [{ label: 'Characters', to: '/player/uo/characters', icon: IconUser, order: 0 }],
|
|
})
|
|
|
|
// ── Feature provider ───────────────────────────────────────────────────────
|
|
//
|
|
// Core keeps a generic flag context and owns none of the semantics. Until this
|
|
// slice core registered this same hook itself under owner id `core`, so that the
|
|
// seam was exercised by real content from the day it was built; the registration
|
|
// moves here and core's is deleted.
|
|
registry.registerFeatureProvider(ID, ID, useShardFlags)
|
|
|
|
// ── Extension slots ────────────────────────────────────────────────────────
|
|
//
|
|
// Three core pages have a piece of this module in them. Each was core's own fill
|
|
// under owner id `core` until this slice, so all three are a swap rather than an
|
|
// addition — and each throws rather than failing open if the slot is unknown or
|
|
// already filled, which is how a slice that forgot to delete core's half finds
|
|
// out immediately instead of rendering core's content forever (§3.7).
|
|
registry.registerExtension(ID, 'site.footer.status', ShardStatusLink)
|
|
registry.registerExtension(ID, 'admin.users.detail', UserShardSections)
|
|
registry.registerExtension(ID, 'player.invite.accepted', InviteGameAccountStep)
|
|
// ── The inverted slot: this module DECLARES, core fills ────────────────────
|
|
//
|
|
// The other three above are core's slots that this module fills. This one is the
|
|
// reverse (TEAMS.md Part 3): Teams are a core primitive that this module
|
|
// populates, but core does not own the word "guild" and publishes no Team page of
|
|
// its own — so the page is ours and core contributes the activity feed to it.
|
|
//
|
|
// Declared under this module's own namespace, which core enforces. Core's fill is
|
|
// applied after every module chunk has evaluated, so declaring it here is early
|
|
// enough; on a core that knows nothing of Teams it simply stays empty.
|
|
registry.declareModuleSlot(ID, 'uo.guild.detail')
|
|
|
|
// `module.json`'s `coreApi` range is checked by the loader before this file is
|
|
// ever served, so there is nothing to re-check here. It is logged because a
|
|
// mismatch between the core that validated the manifest and the core that
|
|
// published this global would otherwise be invisible from the browser, which is
|
|
// where the client half actually fails.
|
|
console.info(`[module-uo] registered against core API ${coreApiVersion}`)
|