Commit Graph

81 Commits

Author SHA1 Message Date
297cc83db5 docs(plan): record the Oxide floor ruling and the API.md table fix
All checks were successful
PR checks / checks (pull_request) Successful in 8m36s
The bundle's Oxide 2.0.7585 is the original compatible version and stays
until the CI staging drill reports an issue (org lead, 2026-10-22). The
API.md table break is fixed in docs #371.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-22 16:02:40 -05:00
ecc556d7ac feat(docs): phase 16 - the RunicNPC subgroup, its API carried in full and drift-checked
All checks were successful
PR checks / checks (pull_request) Successful in 2m11s
PLAN.md §16 phase 16, with the org lead's D72-D74 (count of record 74).

- Eight pages nested in the Rust group at /docs/rust/runicnpc/ (D72): the
  overview, Install RunicNPC, Commands, NPC profiles, NPC placements, Cost and
  caps, Profile fields (the one field reference the editor, rnpc.profile set,
  the data file and the API all share) and The API.
- The API page carries every call, hook and data shape (D62), in the site's
  own words, with no decision numbers and no per-call version history (D74).
  Signatures print from src/data/runicnpc.mjs through Signature.astro.
- checkReference.mjs gains two sections. Against docs main:runicnpc/API.md:
  the API version, 24 call and 10 hook signatures, and the field names of the
  profile, RunicNpc_List rows, placements, routes and owners (D73), plus
  exactly one <Signature> per call on api.mdx. Against runicnpc-rust
  main:plugin/RunicNPC.cs: console verbs, permissions, place options and
  config keys. Six planted drifts all failed before it was trusted.
- checkSidebar and checkTerms read nested sidebar groups via flatLabels().
- The Rust overview, Events zones and rewards, and Canonical documents link
  the new pages; the canonical documents gain the four runicnpc/ docs.

Both NPC admin screens were walked on the walk site first (D36); nothing was
saved. npm run verify is green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-22 15:49:07 -05:00
fe86fc369d Merge pull request 'docs(plan): phase 15's other-repository findings are fixed' (#36) from docs/phase-15-followup into main
All checks were successful
PR checks / checks (push) Successful in 2m42s
Build and publish the image / build (push) Successful in 3m23s
Build and publish the image / deploy (push) Successful in 31s
Reviewed-on: #36
2026-10-22 20:32:25 +00:00
9b28c95305 docs(plan): phase 15's other-repository findings are fixed
All checks were successful
PR checks / checks (pull_request) Successful in 2m42s
PLAN.md §16 recorded three phase-15 findings as reported rather than
fixed. The org lead asked for them fixed the same day: Module-Rust #59
(24 decision numbers in admin copy, and "1 player holds one") and
Rust-Plugins #45 (EventsEnabled's comment). The record now says so.

Reading every EventsEnabled check for that comment also showed the
switch applies at once when saved from the site's config editor, while
the bridge's other settings wait for its next reload. RunicGateway.json
now says that, and adds paying coins to what the switch stops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-22 15:31:45 -05:00
aad3a37c09 Merge pull request 'feat(docs): phase 15 — the Rust group, its reference pages checked against their sources' (#35) from feat/phase-15-rust into main
All checks were successful
PR checks / checks (push) Successful in 2m17s
Build and publish the image / build (push) Successful in 3m41s
Build and publish the image / deploy (push) Successful in 33s
Reviewed-on: #35
2026-10-22 20:26:31 +00:00
75c8ffc07a feat(docs): phase 15 - the Rust group, its reference pages checked against their sources
All checks were successful
PR checks / checks (pull_request) Successful in 2m5s
PLAN.md §16 phase 15. A Rust group of fifteen pages after Ultima Online
(D70): the module, installing the bridge (installer, Pterodactyl egg, by
hand), connecting the site, verifying the bridge, linking Steam accounts,
permissions and groups, mod config, who sees who is online, events, zones
and rewards, optional plugins, the live map, troubleshooting, and three
reference pages - rust-link's sidecar.toml, the plugin's
RunicGateway.json, and the event kinds.

checkReference.mjs gains three Rust sections rather than a new check:
sidecar.toml keys and RUSTLINK_* overrides from Rust-Link config.rs,
RunicGateway.json from the plugin's PluginConfig, and the event kinds
in three classes against Module-Rust server/catalogue.js (D71). Every
admin screen was walked on the walk site before it was described (D36).

Core pages now hand off to the Rust group instead of linking out to the
docs repository. Three defects the re-read found here are fixed: the UO
/health sample's hard-coded protocol 4 (both games' samples now render
platform.json through <Code>), Authentication's missing Steam tab, and
two pages saying the site cannot reach EventsEnabled - false on Rust.

