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>
236 lines
8.5 KiB
Plaintext
236 lines
8.5 KiB
Plaintext
---
|
|
import platform from '../../data/platform.json';
|
|
|
|
/**
|
|
* The data path (PLAN.md §13 phase 3), drawn as inline SVG per §11's motif rule — hand-drawn
|
|
* geometry, used where it explains something, and no raster anywhere.
|
|
*
|
|
* ---------------------------------------------------------------------------------------
|
|
* THE LABELS ARE GENERIC, WITH UO AS THE CAPTION
|
|
* ---------------------------------------------------------------------------------------
|
|
* The org lead settled this before the diagram was drawn. The nodes say "your game server"
|
|
* and "sidecar", not "ServUO shard" and "uo-link", because §10's rule is that a reader
|
|
* should never need to know that `link`, `servuo-plugins` and `installer` are three
|
|
* repositories in order to connect a game server — and because the tagline promises a
|
|
* platform, not a UO product.
|
|
*
|
|
* It does NOT hide what actually ships. The sub-labels and the caption name ServUO and
|
|
* uo-link outright, because §1 says the technical truth wins and today there is exactly one
|
|
* implementation of this shape. An operator running a shard has to see themselves in the
|
|
* picture on the first screen.
|
|
*
|
|
* ---------------------------------------------------------------------------------------
|
|
* WHY THE SVG IS aria-hidden
|
|
* ---------------------------------------------------------------------------------------
|
|
* Not because it is decorative — it is the opposite — but because the steps beside it carry
|
|
* the same four stages in full prose, at real font sizes, in reading order. A `role="img"`
|
|
* with a `<desc>` would make a screen reader read the same path twice, and the second
|
|
* telling would be the worse one. The picture is for people who can see it; the list is the
|
|
* canonical version and everyone gets it.
|
|
*
|
|
* That also means the diagram must never gain a fact the list does not have.
|
|
*
|
|
* The concentric rings behind the nodes are the emblem's own geometry, centred on the
|
|
* boundary line — the one place in the picture where the argument actually happens.
|
|
*/
|
|
---
|
|
|
|
<section class="page section datapath">
|
|
<div class="datapath__head">
|
|
<p class="eyebrow">How it works</p>
|
|
<h2>One path, one direction</h2>
|
|
<p class="prose">
|
|
Everything the website knows about your game arrives the same way. There is no second
|
|
route in, and nothing on the internet can reach the game to ask.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="datapath__body">
|
|
<div class="datapath__figure">
|
|
<svg viewBox="0 0 380 500" class="flow" aria-hidden="true" focusable="false">
|
|
<!-- The emblem's concentric rings, centred on the boundary. Drawn first so the
|
|
panels sit over them. -->
|
|
<g class="rings">
|
|
<circle cx="190" cy="252" r="112" />
|
|
<circle cx="190" cy="252" r="158" />
|
|
<circle cx="190" cy="252" r="204" />
|
|
</g>
|
|
|
|
<!-- Loopback hop: same host, no network involved. -->
|
|
<path class="spine" d="M190 92 V140" />
|
|
<path class="arrow" d="M190 148 l-6 -10 h12 Z" />
|
|
|
|
<!-- The network hop, and the only one. Drawn in the portal colour because this is
|
|
the live feed, and the live signal is cyan everywhere on the site. -->
|
|
<path class="spine spine--live" d="M190 224 V272" />
|
|
<path class="arrow arrow--live" d="M190 280 l-6 -10 h12 Z" />
|
|
|
|
<path class="spine" d="M190 356 V404" />
|
|
<path class="arrow" d="M190 412 l-6 -10 h12 Z" />
|
|
|
|
<!-- The boundary the whole design exists to draw. -->
|
|
<path class="boundary" d="M8 252 H372" />
|
|
<text class="boundary-label" x="372" y="245" text-anchor="end">the network</text>
|
|
|
|
<rect class="node" x="20" y="16" width="340" height="76" rx="12" />
|
|
<text class="node-title" x="42" y="50">Your game server</text>
|
|
<text class="node-sub" x="42" y="72">ServUO today · opens no inbound port</text>
|
|
|
|
<rect class="node" x="20" y="148" width="340" height="76" rx="12" />
|
|
<text class="node-title" x="42" y="182">Sidecar</text>
|
|
<text class="node-sub" x="42" y="204">uo-link · the only network-facing part</text>
|
|
|
|
<rect class="node node--self" x="20" y="280" width="340" height="76" rx="12" />
|
|
<text class="node-title" x="42" y="314">Runic Gateway</text>
|
|
<text class="node-sub" x="42" y="336">your public website</text>
|
|
|
|
<rect class="node" x="20" y="412" width="340" height="76" rx="12" />
|
|
<text class="node-title" x="42" y="446">Browser and app</text>
|
|
<text class="node-sub" x="42" y="468">anyone you choose to let in</text>
|
|
</svg>
|
|
|
|
<p class="datapath__caption">
|
|
Today that game server is a ServUO shard and that sidecar is uo-link. The shape is the
|
|
contract; the implementations are what plug into it.
|
|
</p>
|
|
</div>
|
|
|
|
<ol class="datapath__steps">
|
|
<li>
|
|
<h3>Your game server</h3>
|
|
<p>
|
|
A plugin inside the server dials <strong>out</strong> to the sidecar over loopback.
|
|
The game never listens for anything, so there is nothing on it to find. Events go
|
|
onto a bounded queue and the game moves on — a sidecar that is wedged or missing
|
|
cannot slow the world down.
|
|
</p>
|
|
</li>
|
|
<li>
|
|
<h3>The sidecar</h3>
|
|
<p>
|
|
A small service beside the game, and the only piece of the bridge anything else can
|
|
reach. It speaks a versioned wire protocol — protocol {platform.protocol} today — so
|
|
a mismatched pair is refused rather than misread, and it answers only your website's
|
|
backend, over an authenticated WebSocket and REST.
|
|
</p>
|
|
</li>
|
|
<li>
|
|
<h3>Runic Gateway</h3>
|
|
<p>
|
|
Your site ingests the live feed and fans it back out on two streams: a public one
|
|
carrying an allowlist of safe events, and a staff-only one carrying the rest. That
|
|
split is a security boundary, not a preference. When the game is down the site stays
|
|
up and shows it as offline.
|
|
</p>
|
|
</li>
|
|
<li>
|
|
<h3>Browser and app</h3>
|
|
<p>
|
|
The web client reads same-origin JSON and server-sent events. The Android app talks
|
|
to the same documented API with bearer tokens. Neither has any idea where the game
|
|
server is, because neither is ever told.
|
|
</p>
|
|
</li>
|
|
</ol>
|
|
</div>
|
|
</section>
|
|
|
|
<style>
|
|
.datapath__head h2 {
|
|
margin: 0 0 0.75rem;
|
|
font-size: clamp(1.6rem, 3.2vw, 2.1rem);
|
|
}
|
|
|
|
.datapath__head .prose {
|
|
margin: 0;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.datapath__body {
|
|
display: grid;
|
|
gap: clamp(1.75rem, 4vw, 3rem);
|
|
margin-top: 2.5rem;
|
|
grid-template-columns: minmax(0, 380px) minmax(0, 1fr);
|
|
align-items: start;
|
|
}
|
|
|
|
.datapath__figure {
|
|
position: sticky;
|
|
top: calc(var(--header-h) + 1.5rem);
|
|
}
|
|
|
|
.datapath__caption {
|
|
margin: 1rem 0 0;
|
|
max-width: 380px;
|
|
color: var(--dim);
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
/* The SVG vocabulary this diagram draws with -- .node, .spine, .arrow,
|
|
.boundary, .rings -- now lives in src/styles/diagram.css, shared with
|
|
/architecture/'s three. It was duplicated in four files the moment the
|
|
second diagram existed, and the rules it holds are decisions about what a
|
|
diagram on this site looks like rather than about this one.
|
|
|
|
The layout below stays here: the right-hand column is a numbered walk,
|
|
not the notes column .diagram__body assumes. */
|
|
|
|
/* ---- The list ---------------------------------------------------------- */
|
|
.datapath__steps {
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
counter-reset: step;
|
|
}
|
|
|
|
.datapath__steps li {
|
|
position: relative;
|
|
padding-left: 3.25rem;
|
|
counter-increment: step;
|
|
}
|
|
|
|
.datapath__steps li + li {
|
|
margin-top: 1.75rem;
|
|
}
|
|
|
|
.datapath__steps li::before {
|
|
content: counter(step);
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
display: grid;
|
|
place-items: center;
|
|
width: 2.25rem;
|
|
height: 2.25rem;
|
|
border: 1px solid var(--gold-deep);
|
|
border-radius: var(--radius-pill);
|
|
color: var(--gold);
|
|
font-family: var(--display);
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.datapath__steps h3 {
|
|
margin: 0.3rem 0 0.4rem;
|
|
font-size: 1.08rem;
|
|
}
|
|
|
|
.datapath__steps p {
|
|
margin: 0;
|
|
max-width: var(--measure);
|
|
color: var(--muted);
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.datapath__body {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
|
|
/* Sticky is a wide-screen affordance: the figure should scroll away with
|
|
everything else once it is above the list rather than beside it. */
|
|
.datapath__figure {
|
|
position: static;
|
|
justify-self: center;
|
|
}
|
|
}
|
|
</style>
|