Files
runicgateway.com/src/components/home/DataPath.astro
wtclaude 2d19ee4220
All checks were successful
PR checks / checks (pull_request) Successful in 9m9s
feat(marketing): phase 4 — the marketing pages
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>
2026-08-24 01:53:11 -05:00

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>