refactor(modules)!: de-UO core's copy, and enforce it (phase 3, slice 4)
All checks were successful
PR Checks / bot-install (pull_request) Successful in 18s
PR Checks / client-build (pull_request) Successful in 27s
PR Checks / server-tests (pull_request) Successful in 31s

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>
This commit is contained in:
2026-08-11 21:41:18 -05:00
parent a99ead4ee4
commit 0c4eacfa4a
21 changed files with 551 additions and 289 deletions

View File

@@ -35,7 +35,8 @@ import { NAV as PLAYER_NAV } from '../../player/PlayerPortalLayout.jsx'
// Three things shape the screen:
//
// • The palette is filtered to the editing admin's OWN visible rows (§8.1) —
// the base array run through their role and this shard's feature gates. An
// the base array run through their role and the feature gates of whichever
// module registered each row (client/src/modules/featureGate.js). An
// admin cannot drag in, and so can never accidentally advertise, something
// they cannot see themselves. An override on a row they cannot see is
// carried through their save untouched rather than quietly reset.
@@ -458,8 +459,8 @@ export default function NavEditor() {
<section style={{ maxWidth: 860, display: 'flex', flexDirection: 'column', gap: 22 }}>
<p className="sans dim" style={{ margin: 0, fontSize: '0.82rem', lineHeight: 1.7 }}>
Rename, reorder and hide the entries in each navigation. The pages themselves are unchanged this
only decides what is advertised, and it can never show anyone a link their role or this shard&rsquo;s
visibility settings would hide.
only decides what is advertised, and it can never show anyone a link their role, or the visibility
settings of an installed module, would hide.
</p>
{/* ── Tabs ───────────────────────────────────────────────── */}
@@ -552,8 +553,8 @@ export default function NavEditor() {
</div>
<p className="sans dim" style={{ margin: 0, fontSize: '0.76rem', lineHeight: 1.7 }}>
Only entries you can see yourself are listed. Anything hidden from you by your role or by Shard
Visibility keeps whatever it was already set to.
Only entries you can see yourself are listed. Anything hidden from you by your role, or by a
module&rsquo;s visibility settings, keeps whatever it was already set to.
</p>
</section>
)

View File

@@ -10,19 +10,19 @@ export default function About() {
<PageHeader eyebrow="About" title={`About ${siteShortName}`} />
<div className="prose">
<p>
{siteShortName} is an independent, privately-run Ultima Online shard built by a small group of long-time players.
It is not affiliated with or endorsed by the owners of Ultima Online it is a labor of love for the old
worlds and the friendships made in them.
{siteShortName} is an independent, privately-run game server built by a small group of long-time
players. It is not affiliated with or endorsed by the owners of the game it runs it is a labor of
love for the old worlds and the friendships made in them.
</p>
<p>
Our aim is a calm, hand-tended world: a contested wilderness worth exploring, safe towns worth living in,
and systems that reward curiosity over grind. We are building slowly and in the open, sharing news,
Our aim is a calm, hand-tended world: somewhere worth exploring, somewhere worth living in, and
systems that reward curiosity over grind. We are building slowly and in the open, sharing news,
screenshots, and guides as the world comes online.
</p>
<h2>What to expect</h2>
<ul>
<li>A hybrid ruleset safe towns, a dangerous wild.</li>
<li>Custom crafting, housing, and exploration content.</li>
<li>A world that is hand-tended rather than left to run itself.</li>
<li>Custom content, and changes explained before they land.</li>
<li>A small, friendly population and an active wiki.</li>
</ul>
</div>

View File

@@ -16,7 +16,7 @@ export default function Screenshots() {
<PageHeader
eyebrow="Gallery"
title="Gameplay Pictures"
lead="Glimpses of towns, dungeons, events, and daily life on the shard."
lead="Glimpses of the world, its events, and daily life on the server."
/>
{loading && <Loading />}
{error && <ErrorState message="Could not load the gallery right now." />}

View File

@@ -19,7 +19,7 @@ export default function Status() {
return (
<PublicLayout section="website">
<div className="shell-narrow page-body">
<PageHeader eyebrow="Live" title="Shard Status" />
<PageHeader eyebrow="Live" title="Site Status" />
{loading && <Loading />}
{error && <ErrorState message="Could not load status right now." />}
@@ -58,7 +58,7 @@ export default function Status() {
{isLive ? 'Live — the gates are open' : 'Maintenance — building in progress'}
</strong>
<span style={{ color: isLive ? '#a9cdb8' : '#cdbf9a', fontSize: '0.98rem' }}>
{statusMessage || (isLive ? 'The shard is online.' : 'The gates are closed while we shape the world. Public login is not open yet.')}
{statusMessage || (isLive ? 'The site is open.' : 'The gates are closed while we shape the world. Public login is not open yet.')}
</span>
</div>
</section>

View File

@@ -4,12 +4,12 @@ import PageHeader from '../../components/PageHeader.jsx'
import { useSite } from '../../contexts/SiteContext.jsx'
const CARDS = [
{ kicker: 'Gallery', title: 'Gameplay Pictures', body: 'Screenshots from towns, dungeons, events, and daily life on the shard.', to: '/site/screenshots' },
{ kicker: 'Updates', title: 'Development News', body: 'Progress notes, shard milestones, and public announcements.', to: '/site/news' },
{ kicker: 'Gallery', title: 'Gameplay Pictures', body: 'Screenshots of the world, its events, and daily life on the server.', to: '/site/screenshots' },
{ kicker: 'Updates', title: 'Development News', body: 'Progress notes, project milestones, and public announcements.', to: '/site/news' },
{ kicker: 'Community', title: 'Five on Friday', body: 'Weekly questions, small previews, and notes from the team.', to: '/site/five-on-friday' },
{ kicker: 'Long-form', title: 'Monthly Newsletter', body: 'Fuller summaries for players who want the whole picture.', to: '/site/newsletter' },
{ kicker: 'Reference', title: 'Wiki', body: 'Guides and reference pages for the game world.', to: '/wiki' },
{ kicker: 'Live', title: 'Shard Status', body: 'Launch state, test windows, and known issues.', to: '/site/status' },
{ kicker: 'Live', title: 'Site Status', body: 'Launch state, test windows, and known issues.', to: '/site/status' },
]
export default function Website() {

View File

@@ -97,7 +97,7 @@ export default function Wiki() {
center
eyebrow="Knowledge base"
title={`${siteShortName} Wiki`}
lead="A calm starting point for shard guides, the world and its lore, gameplay systems, and community rules."
lead="A calm starting point for guides, the world and its lore, gameplay systems, and community rules."
/>
<SearchBox initial={activeQ || ''} onSubmit={runSearch} />