Phase 3's acceptance criterion 1, made real. Three things, one review: **The dead bindings.** `client/src/api/client.js` still carried ~190 lines of UO namespaces — `shard`, `atlas`, the two SSE URLs, `admin.shard/shardOps/atlas/ userShard`, the uo-link and town-crier calls, `player.shard` — with zero core consumers since slice 3 deleted the views. module-uo vendors its own bindings. The five assertions core's `apiClient.test.js` made about those URLs moved with them (Module-uo#5); the encoding test that used `governorHistory` now uses a core route. **The copy.** Core is the platform, not one game's site, so its words are game-neutral now: `About`, `Screenshots`, `Website`'s cards, `Status` (which was never about a game server at all — it reports site mode), `Wiki`, `SiteFooter`, the default hero, `brand.js`'s tagline and description, the seeded wiki categories, and two user-visible NavEditor strings that named a module's admin screen by its proper name. Which game an instance is for is the operator's to say — BRAND_* vars, the hero editor, CMS pages — and every real instance already does: `.env.uomysticmoon.example` sets both brand strings explicitly, so nothing live changes wording. Wiki page SLUGS are untouched: `seedDefault*` only inserts what is absent, so renaming one adds a duplicate page to every install. Also gone: an orphan comment block in `schema.sql` describing the spawn-atlas tables slice 1 took away, and the two settings rows core seeded for a module (`game_account_signup`, `uo_link_protocol_3_migrated`). The second was a live defect — see Module-uo#5, which takes ownership of both and repairs the one-shot migration core's ordering had disabled. **The check.** `scripts/checkModuleIdentifiers.js` + `npm run check:modules`, first step of the server-tests job because it needs no dependencies. It reads CODE, not prose — file names, import specifiers, route path literals, declared identifiers and property names — per §5.2, so core's English may still say "shard" where saying it is worth more than the word costs. Two things it gets right only because getting them wrong was tried first: it matches WHOLE WORDS (a substring pass flags `defaultImage`, which contains "ultIma", four times in this repo), and it strips comments and string bodies in one character walk (a comment contains quotes, a string contains `//`) — the `checkImports.js` lesson. It has its own 17-test suite, because a boundary check that silently stops checking is worse than none. The three §6.5 grandfathering allowlists are exempt by name, and an exemption that stops matching fails the build rather than lingering. BREAKING CHANGE: core no longer seeds `game_account_signup` or `uo_link_protocol_3_migrated`; module-uo's schema fragment does. An install running core without module-uo keeps whatever rows it already has and gains no new ones — nothing in core reads either key. Deferred to slice 5, deliberately: README.md's 48 UO mentions, including a `## Shard integration (uo-link)` section and the architecture diagram. That is documentation, which §5.2 does not cover, and it belongs with the phase-closing docs pass rather than half-done here. Co-Authored-By: Claude <noreply@anthropic.com>
229 lines
11 KiB
JavaScript
229 lines
11 KiB
JavaScript
import { Routes, Route, Navigate, Outlet } from 'react-router-dom'
|
|
import { AuthProvider } from './contexts/AuthContext.jsx'
|
|
import { SiteProvider } from './contexts/SiteContext.jsx'
|
|
import MaintenanceGate from './components/MaintenanceGate.jsx'
|
|
import RequireAuth from './components/RequireAuth.jsx'
|
|
import RequirePlayer from './components/RequirePlayer.jsx'
|
|
import RoleGate from './components/RoleGate.jsx'
|
|
import { routesFor } from './modules/registry.js'
|
|
import { ModuleFeaturesProvider } from './modules/features.jsx'
|
|
|
|
// Public
|
|
import Portal from './routes/public/Portal.jsx'
|
|
import Website from './routes/public/Website.jsx'
|
|
import News from './routes/public/News.jsx'
|
|
import Screenshots from './routes/public/Screenshots.jsx'
|
|
import FiveOnFriday from './routes/public/FiveOnFriday.jsx'
|
|
import Newsletter from './routes/public/Newsletter.jsx'
|
|
import NewsletterIssue from './routes/public/NewsletterIssue.jsx'
|
|
import About from './routes/public/About.jsx'
|
|
import Status from './routes/public/Status.jsx'
|
|
import Wiki from './routes/wiki/Wiki.jsx'
|
|
import WikiArticle from './routes/wiki/WikiArticle.jsx'
|
|
import CmsPage from './routes/public/CmsPage.jsx'
|
|
|
|
// Admin
|
|
import AdminLogin from './routes/admin/AdminLogin.jsx'
|
|
import AdminLayout from './routes/admin/AdminLayout.jsx'
|
|
import Dashboard from './routes/admin/views/Dashboard.jsx'
|
|
import PostsAdmin from './routes/admin/views/PostsAdmin.jsx'
|
|
import PagesAdmin from './routes/admin/views/PagesAdmin.jsx'
|
|
import PageBuilder from './routes/admin/views/PageBuilder.jsx'
|
|
import WikiAdmin from './routes/admin/views/WikiAdmin.jsx'
|
|
import HeroEditor from './routes/admin/views/HeroEditor.jsx'
|
|
import AppearanceAdmin from './routes/admin/views/AppearanceAdmin.jsx'
|
|
import NavEditor from './routes/admin/views/NavEditor.jsx'
|
|
import SettingsAdmin from './routes/admin/views/SettingsAdmin.jsx'
|
|
import ActivityAdmin from './routes/admin/views/ActivityAdmin.jsx'
|
|
import BotActivityAdmin from './routes/admin/views/BotActivityAdmin.jsx'
|
|
import DiscordBotAdmin from './routes/admin/views/DiscordBotAdmin.jsx'
|
|
import AuthProvidersAdmin from './routes/admin/views/AuthProvidersAdmin.jsx'
|
|
import UsersAdmin from './routes/admin/views/UsersAdmin.jsx'
|
|
import UserDetail from './routes/admin/views/UserDetail.jsx'
|
|
import InvitesAdmin from './routes/admin/views/InvitesAdmin.jsx'
|
|
import AccountAdmin from './routes/admin/views/AccountAdmin.jsx'
|
|
import Moderation from './routes/admin/views/Moderation.jsx'
|
|
import ModerationUser from './routes/admin/views/ModerationUser.jsx'
|
|
import Appeals from './routes/admin/views/Appeals.jsx'
|
|
|
|
// Player portal
|
|
import PlayerLogin from './routes/player/PlayerLogin.jsx'
|
|
import PlayerRegister from './routes/player/PlayerRegister.jsx'
|
|
import ForgotPassword from './routes/player/ForgotPassword.jsx'
|
|
import ResetPassword from './routes/player/ResetPassword.jsx'
|
|
import AcceptInvite from './routes/player/AcceptInvite.jsx'
|
|
import PlayerPortalLayout, { PlayerIndex } from './routes/player/PlayerPortalLayout.jsx'
|
|
import PlayerAccount from './routes/player/PlayerAccount.jsx'
|
|
import PlayerAppeals from './routes/player/PlayerAppeals.jsx'
|
|
|
|
export default function App() {
|
|
return (
|
|
<AuthProvider>
|
|
<SiteProvider>
|
|
{/* Inside the auth and site contexts, because a feature provider is a
|
|
hook that may well read either — a live-status one does, indirectly,
|
|
by asking an endpoint whose answer depends on the session. Outside the
|
|
routes, so the nav in every layout is filtered by the same gate and
|
|
the provider hooks are called once for the whole app rather than
|
|
once per screen. */}
|
|
<ModuleFeaturesProvider>
|
|
<Routes>
|
|
{/* Landing hero — always public, even in maintenance mode. The hero is
|
|
itself the pre-launch "coming soon" page, so it sits outside the
|
|
MaintenanceGate and every visitor sees it regardless of auth/site mode. */}
|
|
<Route path="/" element={<Portal />} />
|
|
|
|
{/* Rest of the public site — gated by maintenance mode (admins preview through it) */}
|
|
<Route
|
|
element={
|
|
<MaintenanceGate>
|
|
<Outlet />
|
|
</MaintenanceGate>
|
|
}
|
|
>
|
|
<Route path="/site" element={<Website />} />
|
|
<Route path="/site/news" element={<News />} />
|
|
<Route path="/site/screenshots" element={<Screenshots />} />
|
|
<Route path="/site/five-on-friday" element={<FiveOnFriday />} />
|
|
<Route path="/site/newsletter" element={<Newsletter />} />
|
|
<Route path="/site/newsletter/:id" element={<NewsletterIssue />} />
|
|
<Route path="/site/about" element={<About />} />
|
|
<Route path="/site/status" element={<Status />} />
|
|
<Route path="/wiki" element={<Wiki />} />
|
|
<Route path="/wiki/:slug" element={<WikiArticle />} />
|
|
{/* Installed modules' public pages, namespaced `/<id>/…` — the
|
|
registry prefixes the segment, so a module cannot spell its way
|
|
out of it (docs/website/MODULE_API.md §3.3). Declared before the
|
|
CMS catch-all below: React Router ranks a static segment over a
|
|
dynamic one, so the order is not what saves us, but keeping the
|
|
two adjacent makes the relationship visible to whoever adds the
|
|
next route here. */}
|
|
{routesFor('public').map((r) => (
|
|
<Route key={r.path} path={`/${r.path}`} element={r.element} />
|
|
))}
|
|
{/* CMS pages: top-level /:slug, matched only after the named routes
|
|
above (React Router ranks static routes over this dynamic one). */}
|
|
<Route path="/:slug" element={<CmsPage />} />
|
|
</Route>
|
|
|
|
{/* Draft-preview link (token-gated). Outside the maintenance gate so a
|
|
preview link works regardless of site mode. */}
|
|
<Route path="/preview/:id/:token" element={<CmsPage preview />} />
|
|
|
|
{/* Admin */}
|
|
<Route path="/admin/login" element={<AdminLogin />} />
|
|
<Route
|
|
path="/admin"
|
|
element={
|
|
<RequireAuth>
|
|
<AdminLayout />
|
|
</RequireAuth>
|
|
}
|
|
>
|
|
<Route index element={<Dashboard />} />
|
|
<Route path="posts" element={<PostsAdmin />} />
|
|
<Route path="pages" element={<PagesAdmin />} />
|
|
<Route path="pages/new" element={<PageBuilder />} />
|
|
<Route path="pages/:id" element={<PageBuilder />} />
|
|
<Route path="wiki" element={<WikiAdmin />} />
|
|
<Route path="hero" element={<HeroEditor />} />
|
|
{/* Theme editing writes an admin-only settings key; the route sits
|
|
behind the same RoleGate as the sidebar entry that reaches it,
|
|
and PUT/DELETE /admin/settings is admin-only server-side too. */}
|
|
<Route
|
|
path="appearance"
|
|
element={
|
|
<RoleGate roles={['admin']}>
|
|
<AppearanceAdmin />
|
|
</RoleGate>
|
|
}
|
|
/>
|
|
{/* Same reasoning as Appearance: the nav overrides are an admin-only
|
|
settings key, so the route carries the same RoleGate as the
|
|
sidebar entry that reaches it. */}
|
|
<Route
|
|
path="navigation"
|
|
element={
|
|
<RoleGate roles={['admin']}>
|
|
<NavEditor />
|
|
</RoleGate>
|
|
}
|
|
/>
|
|
<Route path="settings" element={<SettingsAdmin />} />
|
|
<Route
|
|
path="moderation"
|
|
element={
|
|
<RoleGate roles={['admin', 'moderator']}>
|
|
<Outlet />
|
|
</RoleGate>
|
|
}
|
|
>
|
|
<Route index element={<Moderation />} />
|
|
<Route path="user/:discordId" element={<ModerationUser />} />
|
|
<Route path="appeals" element={<Appeals />} />
|
|
</Route>
|
|
<Route path="activity" element={<ActivityAdmin />} />
|
|
<Route path="bot-activity" element={<BotActivityAdmin />} />
|
|
<Route path="discord-bot" element={<DiscordBotAdmin />} />
|
|
<Route path="auth-providers" element={<AuthProvidersAdmin />} />
|
|
<Route path="users" element={<UsersAdmin />} />
|
|
<Route path="users/:id" element={<UserDetail />} />
|
|
<Route path="invites" element={<InvitesAdmin />} />
|
|
<Route path="account" element={<AccountAdmin />} />
|
|
{/* Installed modules' admin pages, at /admin/<id>/…, already inside
|
|
RequireAuth + AdminLayout. A module cannot supply its own auth
|
|
wrapper — only an optional { roles }, which core applies as the
|
|
same RoleGate its own routes above use, so the sidebar and the
|
|
route table cannot disagree about who may see what. Before the
|
|
`*` redirect, which would otherwise swallow every one of them. */}
|
|
{routesFor('admin').map((r) => (
|
|
<Route
|
|
key={r.path}
|
|
path={r.path}
|
|
element={r.gate ? <RoleGate roles={r.gate.roles}>{r.element}</RoleGate> : r.element}
|
|
/>
|
|
))}
|
|
<Route path="*" element={<Navigate to="/admin" replace />} />
|
|
</Route>
|
|
|
|
{/* Player portal */}
|
|
<Route path="/account/login" element={<PlayerLogin />} />
|
|
<Route path="/account/register" element={<PlayerRegister />} />
|
|
<Route path="/account/forgot" element={<ForgotPassword />} />
|
|
<Route path="/account/reset/:token" element={<ResetPassword />} />
|
|
<Route path="/invite/:token" element={<AcceptInvite />} />
|
|
<Route
|
|
element={
|
|
<RequirePlayer>
|
|
<PlayerPortalLayout />
|
|
</RequirePlayer>
|
|
}
|
|
>
|
|
{/* The portal index resolves to the first nav row this viewer can
|
|
reach rather than naming a page: `PlayerCharacters` was a UO
|
|
page and left with the client half (MODULE_SYSTEM.md §2.7.1).
|
|
With the UO module installed that is still Characters. */}
|
|
<Route path="/player" element={<PlayerIndex />} />
|
|
<Route path="/account" element={<PlayerAccount />} />
|
|
<Route path="/account/appeals" element={<PlayerAppeals />} />
|
|
{/* Installed modules' player-portal pages, at /player/<id>/…. This
|
|
group's own routes are absolute (its layout route has no path),
|
|
so the prefix is written here rather than inherited — the one
|
|
place the three areas do not read alike. */}
|
|
{routesFor('player').map((r) => (
|
|
<Route
|
|
key={r.path}
|
|
path={`/player/${r.path}`}
|
|
element={r.gate ? <RoleGate roles={r.gate.roles}>{r.element}</RoleGate> : r.element}
|
|
/>
|
|
))}
|
|
</Route>
|
|
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
</ModuleFeaturesProvider>
|
|
</SiteProvider>
|
|
</AuthProvider>
|
|
)
|
|
}
|