Installer v0.8.1 moved checkFacts red mid-phase; platform.json follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-22 15:23:07 -05:00
a100195e7b Merge pull request 'feat(docs): phase 14 — game-neutral core docs, an Ultima Online group, and checkTerms' (#34) from feat/phase-14-vocabulary into main
Some checks failed
Build and publish the image / build (push) Failing after -1m50s
Build and publish the image / deploy (push) Has been skipped
PR checks / checks (push) Successful in 8m56s
Reviewed-on: #34
2026-10-22 19:41:17 +00:00
4426d3e339 feat(docs): phase 14 - game-neutral core docs, an Ultima Online group, and checkTerms
All checks were successful
PR checks / checks (pull_request) Successful in 2m10s
PLAN.md §16, D63/D64, plus D67-D69 taken 2026-10-22.

- Core docs use core's words: a Team, a game server, the sidecar, the bridge
  plugin, the game module. Detail that belonged to Ultima Online moved into
  its pages rather than being dropped.
- New "Ultima Online" sidebar group (nine pages): an overview, Connect a
  ServUO shard, Verify the bridge, The shard connection, Client files,
  Troubleshooting, Bridge.cfg, sidecar.toml, Shard event catalog. Five moved
  URLs answer a real 301 (src/config/redirects.mjs, asserted by the served
  test).
- Getting started is game-neutral. "Connect your game server" keeps the old
  URL and hands off to each game; Rust links docs main:rust-link/INSTALL.md
  until phase 15. "Verify the whole stack" stays, neutral (D69).
- D68: GameExample.astro labels a game's real example on a core page with the
  game's name; checkTerms allows that game's words inside it.
- scripts/checkTerms.mjs, the twelfth check: 16 game words, docs pages and
  both sidebar trees, one allowlisted phrase (Discord's "Guild (server) ID").
  Wired into verify and pr-checks.yml.
- canonicalDocs grouped by game; the Rust group carries four documents.
- D67: the screenshot rule from #32 is adopted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-22 08:54:37 -05:00
6046214d71 Merge pull request 'feat(facts): phase 13 — per-game facts, Rust and RunicNPC checked, main green again' (#33) from feat/phase-13-facts into main
All checks were successful
Build and publish the image / build (push) Successful in 2m27s
PR checks / checks (push) Successful in 2m50s
Build and publish the image / deploy (push) Successful in 37s
Reviewed-on: #33
2026-10-22 13:11:14 +00:00
9938e71e07 feat(facts): phase 13 - per-game facts, Rust and RunicNPC checked, main green again
All checks were successful
PR checks / checks (pull_request) Successful in 8m25s
platform.json drops the single protocol/bundle/capability list for a
`games` object (uo, rust) plus a `runicnpc` block. checkFacts.mjs loops
the protocol, plugin-protocol, capability and bundle checks over both
games, reads the schema-2 bundles at v2/<game>/current.json, checks each
module's coreApi, and reads RunicNPC's plugin.toml. 41 facts, up from 19;
clears the two that were red on main (Module API 1.17.0, installer
v0.8.0).

D66: the hero, footer and structured data show the Module API only; each
game's protocol and bundle go on that game's pages, and the docs landing
table splits into shared versions plus one column per game.

The re-read section 1 requires found, and this fixes:
- three hard-coded "1.6.0" Module API versions in the module docs
- module-lifecycle saying modules load alphabetically (the loader now
  refuses a second module, W19), and the install pages never mentioning
  the one-module rule
- "there is one module" in five places, now two modules both built
  inside this project; the Integration Kit keeps its draft banner
- the homepage diagram's ServUO/uo-link sub-labels (D63 wording now)

notBuilt drops `second-module` (resolved by Module-Rust's release) and
rewords `multi-module`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-22 08:09:22 -05:00
2971b84d51 Merge pull request 'docs(plan): revision 4 — a second game, RunicNPC, and a vocabulary rule' (#32) from docs/second-game-plan into main
Some checks failed
PR checks / checks (push) Failing after 2m4s
Build and publish the image / build (push) Successful in 4m11s
Build and publish the image / deploy (push) Successful in 1m23s
Reviewed-on: #32
2026-10-22 12:59:55 +00:00
b1584043a3 docs(plan): revision 4 - a second game, RunicNPC, and a vocabulary rule
Some checks failed
PR checks / checks (pull_request) Failing after 1m52s
Adds PLAN.md section 16 with the org lead's six decisions of 2026-10-22
(D60-D65) and phases 13-18:

- D60 per-game sidebar groups (Ultima Online, Rust); core docs go
  game-neutral, moved pages keep redirects
- D61 RunicNPC as a subgroup inside Rust, plus a standalone /runicnpc/
- D62 the RunicNPC API carried in full, with a drift check against
  docs/runicnpc/API.md (a deliberate exception to section 1)
- D63 game names allowed outside game sections, game vocabulary not
- D64 checkTerms.mjs, a twelfth check, amending D53
- D65 Rust screenshots last, captured from the rigs

Also records what moved since the site's last fact check: Module API
1.17.0 and installer v0.8.0 (main is already red on both), Module-Rust
v0.5.0, the v2/rust bundle at protocol 14, and RunicNPC v1.0.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-22 07:17:09 -05:00
c3c347d237 Merge pull request 'docs(site): the Asset Bridge — protocol 8, a screen for client files, and pictures in the screenshots (Phase 9c)' (#31) from docs/asset-bridge-p9c into main
All checks were successful
PR checks / checks (push) Successful in 9m28s
Build and publish the image / build (push) Successful in 2m59s
Build and publish the image / deploy (push) Successful in 1m20s
Reviewed-on: #31
2026-09-15 12:11:16 +00:00
4e1ce2316a docs(site): the Asset Bridge — protocol 8, a screen for client files, and pictures in the screenshots (Phase 9c)
All checks were successful
PR checks / checks (pull_request) Successful in 1m41s
The last leg of the Asset Bridge (docs/link/v8.md §16, phase 9c). The cutover put
protocol 8 on `main` in every declaration site, which turned this repo red by
construction: `checkFacts.mjs` reads the protocol from `link`'s `main`.

Two checks were failing, and only one of them was expected.

**checkFacts (10)** — protocol 7 → 8 in all three reads (sidecar, overlay, bundle),
the bundle triple to 2026.09.15 / sidecar v2.3.0 / overlay v1.3.0, and the releases
to link v2.3.0, installer v0.2.0, Module-uo v1.3.0. `moduleApi` and the capability
list did not move: core's whole share of eight phases was a deletion.

**checkReference (9)** — nobody had planned for this one. `Bridge.cfg` grew nine keys
across phases 5, 6 and 7 and the reference page had gone on not mentioning them,
which is the failure mode that check exists for. They are listed as two new groups,
`Client assets` and `Spawn files`, because they are two separate consents.

## The prose the bridge changed

- **New page, Administration → Client files.** `Admin → Client files` is a screen an
  operator has to press a button on and the site had no page for it. It carries the
  rule nothing else states: **nothing here happens on a restart**, so a patched client
  keeps serving the old artwork until somebody says so.
- **Protocol versions** said the most recent bump touched five repositories and named
  `website`. Protocol 8 touched four and did not reach core — its absence is the
  interesting half, and the page now says why. The store-migration paragraph gains 8,
  which changed no line of the sidecar's store despite moving megabytes.
- **The bridge** said "two ways in". There are three: bulk reads go over the
  request/reply path in pages, one in flight at a time, and the reason they must not
  ride the event stream is structural rather than about speed.
- **Requirements** gains `libgdiplus` for Linux shard hosts, and the note that no game
  client has to be installed for any of this — a ServUO shard cannot boot without one.
- **Troubleshooting** gains the failure mode that did not exist before: a bestiary with
  no pictures, or items reading as numbers.
- **Maintenance** gains the button to press after patching a client, and **Verify the
  whole stack** the `doctor` check that landed with it.
- The spawn atlas and the marketplace both carry artwork now, so both capability
  descriptions say so, and `canonicalDocs` points at `link/v8.md`.

## Screenshots, retaken against the real thing

The atlas and marketplace shots predate the asset plane, so both were retaken on the
demo deployment against a real shard and a real sidecar, plus the new admin screen:
1,095 portraits imported in 2.1 s, 67,496 names in 1.4 s, 313 item pictures in 0.8 s.
Nothing here is a mock-up. The atlas shot moved 36px down its page because portraits
made the rows taller.

## One defect found on the way

`seedDemo.mjs` pinned the demo deployment at **protocol 4** with a comment explaining
a `module-uo` debt that has since been paid. Its default now comes from
`platform.json`, so the rig cannot drift two protocols behind the platform again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-15 05:45:38 -05:00
73f664c38e Merge pull request 'docs(site): the Event System — the platform's facts, and two pages for it' (#30) from docs/events-system into main
All checks were successful
Build and publish the image / build (push) Successful in 4m14s
Build and publish the image / deploy (push) Successful in 42s
PR checks / checks (push) Successful in 8m22s
Reviewed-on: #30
2026-09-10 04:27:50 +00:00
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
2026-09-09 22:04:27 -05:00
5f58a4cbf2 Merge pull request 'docs(site): /privacy tells the truth about engagement retention again' (#29) from docs/engagement-retention into main
All checks were successful
Build and publish the image / build (push) Successful in 1m22s
Build and publish the image / deploy (push) Successful in 11s
PR checks / checks (push) Successful in 1m32s
Reviewed-on: #29
2026-09-01 20:57:24 +00:00
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>
2026-09-01 15:41:14 -05:00
a0af01026e Merge pull request 'docs(site): the engagement system — cutover 7 of 7 (edge → main)' (#28) from edge into main
All checks were successful
PR checks / checks (push) Successful in 1m38s
Build and publish the image / build (push) Successful in 3m30s
Build and publish the image / deploy (push) Successful in 27s
Reviewed-on: #28
2026-09-01 18:04:20 +00:00
5c6735bd1d Merge pull request 'chore(facts): protocol 5, Module API 1.9.0, and the 2026.09.01 bundle' (#27) from docs/platform-facts-protocol-5 into edge
All checks were successful
PR checks / checks (pull_request) Successful in 1m24s
Reviewed-on: #27
2026-09-01 17:45:16 +00:00
5e987518c6 fix(checks): read files through Gitea's contents endpoint, not raw
The three checks that read another repository -- checkFacts, checkQuickstart,
checkReference -- fetched source files from the API's `raw` route, which answers
`Cache-Control: public, max-age=21600`. The CDN in front of Gitea caches that,
so the checks can read a blob most of a working day old.

It bit on cutover day. checkFacts reported

    FAIL  moduleApi
          platform.json says : 1.9.0
          website main:server/src/modules/version.js says : 1.6.0

against a `main` that says 1.9.0 -- the served copy was two weeks old
(`cf-cache-status: HIT`, `Age: 15713`, `last-modified: 18 Aug`). No edit in this
repository could have made it pass, and the same run reported a bundle triple
that had already been republished as still current: a stale read fails BOTH
ways, and the false pass is the dangerous one.

The `contents` endpoint answers `private, must-revalidate`, which the CDN
bypasses, so it is always the ref's current blob. The cost is a JSON parse and a
base64 decode. checkReference's canonical-document existence loop already used
it, which is why that half was never affected.

PLAN.md 12 records the finding next to the check it constrains.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-01 12:41:01 -05:00
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>
2026-09-01 12:36:12 -05:00
d01fd55f43 Merge branch 'edge' into docs/platform-facts-protocol-5 2026-09-01 13:19:46 +00:00
83c438885e Merge pull request 'docs(admin): the engagement rules screen, and the privacy inventory an engagement mailer changes' (#26) from docs/engagement-rules-and-privacy into edge
Reviewed-on: #26
2026-09-01 13:18:56 +00:00
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>
2026-09-01 08:14:09 -05:00
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>
2026-09-01 08:11:10 -05:00
916921551f Merge pull request 'docs(troubleshooting): suppression and bounces (engagement Phase 9)' (#25) from docs/engagement-deliverability into edge
Reviewed-on: #25
2026-08-31 15:53:24 +00:00
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>
2026-08-31 10:49:26 -05:00
92a1a33121 Merge pull request 'docs(admin): on-site notifications, and the defaults page that was wrong' (#24) from docs/engagement-inapp-channel into edge
Reviewed-on: #24
2026-08-31 07:23:21 +00:00
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>
2026-08-31 02:07:35 -05:00
782e3df7e2 Merge pull request 'docs(admin): Team notification emails are engagement rules now' (#23) from docs/engagement-email-channel into edge
Reviewed-on: #23
2026-08-31 06:07:33 +00:00
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>
2026-08-29 20:12:27 -05:00
ebd6ba8dbf Merge pull request 'docs(admin): a page for the message-template editor' (#22) from docs/engagement-template-editor into edge
Reviewed-on: #22
2026-08-29 23:37:07 +00:00
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>
2026-08-29 18:14:21 -05:00
5ba91717c0 Merge pull request 'docs(admin): email is SMTP now, and the Gmail connect flow is gone' (#21) from docs/engagement-phase-1-smtp into edge
Reviewed-on: #21
2026-08-29 02:10:14 +00:00
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>
2026-08-28 21:01:03 -05:00
268e1c98fc Merge pull request 'fix(footer): send each documentation link to the section its label names' (#20) from fix/footer-docs-links into main
All checks were successful
PR checks / checks (push) Successful in 1m35s
Build and publish the image / build (push) Successful in 1m43s
Build and publish the image / deploy (push) Successful in 31s
Reviewed-on: #20
2026-08-27 05:31:35 +00:00
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>
2026-08-27 00:30:15 -05:00
ce13a44ed7 Merge pull request 'feat(compose): publish on every interface, so the host's own address answers' (#19) from feat/publish-on-all-interfaces into main
All checks were successful
PR checks / checks (push) Successful in 1m27s
Build and publish the image / build (push) Successful in 1m54s
Build and publish the image / deploy (push) Successful in 10s
Reviewed-on: #19
2026-08-27 05:07:31 +00:00
b86f4cabf0 feat(compose): publish on every interface, so the host's own address answers
All checks were successful
PR checks / checks (pull_request) Successful in 9m56s
D55 bound the published port to 127.0.0.1, on the reasoning that TLS terminates
at a proxy on the same host and nothing else has business reaching the container.
That is right for the host this ends up on and wrong for every step before it: a
loopback binding cannot be opened from a browser on another machine, which is the
first thing an operator wants to do — look at the site on the VM's own address,
before DNS exists, before the proxy exists, from a desktop or a phone.

The port line is now "${SITE_BIND_ADDR:-0.0.0.0}:${SITE_HOST_PORT:-4321}:4321",
so http://<vm-ip>:4321 answers out of the box, the way a normal bridge publish
behaves. Which addresses it answers on is a variable rather than an edit:
SITE_BIND_ADDR narrows it to one interface, or back to loopback, without touching
a file that `docker compose pull` replaces. That also retires the "change the port
line yourself" instruction DEPLOY.md had to give a proxy running in another
container or on another machine.

What is given up, said plainly in DEPLOY.md §3.1: on a host with a public address
the default answers on port 4321 from the internet, plain HTTP beside the proxy's
443, with no proxy in the path to set X-Forwarded-For — so signups arriving that
way share one rate-limit bucket. There is no login and no secret behind it, so it
is untidy rather than dangerous, and both remedies are named (firewall the port,
or narrow the binding).

Verified by running it, not only by reading it: `docker compose config` accepts
both bindings and resolves host_ip 0.0.0.0 and 127.0.0.1 respectively; the stack
came up healthy, `docker compose port site 4321` reported 0.0.0.0:4321, and the
site answered 200 on both 127.0.0.1 and the machine's LAN address, still carrying
its own per-page CSP.

Recorded as D59, amending D55. Count of record fifty-nine.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-26 23:42:45 -05:00
e60812fb34 Merge pull request 'fix(image): ship node_modules in three layers, and count them before pushing' (#18) from fix/registry-layer-cap into main
All checks were successful
PR checks / checks (push) Successful in 1m20s
Build and publish the image / build (push) Successful in 4m3s
Build and publish the image / deploy (push) Successful in 46s
Reviewed-on: #18
2026-08-27 04:23:18 +00:00
92f00ab20a fix(image): ship node_modules in three layers, and count them before pushing
All checks were successful
PR checks / checks (pull_request) Successful in 9m47s
The merge that landed phase 12 built its image and could not publish it.
`docker push` answered 413 Payload Too Large on one blob and stopped, so the
registry stayed empty and `needs: build` meant the deploy never ran — the site
was merged and undeployed, and the log said only that a digest was too large.

The limit is Cloudflare's, not Gitea's: the instance is proxied, and Cloudflare
refuses a request body over 100 MB below Enterprise. A push uploads each layer
as one monolithic PUT, so the ceiling is per layer and the rejection happens at
the edge, where Gitea never sees it and no Gitea setting can lift it.

One layer was over, by nine megabytes: COPY node_modules at 108.8 MB compressed,
in a 188.6 MB image whose next largest layer is the 47.6 MB Node base. @pagefind
and @img (sharp's libvips) account for it and both are needed at run time — the
boot rewrite re-indexes the site and re-derives the brand images — so what could
move is where they land, not whether they ship.

The build stage moves them aside after `npm prune` and the runtime stage copies
them as their own layers: 46.8 + 50.5 + 11.5 MB, and the image is exactly the
same total size, the same bytes divided differently. Moving rather than copying
twice keeps the three disjoint, so a dependency added later needs no maintenance
here.

A split is a margin and not a guarantee, so the workflow now counts layers before
it pushes: docker save, re-compress anything over 8 MB the way the push would,
fail at 90 MB — not 100, the blob is not the only thing in the request — naming
the layer and what would have happened. Tested in both directions; on the broken
image it reports 108 MB, which is what the registry recorded.

Verified by running the built container, not only by measuring it: healthy in
~25s, the mounted brand rewritten across 51 files, 50 pages re-indexed by
pagefind, and the favicon served as `x-brand-source: derived:mount` — which is
sharp resolving from its new layer. npm run verify is green, all eleven checks
and both suites.

Recorded as D58; DEPLOY.md gains the symptom and what it means for the host
(nothing — the running container is untouched).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-26 23:10:34 -05:00
3b4067fa37 Merge pull request 'feat(delivery): phase 12 — the container, and the defect only a proxy could find' (#17) from feat/phase-12-delivery into main
Some checks failed
PR checks / checks (push) Successful in 1m31s
Build and publish the image / build (push) Failing after 1m58s
Build and publish the image / deploy (push) Has been skipped
Reviewed-on: #17
2026-08-27 03:53:50 +00:00
1558111050 docs(deploy): correct the image size, and say what a missing rgcom runner does
All checks were successful
PR checks / checks (pull_request) Successful in 1m19s
The image measures 757 MB, not the ~600 MB the requirements table guessed. And
until the rgcom runner exists the deploy job simply queues, which is worth stating
in both the workflow and DEPLOY.md: build has already published the image by then,
so the manual pull works throughout and the queued job goes when the runner does.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 16:57:25 -05:00
f2e59a2426 feat(delivery): phase 12 — the container, and the defect only a proxy could find
All checks were successful
PR checks / checks (pull_request) Successful in 9m46s
PLAN.md §13 phase 12, the last one. Four decisions of record, D54–D57, taking the
count to fifty-seven; recorded in §6, "How phase 12 delivered it".

A two-stage Dockerfile, a pull-only docker-compose.yml carrying both bind mounts,
.env.example, the workflow that publishes and deploys, CONTRIBUTING.md, the
community-health files this was the only repository of the ten to lack, and
DEPLOY.md.

D54 — a merge deploys, amending D6. build-image.yml pushes
runicgateway-site:latest and :sha-<7>, then rolls the container over on the
`rgcom` runner out of /opt/runicgateway.com, and waits for the container's own
healthcheck rather than for `up -d` to return.

D55 — the site runs on its own host behind a generic reverse proxy, so DEPLOY.md
states the four requirements rather than one worked example, and the container
binds 127.0.0.1 so the safe configuration is the default.

D56 — @astrojs/node derives the request protocol from req.socket.encrypted and
never reads x-forwarded-proto, so behind a TLS-terminating proxy the browser sends
Origin: https://… while the container computes http://… and Astro's CSRF check
compares them for equality. Every beta signup, from every visitor, was answered
403. serve.mjs now normalises both forwarded headers, unconditionally — the image
should deploy and work. Two assertions in test/headers.test.mjs hold both halves.

D57 — DEPLOY.md rather than a README section; SECURITY.md and CODE_OF_CONDUCT.md
are pointers to the org's copies rather than copies, because a copy would hard-code
the contact address D13 confines to brand.json.

Verified: npm run verify green (eleven checks, 36 unit tests, 7 served tests,
astro check 0 errors). The image was built and run with both mounts — a mounted
brand reached 51 files and all 50 search pages, /brand/* fell back per file, a
proxy-shaped signup reached the store, and the export CLI wrote both Play files to
the host mount. docker compose config caught a YAML trap in the healthcheck: a
block sequence reads the `: ` in `r.ok ? 0 : 1` as a mapping.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 16:54:38 -05:00
18064062a9 Merge pull request 'feat(validation): phase 11 — the walk that found what the checks could not' (#16) from feat/phase-11-validation into main
All checks were successful
PR checks / checks (push) Successful in 1m18s
Reviewed-on: #16
2026-08-25 20:36:57 +00:00
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>
2026-08-25 15:16:08 -05:00
58883951a4 Merge pull request 'feat(polish): phase 10 — search, accessibility, SEO and a real CSP' (#15) from feat/phase-10-polish into main
All checks were successful
PR checks / checks (push) Successful in 9m35s
Reviewed-on: #15
2026-08-25 19:28:51 +00:00
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>
2026-08-25 14:14:53 -05:00
a34c2ce538 Merge pull request 'feat(screens): retake the two frames a signed-in character changes' (#14) from feat/screens-signed-in-retake into main
All checks were successful
PR checks / checks (push) Successful in 9m51s
Reviewed-on: #14
2026-08-25 15:38:38 +00:00