73f664c38e4059d0dcc57b722c2ee49b7ffe79fc
27 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| e91e76bfa9 |
docs(site): the Event System — the platform's facts, and two pages for it
All checks were successful
PR checks / checks (pull_request) Successful in 9m34s
Phase 16c of the events plan: `runicgateway.com`'s half of the workstream, now that `main` carries the engine, the module, the app and the bundle. The checks were already red and named their own answers: * `checkFacts` — nine values had moved. Protocol 5 → 7 in all three declaration sites, `moduleApi` 1.9.0 → 1.10.0, the bundle to 2026.09.10 with sidecar v2.2.0 and overlay v1.2.0, `link` v2.2.0, `Module-uo` v1.2.2. * `checkReference` — twenty-seven `Bridge.cfg` keys the site listed nowhere: the events switch and its sweep, the ten caps, the oracle NPC, the two lease keys and the seven participation keys. They are five new groups rather than an appendix to an existing one, because `EventsEnabled` is a second consent switch and belongs beside its own ceilings. Two pages, matching the treatment Teams has: * **Scheduled events** (Administration) — where it is and who sees it, authoring and immutable versions, the switchboard that arrives off, caps as a condition on an `UPDATE` rather than a role check, the dry run, the run console, what an event owns versus what it borrows, generated cleanup, the shard's own switches, and what a player sees. * **Events architecture** — the two sentences it turns on, what is a table and what deliberately is not, budgets in SQL, the ledger's two rules, at-most-once on a wire that can lose an answer, the three layers, and the four omissions. And the rest of the surface: * `/privacy` gains **`deploy-events`** — the participation ledger is personal data and no row named it. Scores and ranks against a module-opaque member key, linked to an account where one is linked; the diagnostic log swept after 90 days on terminal runs only; the run, its steps and its participants not swept at all, because they are the record of what was done to a shared world. `deploy-game-data`'s citation moves from `link/v4.md` to `v7.md`. * **Protocol versions** — the most recent bump touched *five* repositories, and the `website` row is the interesting one: core is normally out of a protocol bump's reach and this one reached it, because what changed was not a game noun but the shape of a thing core owns the ledger for. The store-migration paragraph now says four bumps' worth rather than two. * Two capability entries, so `/`, `/features/` and `/modules/` stop omitting the subsystem — an Administration item, and an **Event calendar** under Community with `/site/events` as its deep link. Deliberately *not* `needsModule`: a bare core can author and run an event, and only the world verbs need a module. * **`reference/event-catalog` is retitled "Shard event catalog"** and says what it is not. Two things in the docs were called an event catalog; the route is unchanged, so nothing outside this repository breaks. * `canonicalDocs` gains `website/EVENTS.md` and moves `link/v4.md` → `v7.md`. No screenshots. Capturing the events surfaces means standing the whole rig back up — game server, sidecar, core, module, a published event with a live run — for two or three images that no check requires, and the engagement workstream's own site leg added none either. `npm run verify` green end to end, including `checkReference` against the protocol spec that only reached `docs` `main` in RunicGateway/docs#232 — the seventh cutover step, which 16b had left on `edge`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4 |
|||
| 82b55e3084 |
docs(site): /privacy tells the truth about engagement retention again
All checks were successful
PR checks / checks (pull_request) Successful in 1m23s
ENGAGEMENT.md Phase 14. The deploy-engagement entry in the collection inventory said "Kept until the operator removes them; nothing here expires on its own" and, in its detail, "the delivery log, the suppression list and the per-person rate limits have no retention sweep, so they are as long as the site is old". That was the true answer until Phase 14 landed a sweep, and it is the kind of sentence a Play reviewer reads. It now describes what the code does: a nightly sweep with an operator-settable horizon per table (180 days for the delivery log, 30 for the queue and the rate limits), and the two things that deliberately do NOT expire — an item still waiting to be sent, because it is a message the site still intends to deliver, and the suppression list, because ageing an entry out would mean mailing an address that already bounced. PLAY_DATA_SAFETY.md is unchanged and check:datasafety stays green: deploy-engagement is deployment-scoped, and the Play answers are generated from the app-scoped entries, because Play asks what the app collects rather than what a self-hosted deployment keeps. All ten checks green, 42 tests pass. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 3574bba4d5 |
chore(facts): the cutover values -- bundle 2026.09.01, sidecar v2.1.0, overlay v1.1.0
Phase 13 landed while this branch waited: link/servuo-plugins/website/Module-uo are all on main, the bundle was republished at 2026.09.01, and Module-uo cut v1.1.0. These are the four values PLAN.md 12 said could not be written until the republish existed, plus the two releases that moved with it. Every one is the value checkFacts.mjs itself reports as the authority's answer: bundle.tag 2026.08.19 -> 2026.09.01 bundle.sidecar v2.0.0 -> v2.1.0 bundle.overlay v1.0.0 -> v1.1.0 releases.link v2.0.0 -> v2.1.0 releases.Module-uo v1.0.2 -> v1.1.0 verifiedOn 2026-08-19 -> 2026-09-01 No page hardcodes any of them -- every quote is an interpolation of platform.bundle.* or platform.releases.*, so there is nothing else to re-read. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| d01fd55f43 | Merge branch 'edge' into docs/platform-facts-protocol-5 | |||
| 391a4a131b |
chore(facts): protocol 5 and Module API 1.9.0 -- HOLD until the cutover
Engagement Phase 12b. **This branch is deliberately red and must not be merged until the
Phase 13 cutover has landed link and servuo-plugins on `main` and CI has republished the
bundle.** checkFacts.mjs fetches every value from the source repo's `main`, so it fails
today exactly as designed:
FAIL protocol (sidecar) platform.json 5 link main 4
FAIL protocol (overlay) platform.json 5 servuo-plugins main 4
FAIL moduleApi platform.json 1.9.0 website main 1.6.0
FAIL protocol (bundle) platform.json 5 installer bundles:current.json 4
Every other check is green on this branch: checkLinks 2503, checkReference 22, checkA11y,
checkCsp, checkSidebar, 42 tests.
moduleApi is **1.9.0**, not the 1.7.0 the plan named -- Phase 11 moved it twice after that
sentence was written (1.8.0 for the `admin` ceiling, 1.9.0 for the module seed API).
## Also: the one page whose whole subject is the protocol number was the one page that
## hardcoded it
platform.json's own header says no version number is ever hardcoded in prose, and
`architecture/protocol-versions.mdx` had `4` written out three times -- the headline
sentence and both declaration-site rows. It now imports platform.json like every other
page that quotes a fact, so it moves with the JSON and cannot say 4 while the JSON says 5.
Note the interpolation is OUTSIDE the code spans: MDX does not evaluate an expression
inside backticks, so `PROTOCOL_VERSION: u32 = {platform.protocol}` would have rendered the
braces literally. Verified in the built HTML -- "currently 5", and no `platform.protocol`
survives anywhere in the output.
The same page's "what a bump obliges" section said version 4 was the first bump to need a
store migration, which read as though every bump does. v5 needed none (it only widens
frames the store already keeps whole -- docs link/v5.md), so the sentence now says which
did and which did not, and v5.md joins v4.md under canonical documents.
## The fill-in step, at merge time
Three values are not knowable today because the artefact does not exist yet. After the
bundle republishes, run:
GITEA_TOKEN=<token> node scripts/checkFacts.mjs
and copy what it prints in the "says" column into src/data/platform.json:
* bundle.tag, bundle.sidecar, bundle.overlay -- from installer bundles:current.json
* releases.Module-uo -- if the cutover cuts a new module release
* verifiedOn -- the date you ran it
Then re-read `getting-started/connect-a-game-server` and `administration/the-shard-connection`,
which quote the bundle, before merging. The check is green when all 19 agree.
AI-assisted: written with Claude Code.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| c8a293b8f6 |
docs(admin): the engagement rules screen, and the privacy inventory an engagement mailer changes
Engagement Phase 12a. The site had pages for where a message goes (Notifications and
email) and what it says (Message templates), and nothing at all for what makes one get
sent -- the four Engagement screens the workstream built.
New page: Engagement rules. Rules, Audiences, the trigger catalog and the send log on
one page, sitting between the two it joins up. Templates already has its own page and
Suppressions is in Troubleshooting, so neither is repeated here.
Two things it exists to state plainly:
* Every rule ships disabled, including the ones a module brings. "Installed" is not
"on", and an upgrade whose Team mail went quiet is the same fact.
* The ceiling is a TREE, not a ladder. The tempting reading -- a staff-only event
could obviously also go to one person -- is wrong, and the example is the argument:
"one person" for cheat detection is the player it was detected on.
Troubleshooting gains the symptom that page answers ("nothing is sent for one
particular event"): the rule is off, the rule is dormant, its own cooldown held it, or
the audience is empty.
Privacy: two rows the engagement work makes necessary, and one sentence it made false.
* app-content claimed "Nothing is cached for offline use". Phase 8 shipped a DataStore
snapshot of the inbox, so it was untrue -- and that row feeds the generated Play Data
Safety answers, which is a store-review matter rather than a doc nit. The snapshot now
has its own row and its own Play mapping (Messages / Other in-app messages; not
collected by us, stored on the device), and app-content's claim is narrowed to
everything else.
* deploy-engagement, for the deployment scope: an address is now used for more than
getting into an account, there is a delivery log holding a one-way hash of it, and
there is a suppression list. Its retention line says what is true rather than what a
reader assumes -- none of these tables has a retention sweep.
PLAY_DATA_SAFETY.md regenerated from the inventory; legal.lastUpdated moved with the page
it dates.
Verified: the whole `verify` chain green -- checkSidebar (plannedSidebar moved with the
live tree), checkFacts 19/19, checkQuickstart 59, checkReference 22, checkLinks 2605,
checkA11y, checkCsp, playDataSafety --check, 42 + 7 tests. Read in a browser as well, in
the served build.
AI-assisted: written with Claude Code.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| 7709b055a4 |
docs(troubleshooting): suppression and bounces (engagement Phase 9)
ENGAGEMENT.md §6.0b assigns this repo the operator-facing half of Phase 9 (website#176 + docs#191). Two new sections, split along the line that actually matters when somebody reports it. "One person stopped receiving email" is the suppression case, and the three things an operator gets wrong about it: they can still reset their password (suppression scopes to engagement rules only, so that is the expected shape of the problem rather than a contradiction); Not sent, Bounced and Failed in the Send Log mean three different things and only one of them is about your configuration; and no row at all means they were excluded before anything was queued, by an opt-in or by the verification gate. "Everyone stopped receiving email at once" exists to stop the wrong reflex. A whole-deployment stop is never the suppression list — a wrong password never suppresses anybody, only the receiving server naming a specific dead mailbox does — and it says so before an operator starts clearing rows. Also notes that lifting a suppression asks for the full address because addresses are stored one way, so it reads as the privacy design rather than a missing feature. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 283814dbf2 |
docs(admin): on-site notifications, and the defaults page that was wrong
Engagement Phase 7 gives the platform a third notification channel — an inbox on the site itself — so `notifications-and-email.mdx` gains a section for it: what it is, that it is the one channel on by default, that its body is always plain text, and that the nightly prune takes read items only. Two corrections in the same file, both of which were already false before this phase and would have become misleading with it: - "Who receives what" said push was opt-OUT. Push stream subscriptions have always been opt-IN, and engagement Phase 3 made that explicit in the channel registry. Rewritten as three defaults plus the Team mute that overrides all three, and pointed at the preferences grid the same phase gave the web. - `capabilities.mjs` claimed "Web, push and email … push arrives by default". The web channel did not exist until now and push has never arrived by default. Reworded to name the on-site inbox as the opt-out one. The push section now says what a tickle raised by an engagement rule carries, and that it is still only a pointer. Code: RunicGateway/website#TBD · Docs: RunicGateway/docs#TBD Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| e0d491d33b |
docs(admin): Team notification emails are engagement rules now
The Teams page gains the section engagement Phase 6 owes it. The operator-facing fact is the one that has to land first: Team emails used to send with no configuration and now arrive switched off, so nobody gets them until a rule is turned on in Admin → Engagement → Rules. Also says what each of the four seeded rules sends and why the two roster ones ship off, that a rule decides whether the site sends at all while members still choose per Team, why a digest is re-read at send time, and that unsubscribe links in mail already sent still work — now stopping the emails they came with without also silencing that Team's push. Site: RunicGateway/website#TBD · Docs: RunicGateway/docs#TBD Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 289b6b3a8d |
docs(admin): a page for the message-template editor
The operator half of engagement Phase 5b, which 6.0b of ENGAGEMENT.md assigns to this repo: what the Templates screen is for, how a shipped default is edited in place without an upgrade taking the edit back, why variables are clicked rather than typed, the two halves of every message, draft vs published, the preview and its dark-mode approximation, test sends, duplicating to make a new template, and the send log. Written for someone running a site, not someone reading the design document: it explains what to do and why the refusals exist, and names no version numbers - the facts check reads authority from each repo's `main`, and none of this is there yet. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 2da014790e |
docs(admin): email is SMTP now, and the Gmail connect flow is gone
Engagement Phase 1's share of this repo (docs/website/ENGAGEMENT.md §6.0b). Four pages described a delivery path that no longer exists — one of them under the heading "There is no SMTP option", which is now the opposite of true. notifications-and-email.mdx: the Email section is rewritten around the three postures the org lead settled on (§7.1 Q5), leading with a relay and naming smtp.gmail.com:587 with an app password as the migration off OAuth2. Two cautions carry the failures that produce no error at all — Implicit TLS left on for port 587, which hangs, and an operator-typed sender the relay will not accept, which is an SPF/DMARC rejection that looks like nothing. Send test is what proves both. troubleshooting.mdx gains those two, plus the enabled toggle, which now gates every message rather than some of them. configuration.mdx loses the "set up Google first" ordering constraint, which is gone with the borrowed client. system-architecture.mdx's encrypted-at-rest list is corrected: the Gmail refresh token is replaced by the transport credentials, which are write-only like the sidecar token. This lands on `edge`, so nothing here is published while `main` still carries the Gmail flow. platform.json and capabilities.mjs are untouched — they belong to Phase 12. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 12c416f5fc |
fix(footer): send each documentation link to the section its label names
All checks were successful
PR checks / checks (pull_request) Successful in 9m38s
All three links under "Documentation" pointed at /docs/. Three labels — Getting started, Administration, Building a module — and one destination, which is the docs home and also what the header's Docs link already opens. They now land inside the section they name: Getting started /docs/getting-started/requirements/ Administration /docs/administration/configuration/ Building a module /docs/modules/building-a-module/ The docs home stays the header's link rather than becoming a fourth route to the same page. Eleven checks and two suites could not see this, and the reason is worth keeping: the bug is not a broken link. checkLinks resolves every internal href against the build and /docs/ resolves — three links to a page that exists are three valid links. checkSidebar compares the docs tree to the planned tree and never looks at the footer. checkA11y checks structure, and three correctly marked-up anchors are correct markup. Nothing asserted that a link goes where its label says. So the columns move to src/data/footer.mjs, beside legal.mjs and collection.mjs, and test/footer.test.mjs asserts it: every destination in a column distinct, no destination repeated across columns, each documentation link inside its own section prefix, none of them the docs home, and the two Project links still read from the brand. A column list inside an .astro component cannot be imported by a test, which is the whole reason for the move. Verified by reintroducing the bug: the suite fails with "Administration points at /docs/, which is not inside /docs/administration/". Restored, npm run verify is green — 42 tests, eleven checks, and the built index.html renders three distinct hrefs. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| de9d25bbe7 |
feat(validation): phase 11 — the walk that found what the checks could not
All checks were successful
PR checks / checks (pull_request) Successful in 1m26s
The checks were green before this phase started and are green now. What found anything was the part no script does: fifty pages at three widths in a real browser, a signup walked against its store, and a full brand mount applied and restarted. D51 — the chrome and the head follow the mount; the consent sentence does not. With a complete brand.json mounted, forty-nine pages came back rebranded and /beta did not. applyBrand.mjs rewrites files in dist/client and /beta renders per request, so its HTML never exists as a file to rewrite; liveBrand() was there for exactly that and was used for betaOptInUrl alone. Everything around the form — title, OG tags, header lockup, footer Source and Discord links — came from the shared chrome, and the shared chrome was baked. renderBrand() picks by Astro.isPrerendered, in one place, so the other forty-nine keep taking the value the boot rewrite will replace. CONSENT_TEXT stays a constant: it is stored verbatim in a person's row, so following a mounted name would change the recorded text of a consent already given. D52 — the documentation half gets phase 10's skip-link fix. Starlight's skip link targets the page <h1>, which is no more focusable than the <main> phase 10 fixed, so following it moved the viewport and not the focus on forty pages. A PageTitle override adds tabindex="-1". D53 — no twelfth check. The external-link sweep (73 of 74 alive) and the brand-mount walk stay throwaway scripts: one would make the build depend on other people's uptime and the other needs Chrome on the runner. Also recorded and deliberately not fixed: Starlight's heading anchor links measure under 24px at 390, and are exempt under SC 2.5.8's Equivalent clause because the mobile table of contents links to every one of the same anchors. npm run verify green — fourteen steps, both suites, all eleven checks. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| e71ff4acd4 |
feat(polish): phase 10 — search, accessibility, SEO and a real CSP
All checks were successful
PR checks / checks (pull_request) Successful in 9m36s
PLAN.md §13 phase 10, with four decisions of record — D47-D50, taking the count
to fifty. Three were straightforward; the CSP turned into the phase's real work,
because the thing meant to be a configuration flag was broken in a dependency and
broken silently.
D47 — search reaches the marketing pages, and the header gets a box.
Base.astro marks its <main> as a Pagefind body, so all ten join the index the
docs already query, and Search.astro opens it in a <dialog>. Nothing is fetched
until the dialog is opened (the bundle is 120 kB and these pages otherwise ship
almost no JavaScript). Pagefind titles a result from the first <h1>, and these
pages have editorial ones — "The app for a deployment you already use" — so the
index is given the page's short name instead. applyBrand.mjs now re-indexes after
a rewrite, closing a note phase 2 left for this phase.
D48 — the CSP is a real response header, sent by the container. Not a <meta>,
which ignores frame-ancestors, and not advice for someone's reverse proxy, which
puts the strictest promise in §6 outside what this repo tests. Three things
fought it, all the same shape — correct build, broken page, no error:
* Astro does not hash <script is:inline>, and Starlight ships six per docs
page, so the first build with CSP on had a strict header and a dead theme
switcher. The hashes are now generated into src/config/cspHashes.mjs and
checkCsp.mjs verifies every inline block against its own page's policy.
* Expressive Code writes ~3,700 inline style ATTRIBUTES, which cannot be
hashed, hence style-src-attr 'unsafe-inline' — scoped to that directive, so
script-src is untouched.
* @astrojs/node matched a request to a policy with pathname.includes(), a
substring test: /modules/ was served /docs/modules/building-a-module's
policy and rendered with its own stylesheet refused. scripts/serve.mjs keeps
the same _headers.json and matches by equality; test/headers.test.mjs starts
the server and reads the responses, because nothing that reads dist/ can see
this.
D49 — robots.txt allows everything and names the sitemap (there was no way to
find it: no robots.txt, and D9 rules out a search console). D50 — Organization
and SoftwareApplication, no ratings and no docs-wide Article markup.
checkA11y.mjs is the eleventh check: seven structural rules over all fifty pages,
verified by breaking each in turn. The walk at 390/768/1280 found no overflow
anywhere, the CSP violations above, a 17x17 consent checkbox (WCAG 2.2 SC 2.5.8
wants 24), and a skip link that moved the scroll but not the focus.
npm run verify is green: fourteen steps, both test suites, all eleven checks.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| 8b6efd5c0a |
feat(screens): retake the two frames a signed-in character changes
All checks were successful
PR checks / checks (pull_request) Successful in 1m13s
The org lead signed a character into the shard by hand — the step automation could not reach — so shard-status and app-shard were retaken. The shard page now reads 1 player online in Britain, and the app's card agrees. Two things worth noticing in the retake. Presence reaches the public page as counts and regions rather than names, which is the visibility framework working unprompted. And the guild board's online column did not move: it refreshes only when a guild's signature changes, which is the stale-roster defect wearing a different hat. A sixth defect, visible in the shipped phone capture: the app interpolates a count into a fixed plural and says "1 players online". Raised, not fixed — it wants a plurals resource in the app. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| c29ec94f46 |
feat(screens): phase 9 — real screenshots, from a real shard
All checks were successful
PR checks / checks (pull_request) Successful in 1m25s
D4 asked for screenshots of the review stack rather than placeholders. Seventeen
of them: eleven of the site in a browser, six of the app on a phone, all from one
demo deployment wired to a running ServUO shard over a real sidecar, captured on
one day (D42).
The deployment is branded "Runic Gateway Demo" rather than a real community (D43),
and the captures sit beside the claims they support — the homepage, /features/, and
five of the administration pages phase 7 could describe but not show (D44).
The rig is committed rather than remembered (D45):
scripts/seedDemo.mjs content, by driving the site's own API — never SQL,
because a row the product could not have produced is
a screenshot of a product that does not exist
src/data/screens.mjs every capture: route, viewport, scroll, alt, caption
scripts/captureScreens.mjs npm run screens:capture
scripts/checkScreens.mjs the ninth check script, in CI
Shard-side dressing is servuo-plugins' scaffolding (D46), never deployed.
The rig found five things nothing else had. One is fixed upstream — a fresh
module-uo install pinned wire protocol 3 against a sidecar speaking 4, released as
v1.0.2, which this repo's own facts check then caught in platform.json. Four are
raised as product observations and worked around in the rig: a renamed guild
member never reaches the site, a guild deleted while the shard is down is a ghost
row forever, "Houses in danger" cannot show a house that was already collapsing,
and the app's news list prints raw ISO timestamps.
Players online reads 0. Logging a character in needs a UO client driven by hand,
and that is where this stopped — PLAN.md §10 says exactly why, and how to retake
the two frames that would change.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| d89ce06bb8 |
docs(builder): phase 8 — modules, architecture and reference
All checks were successful
PR checks / checks (pull_request) Successful in 1m13s
Twenty pages completing the tree section 10 planned: Modules (8), Architecture
(5) and Reference (7). Four decisions, D38-D41, recorded in PLAN.md section 10.
D39 is the one that shaped the phase. Section 1 forbids re-specifying a
contract, and a Reference section is exactly where that rule is most tempting to
break, so the line is drawn at names: every environment variable, config key,
installer command, visibility rung and canonical document is listed with one
terse line saying what it is FOR, while shapes, semantics and every "why" stay
in the canonical document.
That is only safe because the names are checked. checkReference.mjs compares six
enumerations against the repositories that own them, over the Gitea API, as set
comparisons in BOTH directions -- and the second direction is the one that earns
its keep, because a reference page does not usually rot by describing something
that vanished, it rots by quietly not mentioning what was added since.
The check went green on its first run, which is the least trustworthy possible
outcome, so it was verified by breaking it: seven mutations, all caught. The one
worth keeping is the visibility ladder REORDERED with its membership unchanged
-- it is a security boundary, and a set comparison alone would have passed it.
D41 turns plannedSidebar from a checklist into a checked invariant, and finding
out why was the phase's first defect: it had already drifted, because phase 7
added the Content page under D37 and never updated the list. Nothing failed,
because nothing read it. checkSidebar.mjs now asserts the two trees agree on
groups, labels and order -- order because the order of Getting started IS the
installation path.
Two more things the writing found. PLAN.md's page count was wrong and had been
since section 10 was written ("roughly 38, 37 planned" for a tree of forty).
And module.json's `mounts` and the SPA's paths are different mechanisms that no
single document stated plainly -- module-uo declares admin: ["/shard",
"/uo-link"] while its screen lives at /admin/uo/link, because API routes are
deliberately NOT namespaced while SPA routes are. That is precisely the
distinction the installer got wrong in v0.1.0, and it now has a named home.
D40: the docs link to /architecture/'s drawn diagrams rather than importing
them. Those components carry marketing chrome and depend on diagram.css, which
Starlight does not load; the docs use text diagrams, which paste into an issue.
npm run verify green: 40 pages across 5 groups agree with plannedSidebar, 2390
internal links resolve, 123 repository links point at a branch, 19 facts, 59
quickstart checks, 22 reference enumerations, astro check 0 errors, 36 tests.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| e8cb6061fe |
fix(facts): installer v0.1.1 is released, so the note names a version
All checks were successful
PR checks / checks (pull_request) Successful in 9m22s
Recovering installer's orphan tag published v0.1.1, which moved the platform
under this branch and turned checkFacts red -- the check working exactly as it
should, since a version this site quotes had changed.
FAIL release installer
platform.json says : v0.1.0
installer releases/latest says : v0.1.1
The 500 that orphaned the tag was a race with the tag push one second earlier,
not a structural failure: re-running the workflow took the built-in orphan-tag
recovery path and published all four assets unchanged.
So the stale-path Aside stops saying "v0.1.0 is still the current download",
which is no longer true, and says the durable thing instead -- v0.1.0 prints
the old path, v0.1.1 prints the real one -- which stays correct however many
releases follow. platform.json and the PLAN.md version table move to v0.1.1,
and the phase 7 findings record the pipeline defect as a fourth finding.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| a993b872ac |
fix(docs): clear the quickstart drift the upstream fixes caused
All checks were successful
PR checks / checks (pull_request) Successful in 1m1s
The three defects phase 7 found are fixed and merged: website#163 (SECRET_ENC_KEY missing from the root .env.example, plus BOT_INTERNAL_KEY in the README's "set at least" list) and installer#22 + docs#174 (the handoff printing /admin/shard). website#163 turned checkQuickstart red here, which is precisely what the declaration was built to do -- it fails the moment a declared key appears upstream, so the note describing the omission cannot outlive the defect. The SECRET_ENC_KEY entry is deleted and notInUpstreamEnvExample is now empty; the export stays so the next divergence gets an entry rather than passing quietly. The stale-path Aside on Connect a game server is pinned to v0.1.0 rather than calling the installer permanently wrong, and now says WHY the old path is worse than a 404: the SPA has no route for it, so it redirects to the dashboard and the link looks like it worked. v0.1.0 is still the current download, and not only because releases lag. The release run for installer#22 built every artifact and pushed tag v0.1.1, then took a 500 creating the release -- so the tag is orphaned and no binaries were published. Raised on installer; nothing is worked around here. This also recovers |
|||
| f499f2b72b |
docs(journey): phase 7 — the installation path and administration
All checks were successful
PR checks / checks (pull_request) Successful in 9m25s
Twenty documentation pages: Getting started (7) and Administration (13), the journey no existing document owns end to end because the repositories are organised by component and an operator is not. Four decisions of record, taken before anything was written (D34–D37, PLAN.md §10 "How phase 7 built the documentation journey"): - D34 one PR for all twenty pages. - D35 the install page is SELF-CONTAINED: it prints a complete Compose file and a complete .env that an operator copies without visiting another repository. That is a copy of somebody else's file, so it is checked rather than trusted — scripts/checkQuickstart.mjs re-reads website main:docker-compose.yml and main:.env.example over the Gitea API and fails on any disagreement, in both directions: a value that drifts fails, and a service or variable that appears upstream fails until it is either included or recorded as deliberately omitted with a reason. Its first run found two stale entries. - D36 every Administration screen was walked on a real deployment before it was described — the rig being the quickstart itself, against the published image, so one run proved the install page and produced the detail the admin pages needed. - D37 a thirteenth Administration page, Content, so that every admin nav row has a home without organising the docs by the app's menu. What the live deployment disproved, all three now documented: - The documented Compose deploy does not boot. SECRET_ENC_KEY is required in production (utils/secretBox.js throws at require time) and is missing from website's ROOT .env.example — the file Compose reads. It is present in server/.env.example, which is why dev never hits it. The quickstart carries it, declared as an upstream omission so the check fails the day it is fixed. - The installer points operators at a screen that no longer exists: it prints <site>/admin/shard, and INSTALL.md §5 repeats it, but since the module cutover the screen is /admin/uo/link. Both the binary and the guide are stale. - The admin Restart button opens a window.confirm whose text is the honest warning that a deployment with no supervisor does not come back — which is why `restart: unless-stopped` is called out as load-bearing rather than left as boilerplate. And the defect only a look found, three phases running: the .env block's prose promised that every highlighted line must be changed, while `mark` given the variable names highlighted the names alone and left the values unmarked. Every check passed on a page that was wrong about its own highlighting. verify green: 890 internal links, 52 branch links, 19 facts, 59 quickstart checks, 0 astro-check errors. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| a2faf07104 |
feat(legal): phase 6 — the privacy policy and the terms
All checks were successful
PR checks / checks (pull_request) Successful in 55s
PLAN.md §9. Builds /privacy and /terms, links them from the footer on every page,
and generates the Play Data Safety notes from the same inventory the policy renders.
Four decisions taken by the org lead before either page was written, recorded in
§9 under "How phase 6 built the legal pages":
D30 DNS-only records, so the reverse proxy on the host keeps the only access
log. Described qualitatively — the retention belongs to the proxy, and a
policy that quotes a number the deployment does not enforce is worse than
one that does not.
D31 Eighteen or older. Above the children's-consent threshold everywhere in the
EEA, so consent works with no parental-consent machinery this form could not
honestly operate. Four surfaces render it from src/data/legal.mjs, and every
one says plainly that nothing verifies it.
D32 No governing-law clause. Nothing of value is contracted for here.
D33 PLAY_DATA_SAFETY.md is generated from src/data/collection.mjs and checked in
CI, so the published policy and the answers given to Google cannot drift.
/privacy is three separately-scoped sections because "we" means three different
parties: this site (one form, no cookies, no third-party requests), the Android app
(we operate no server it talks to — the rows are what the DEVICE holds), and a
self-hosted deployment (the operator is the controller, not us). Every row names the
file it was read out of, because a policy is the document most likely to be written
from a template and least likely to be re-read against the software.
/terms governs only what we run: this site, the beta list, and the APK we publish.
The software is governed by its licence, and a community's deployment by that
community — a terms page claiming authority over every install of a GPL program is
the thing a generated template gets wrong.
Also here:
- the age clause changed CONSENT_TEXT, so CONSENT_VERSION gained a suffix; rows
written from now on carry the new sentence and older rows keep theirs
- PLANNED_ROUTES is now empty — these were its last two entries, and its reverse
check is what forced the deletion; the list stays for phases 7 and 8
- test/legal.test.mjs asserts the structural promises no build check can see,
including that every mapped Play row still answers "not collected, not shared"
- --check normalises line endings: the repo has no .gitattributes and Windows
checkouts are CRLF, so a byte comparison would fail for every Windows developer
while passing in CI
Verified: npm run verify green end to end (tokens, brand, data safety, astro check,
36 tests, build, 214 links, 19 facts), both pages walked in a browser, and neither
overflows at 390px. One defect the checks could not see and a look could: the
retention line was being pushed to the foot of the tallest card in its row, opening
a void in the middle of the short ones.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| 1313e748ae |
feat(beta): phase 5 — the app page and the closed-beta signup
All checks were successful
PR checks / checks (pull_request) Successful in 1m5s
Builds `/app/` and `/beta/`, the SQLite signup store, the rate limiting and the export CLI of PLAN.md §8, and adds this repository's first test suite. Four decisions of record, D26–D29 (§8, "How phase 5 built the app and the beta"): - D26 — the screenshot slot ships empty, reserved for phase 9. §10 promised `/app/` "the 14 existing screenshots"; they are a July trusted-device smoke test against an unseeded dev instance, captured before the theming work, and five of the fourteen are two-factor prompts. Shipping them would break D4. Phase 9 already builds the rig, so it gains an emulator pass. - D27 — the public demo is the tester target. `ConnectScreen.kt` gates the whole app on a validated deployment address, so a tester needs somewhere to point it. The beta therefore waits on the demo VM, and the page says so. - D28 — `/beta` handles its own POST; there is no `/api/beta-signup`. An endpoint cannot report a validation error without JavaScript. §6's diagram is amended. - D29 — the APK and the beta get equal billing, and the APK link is off: `androidApk.serviceable` is false because the published v0.5.0 build does not work. The panel stays and states that plainly rather than being removed. Three mechanisms the plan did not anticipate: - `liveBrand()` — a server-rendered page never passes through the boot rewrite, so `/beta` reads the mounted brand.json itself. Pasting the Play opt-in URL in takes effect on the next request rather than the next restart. - `checkLinks.mjs` derives on-demand routes from `prerender = false` in the source. A PLANNED_ROUTES entry would have been wrong: its reverse check fires when a route has been built, and an on-demand route never produces a file, so the entry could never rot out. - `npm test` — the five existing checks all read built output, and none of this logic appears there. A honeypot can stop working and leave the build identical. Also: `checkFacts.mjs` gains the APK assets and `minSdk`, and learns that RFC 2606 reserved domains are not contact addresses; the D13 rule is otherwise unchanged. Verified end to end against the built server: every outcome renders with no JavaScript, cross-origin POSTs are refused, a mounted opt-in URL appears without a restart, and the export CLI round-trips. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 2d19ee4220 |
feat(marketing): phase 4 — the marketing pages
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> |
|||
| 556dee7355 |
feat(home): phase 3 — the homepage
All checks were successful
PR checks / checks (pull_request) Successful in 49s
Replaces phase 1's scaffold with the real homepage: hero, the data path as
inline SVG, the self-hosted argument, all five capability groups, and the
get-started CTA. Three decisions the org lead took first are recorded in
PLAN.md as D17-D19.
The data path is drawn generically and captioned specifically (D17): the nodes
say "your game server" and "sidecar", the sub-labels and caption name ServUO and
uo-link. The SVG is aria-hidden because the four numbered steps beside it carry
the same path in prose — one telling, not two.
The capability list is data with a check behind it (D18). Every Game-intelligence
item names the module-uo capability slug it comes from, and the build fails if
the page and platform.json disagree either way. That needed a fifteenth fact in
checkFacts.mjs: §12 named the capability list as an externally-sourced fact and
nothing re-read it, so the chain rested on someone remembering. It also found
that the site was omitting two of the module's eight capabilities — guilds and
city governors are now listed, in the page and in §10.
The hero leads with the emblem (D19), derived from whichever logo.png is in
force so one file still changes the hero, header, tab icon and app icon
together.
Also here, both found by standing the build up rather than by review:
- checkBrand.mjs now enforces the demo slot's markup contract. applyBrand.mjs
reveals the demo link by replacing an exact pair of empty attributes; an
attribute inserted between them produces a build where the mount sets a demo
URL, the boot log says nothing and the link never appears. Both halves are
checked and the literal is derived from the expression applyBrand.mjs uses,
so they cannot drift.
- The header nav overflowed at 390px — four links plus the lockup measured
433px against a 390px viewport, so every phone got a horizontally scrolling
page. Phase 1 left this to phase 3 expecting a disclosure control; it got a
wrap instead, because with four links there is nothing to disclose and a
hamburger costs state, script and duplicate markup.
Verified on a clean checkout of this commit: all four checks, astro check, a
production build, a live /brand/* smoke, and a demo URL mounted and reverted.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| fe4abe0ebf |
feat(brand): phase 2 — the branding pipeline
All checks were successful
PR checks / checks (pull_request) Successful in 9m9s
PLAN.md §7: swapping a logo or recolouring the site is a file copy and a container restart, never a rebuild. Phase 2 builds the mechanism and the checks that keep it true. GET /brand/* resolves every file against the mount first and the baked-in defaults second, per file, at stable unhashed URLs with an ETag and a five minute TTL. Nothing goes through Vite, which would fingerprint the names out of the mount's reach. An X-Brand-Source header says which step answered. Three decisions were taken with the org lead (recorded as D14-D16 in §7): D14 — one raster in, every size out. brand-default holds a single logo.png; the header mark at three pixel ratios, both install icons, the apple-touch icon, the favicons and a real multi-resolution favicon.ico are derived on request from whichever logo.png is in force, cached, and limited to an allowlist of sizes. Shipping fifteen precomputed files would have meant an operator producing fifteen to change a mark — and getting a new header with the old favicon. D15 — brand text is applied at boot. Pages are prerendered, so §7's promise about the site name, tagline and links could not hold at render time. npm start now runs scripts/applyBrand.mjs first, rewriting the built HTML from what it last applied to what the mount says. It rewrites from a record in dist/.brand-applied.json rather than from the defaults, because the naive version works exactly once and then silently ignores every later edit. An empty mount is a no-op; removing a mount restores the stock build byte for byte. Verified both ways, plus a second rename. D16 — the header shows the real emblem, replacing phase 1's placeholder glyph, so the site, the product and the Android launcher icon are one mark. It is raster art, so theme.css cannot recolour it; replacing logo.png is how the mark changes. Two defects found and fixed while proving it: The mounted theme.css did not win. Astro emits its own stylesheet after the head markup, so linking the operator's last was not enough and every override was silently a no-op. tokens.css now lives in @layer tokens and the mounted file is unlayered, which takes order out of the mechanism entirely. The documentation was a different site. Starlight builds its own head, so the docs linked a Starlight default /favicon.svg that does not exist here, carried no manifest or OG card, and never loaded the brand stylesheet — a mounted theme recoloured the marketing pages and left the docs stock. A Head override fixes it; half a rebrand looks like a product bug rather than a missed step. brand-default/wordmark.svg and og-image.png are generated by scripts/buildBrandAssets.mjs from the emblem and Cinzel's outlines and are committed, so CI needs neither the artwork nor a font. Type is converted to paths, because an SVG in an <img> can see neither the page's @font-face rules nor fontconfig — the same isolation that broke currentColor in phase 1. Its glyphs are drawn at the origin and translated: opentype.js emits NaN coordinates at a non-zero origin for some glyphs, and a path parser stops at the first malformed command, so the first lockup read "Runic Gate" and looked like a typo rather than a bug. scripts/checkBrand.mjs is the mechanism for the two failures that are otherwise silent: it puts every literal /brand/... URL in the source through the route's own classifier, so a size that is not on the allowlist fails the build instead of 404ing in a browser, and it rejects a brand string short enough that a blind replacement at boot could corrupt a page. Negative-tested three ways before being trusted. It runs in CI ahead of the type check. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| d4ab453361 |
fix(site): anchor the bind-mount ignores, and commit platform.json
Some checks failed
PR checks / checks (pull_request) Failing after 1m1s
`data/` without a leading slash matches a directory of that name at any depth, so it silently swallowed src/data/platform.json -- the single file every page and checkFacts.mjs reads. The working tree still had it, so `npm run verify` passed locally with all 14 facts green. CI cloned fresh and `astro check` failed on the missing module. Anchored both patterns to the repository root. Verified the way it should have been the first time: exported HEAD to a clean directory, npm ci, and ran the checks there rather than in the tree that was hiding the problem. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 66187dde5d |
feat(site): phase 1 — the foundation
Some checks failed
PR checks / checks (pull_request) Failing after 4m19s
Astro 7 with the Node adapter, Starlight mounted at /docs, the token file, both self-hosted typefaces, the layout shell, and the two build-time checks from §12. The palette's gold and cyan are sampled from runic-emblem.png rather than guessed, per §11: 494,059 opaque pixels binned by hue, each value annotated with its measured contrast against the ground, and restricted rather than brightened where a ratio fails. - checkTokens.mjs fails the build on any colour literal outside tokens.css, which is what keeps §7's "recolouring is a file copy" promise true. - checkFacts.mjs re-reads all 14 externally-sourced facts from their authorities over the Gitea API and fails on disagreement. It also enforces D13: no email address in the source outside brand-default/brand.json. - Both were negative-tested; neither has ever been allowed to pass by default. §6 asks for output:'server' with per-page prerender=true. Astro 7 expresses the same runtime shape as output:'static' with an adapter, opting individual routes out — so the default is static rather than accidentally server-rendered. Co-Authored-By: Claude <noreply@anthropic.com> |