All checks were successful
PR checks / checks (pull_request) Successful in 9m9s
PLAN.md §13 phase 4: /features/, /architecture/, /modules/, /integrations/, and /community/ — plus the two scope items the phase table never assigned to anyone. Six decisions taken by the org lead before coding, recorded in PLAN.md §10 as D20-D25: - D20 /features/ is the homepage's list with a `detail` line, not a second list. One data file, two renderings, so they cannot disagree about what exists. - D21 /architecture/ draws reasons, not reference: three new inline SVGs, one per boundary. No endpoint tables, no config keys — those are phase 8's and stay canonical in docs/. - D22 The deliberate absences of §2 become one tagged data file, rendered on the three pages that promise them. - D23 Phase 4 absorbs /community/ (specified in §10 and §14 N3, linked from the header since phase 1, built by no phase) and checkLinks.mjs. - D24 `needsModule`: writing the Teams detail exposed a false claim phase 3 shipped. Teams are module-sourced only — teams.module_id is NOT NULL, there is no create route, sync is gated on providerModuleId() — so the Community group no longer says a bare core does all of it. - D25 The per-capability demo affordance brand.json had promised since phase 2 is a deep link, filled at boot from data-demo-path. checkLinks.mjs reads the built HTML rather than src/, because half these links are assembled from data files and template literals. Its PLANNED_ROUTES list is checked in both directions, so it cannot rot into a permanent exemption. applyBrand.mjs gained a pass that recomputes deep links from their immutable path, making it idempotent and reversible; checkBrand.mjs lifts that pattern out and runs it against the stock markup so the two cannot drift. Both proved against a real mount, in both directions. Fixes a cascade bug the checks could not see: [data-demo-url=''] and a scoped component class are both specificity 0,1,0, so .demo-link's `display` beat the hide rule and twelve links to a nonexistent demo rendered, each resolving to the current page. The rule is now !important. The four diagrams' shared SVG vocabulary moved to src/styles/diagram.css. Verified from a clean checkout: npm ci, all five checks, astro check (0 errors), production build, and a live browser pass at desktop and 390px. Co-Authored-By: Claude <noreply@anthropic.com>
405 lines
13 KiB
Plaintext
405 lines
13 KiB
Plaintext
---
|
|
import Base from '../layouts/Base.astro';
|
|
import PageHeader from '../components/PageHeader.astro';
|
|
import NotBuilt from '../components/NotBuilt.astro';
|
|
|
|
import platform from '../data/platform.json';
|
|
import { capabilityGroup } from '../data/capabilities.mjs';
|
|
|
|
/**
|
|
* `/modules/` — PLAN.md §13 phase 4.
|
|
*
|
|
* §10 gives this page four jobs: what a module is, `module-uo` as the worked example,
|
|
* writing your own, and the Integration Kit with its draft badge (D8). They are in that
|
|
* order because they are increasing commitment — a reader deciding whether to install one,
|
|
* a reader wondering what they get, a reader considering building one.
|
|
*
|
|
* ---------------------------------------------------------------------------------------
|
|
* THE WORKED EXAMPLE READS ITS OWN CAPABILITIES
|
|
* ---------------------------------------------------------------------------------------
|
|
* The `module-uo` section lists what the module publishes, and it takes that list from
|
|
* `capabilities.mjs` rather than retyping it — the same list the homepage names and
|
|
* `/features/` expands, which is already checked against the module's own manifest through
|
|
* `platform.json` (§12). A third hand-maintained copy on this page is exactly the failure
|
|
* that machinery exists to prevent, and this is the page where it would be least visible.
|
|
*
|
|
* ---------------------------------------------------------------------------------------
|
|
* THE DRAFT CHIP IS A DECISION, NOT A DISCLAIMER
|
|
* ---------------------------------------------------------------------------------------
|
|
* D8 marks the Integration Kit draft until a second module is successfully built against
|
|
* it by somebody outside this project, and requires that status to carry its removal
|
|
* condition. Both are here: the chip, and the sentence that says what takes it down. The
|
|
* same absence appears in `notBuilt.mjs`, so a reader who scrolls past the chip meets it
|
|
* again in the list of things that do not exist.
|
|
*/
|
|
const title = 'Modules';
|
|
const description =
|
|
'What a module is, what the Ultima Online module publishes, and what it takes to write ' +
|
|
'one for another game.';
|
|
|
|
const gitea = `${platform.gitea.base}/${platform.gitea.org}`;
|
|
const docs = `${gitea}/docs/src/branch/main`;
|
|
|
|
const gameIntelligence = capabilityGroup('game-intelligence');
|
|
|
|
/** The three ways a module reaches a running deployment. None of them is a build. */
|
|
const installPaths = [
|
|
{
|
|
name: 'From the admin panel',
|
|
body:
|
|
'Paste the URL of a release manifest into Admin → Modules and press restart when it ' +
|
|
'asks. The site downloads the artifact, verifies the checksum the manifest declares, ' +
|
|
'inspects the whole archive before writing a single file, and unpacks it.',
|
|
fits: 'The click path, for a host you have no shell on.',
|
|
},
|
|
{
|
|
name: 'From your environment',
|
|
body:
|
|
'Name the module and its version in one environment variable and the container ' +
|
|
'resolves it at every start. Already at that version means no network call at all, so ' +
|
|
'a restart with the internet down comes up unchanged.',
|
|
fits: 'A compose-managed host, where the running set should be a line you version-control.',
|
|
},
|
|
{
|
|
name: 'By hand',
|
|
body:
|
|
'Unpack the tarball into the modules directory and restart. The bundle is already ' +
|
|
'assembled — the client half is prebuilt and its one runtime dependency ships inside.',
|
|
fits: 'Development, and any host where the other two do not fit.',
|
|
},
|
|
];
|
|
---
|
|
|
|
<Base title={title} description={description}>
|
|
<PageHeader eyebrow="The extension model" title="One platform, whichever game you run">
|
|
<p>
|
|
A module is the entire game-specific half of a deployment, packaged: its routes, its
|
|
screens, its database tables, its navigation rows and its slice of the API
|
|
documentation. The core site holds accounts, Teams, the wiki, posts, moderation and the
|
|
admin panel, and knows nothing about any game at all.
|
|
</p>
|
|
<p>
|
|
That division is not an aspiration bolted on afterwards. The Ultima Online support was
|
|
extracted out of the site into a module, and every URL it had before the move it still
|
|
has — which is the only version of this claim worth making.
|
|
</p>
|
|
</PageHeader>
|
|
|
|
<section class="page section mod" id="what">
|
|
<p class="eyebrow">What you get</p>
|
|
<h2>What installing one actually does</h2>
|
|
|
|
<ul class="mod__grid">
|
|
<li class="panel">
|
|
<h3>It brings its own everything</h3>
|
|
<p>
|
|
Server routes, React screens, tables, nav rows and an OpenAPI fragment the site
|
|
merges into its own spec. Nothing about it is a patch to the core site, so
|
|
upgrading either half does not involve reconciling the other.
|
|
</p>
|
|
</li>
|
|
<li class="panel">
|
|
<h3>You never build it</h3>
|
|
<p>
|
|
The client half ships prebuilt and the artifact is verified against a published
|
|
checksum before anything is written to disk. Production runs an image you pulled;
|
|
an operator who has to compile something has been handed a maintenance job.
|
|
</p>
|
|
</li>
|
|
<li class="panel">
|
|
<h3>It cannot take the site down</h3>
|
|
<p>
|
|
A module whose declared interface version does not match is marked failed and the
|
|
site starts without it — loudly, rather than half-loading. Disabling one closes its
|
|
connections and stops its routes answering.
|
|
</p>
|
|
</li>
|
|
<li class="panel">
|
|
<h3>Your data outlives it</h3>
|
|
<p>
|
|
Uninstalling removes the module and keeps its tables, so reinstalling picks up
|
|
exactly where it was. Destroying the data is a separate, opt-in choice made in its
|
|
own dialog, and it says what it is about to do.
|
|
</p>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
|
|
<section class="page section mod" id="installing">
|
|
<p class="eyebrow">Installing</p>
|
|
<h2>Three ways in, and none of them is a build</h2>
|
|
<p class="prose mod__lede">
|
|
Which one you use is a question about your host, not about the module. All three end
|
|
the same way: a restart, and the module's screens appear in the navigation.
|
|
</p>
|
|
|
|
<ol class="mod__paths">
|
|
{
|
|
installPaths.map((path) => (
|
|
<li class="panel">
|
|
<h3>{path.name}</h3>
|
|
<p>{path.body}</p>
|
|
<p class="mod__fits">{path.fits}</p>
|
|
</li>
|
|
))
|
|
}
|
|
</ol>
|
|
</section>
|
|
|
|
<section class="page section mod" id="module-uo">
|
|
<div class="mod__head">
|
|
<p class="eyebrow">The worked example</p>
|
|
<h2>module-uo</h2>
|
|
<span class="chip chip--version">{platform.releases['Module-uo']}</span>
|
|
</div>
|
|
|
|
<p class="prose mod__lede">
|
|
The Ultima Online module, and the reference every module that follows is measured
|
|
against. It is what turns a general-purpose community site into something that knows
|
|
what a shard is — and it is the proof that the seam described on
|
|
<a href="/architecture/">the architecture page</a> is real, because the code on the far
|
|
side of it was moved there rather than designed there.
|
|
</p>
|
|
|
|
<div class="mod__example">
|
|
<div class="panel mod__caps">
|
|
<h3>What it publishes</h3>
|
|
<ul>
|
|
{gameIntelligence.items.map((item) => <li>{item.label}</li>)}
|
|
</ul>
|
|
<p class="mod__caps-note">
|
|
The same list <a href="/features/">features</a> expands, read from one file that is
|
|
checked against the module's own manifest on every build.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="mod__facts">
|
|
<section>
|
|
<h3>It connects to a real server</h3>
|
|
<p>
|
|
The module talks to the sidecar beside your game server, not to the game. You
|
|
deploy that side with the installer and paste four values into the admin panel;
|
|
nothing here requires the game to exist, and with no server configured the site
|
|
renders normally and shows it offline.
|
|
</p>
|
|
</section>
|
|
<section>
|
|
<h3>It owns its own tables</h3>
|
|
<p>
|
|
Its schema is applied by the site on every boot and its data is its own. The
|
|
module declares which versions of the core interface it speaks — the site runs
|
|
{' '}{platform.moduleApi} — and refuses to load against one it does not.
|
|
</p>
|
|
</section>
|
|
<section>
|
|
<h3>It is a separate release</h3>
|
|
<p>
|
|
Versioned, tagged and published on its own cadence, independently of the site.
|
|
Upgrading one does not mean upgrading the other, as long as the declared interface
|
|
range still holds.
|
|
</p>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="page section mod" id="writing">
|
|
<div class="mod__head">
|
|
<p class="eyebrow">Writing your own</p>
|
|
<h2>The Integration Kit</h2>
|
|
<span class="chip chip--draft">Draft</span>
|
|
</div>
|
|
|
|
<p class="prose mod__lede">
|
|
A four-chapter book on putting a different game on this platform — the module, the
|
|
sidecar beside your game server, the plugin inside it — plus a template module that
|
|
continuous integration builds against a pinned version of the core site, so the
|
|
instructions cannot quietly stop working.
|
|
</p>
|
|
|
|
<div class="panel mod__draft">
|
|
<h3>Why it says draft</h3>
|
|
<p>
|
|
Because nobody outside this project has yet followed it to a working module, and that
|
|
is the only test of a set of instructions that counts. The badge comes off when
|
|
somebody does — that is the stated condition, not a mood, and it is written down so a
|
|
future reader knows when to take it down.
|
|
</p>
|
|
<p>
|
|
Everything it teaches is real and in use. What is untested is whether it is
|
|
<em>sufficient</em>: whether someone with no access to this project's context can get
|
|
from an empty repository to a running module using it alone.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="mod__links">
|
|
<a class="btn btn--primary" href={`${gitea}/Integration-kit`} rel="noopener noreferrer">
|
|
Read the Integration Kit
|
|
</a>
|
|
<a class="btn btn--ghost" href={`${docs}/website/MODULE_API.md`} rel="noopener noreferrer">
|
|
The module contract
|
|
</a>
|
|
<a class="btn btn--ghost" href={`${docs}/modules/uo/README.md`} rel="noopener noreferrer">
|
|
module-uo in depth
|
|
</a>
|
|
</div>
|
|
</section>
|
|
|
|
<NotBuilt scope="modules" title="What the module system does not do" />
|
|
</Base>
|
|
|
|
<style>
|
|
.mod h2 {
|
|
margin: 0 0 0.75rem;
|
|
font-size: clamp(1.6rem, 3.2vw, 2.1rem);
|
|
}
|
|
|
|
.mod__head {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: baseline;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.mod__head .eyebrow {
|
|
flex-basis: 100%;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.mod__head h2 {
|
|
margin: 0;
|
|
}
|
|
|
|
.mod__lede {
|
|
margin: 0.85rem 0 0;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.mod__grid,
|
|
.mod__paths {
|
|
display: grid;
|
|
gap: 1rem;
|
|
margin: 2rem 0 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr));
|
|
}
|
|
|
|
.mod__grid h3,
|
|
.mod__paths h3,
|
|
.mod__caps h3,
|
|
.mod__facts h3,
|
|
.mod__draft h3 {
|
|
margin: 0 0 0.5rem;
|
|
color: var(--gold);
|
|
font-size: 1.02rem;
|
|
}
|
|
|
|
.mod__grid p,
|
|
.mod__paths p {
|
|
margin: 0;
|
|
color: var(--muted);
|
|
font-size: 0.94rem;
|
|
}
|
|
|
|
.mod__paths li {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* Which host each path suits, set apart from what it does — a reader is choosing
|
|
between three, so the distinguishing line should not be buried in the paragraph. */
|
|
.mod__fits {
|
|
margin-top: auto;
|
|
padding-top: 0.85rem;
|
|
color: var(--dim);
|
|
font-size: 0.88rem;
|
|
font-style: italic;
|
|
}
|
|
|
|
.mod__example {
|
|
display: grid;
|
|
gap: 1rem;
|
|
margin-top: 2rem;
|
|
grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
|
|
align-items: start;
|
|
}
|
|
|
|
.mod__caps ul {
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
font-size: 0.94rem;
|
|
}
|
|
|
|
.mod__caps li {
|
|
position: relative;
|
|
padding-left: 1.1rem;
|
|
color: var(--text);
|
|
}
|
|
|
|
.mod__caps li + li {
|
|
margin-top: 0.3rem;
|
|
}
|
|
|
|
/* The same drawn marker the homepage's capability lists use, so a reader who has
|
|
seen this list once recognises it as the same list. */
|
|
.mod__caps li::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0.62em;
|
|
width: 5px;
|
|
height: 5px;
|
|
border-radius: var(--radius-pill);
|
|
background: var(--portal);
|
|
opacity: 0.75;
|
|
}
|
|
|
|
.mod__caps-note {
|
|
margin: 1rem 0 0;
|
|
padding-top: 0.85rem;
|
|
border-top: 1px solid var(--line-soft);
|
|
color: var(--dim);
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.mod__facts section + section {
|
|
margin-top: 1.4rem;
|
|
}
|
|
|
|
.mod__facts p {
|
|
margin: 0;
|
|
max-width: var(--measure);
|
|
color: var(--muted);
|
|
}
|
|
|
|
.mod__draft {
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.mod__draft p {
|
|
margin: 0;
|
|
max-width: var(--measure);
|
|
color: var(--muted);
|
|
font-size: 0.94rem;
|
|
}
|
|
|
|
.mod__draft p + p {
|
|
margin-top: 0.85rem;
|
|
}
|
|
|
|
.mod__links {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.7rem;
|
|
margin-top: 1.75rem;
|
|
}
|
|
|
|
@media (max-width: 860px) {
|
|
.mod__example {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
}
|
|
</style>
|