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>
This commit is contained in:
2026-08-25 09:55:55 -05:00
parent 31d914ba44
commit c29ec94f46
36 changed files with 2253 additions and 59 deletions

136
PLAN.md
View File

@@ -87,7 +87,7 @@ All values re-read from the Gitea API on **2026-08-19**, after revision 1.
| uo-link sidecar | **v2.0.0** (2026-08-19) | release; in bundle 2026.08.19 |
| Plugin overlay | **v1.0.0** (2026-08-19) | release; in bundle 2026.08.19 |
| Installer | **v0.1.1** (2026-08-24) | release |
| `module-uo` | **v1.0.1** (2026-08-19) | release |
| `module-uo` | **v1.0.2** (2026-08-25) | release |
| Android app | **v0.5.0** (2026-08-08), id `com.runicgateway.app` | release; `app/build.gradle.kts` |
| ServUO | **57.4** — min version, and the only version the patch tier is verified against | bundle `overlay.servuo` |
| `website` | **no releases** — ships as container images, never tagged | Gitea releases API (empty) |
@@ -236,7 +236,7 @@ Taken by the org lead (Colby Whitlock) on 2026-08-19. Recorded so they are not r
**Decisions after D13 are recorded where they were taken**, in the section describing the phase that
raised them, rather than appended here — a decision is only re-litigated when its reasoning is
somewhere other than the thing it decided. The count of record is **forty-one**:
somewhere other than the thing it decided. The count of record is **forty-six**:
| # | Where | What it settled |
|---|---|---|
@@ -247,6 +247,7 @@ somewhere other than the thing it decided. The count of record is **forty-one**:
| D30D33 | §9, "How phase 6 built the legal pages" | One logging hop and no edge provider, eighteen or older, no governing-law clause, the Data Safety notes as a generated document |
| D34D37 | §10, "How phase 7 built the documentation journey" | One PR for all twenty pages, a self-contained install quickstart with a drift check, every admin screen walked before it was described, a thirteenth Administration page for content |
| D38D41 | §10, "How phase 8 built the builder and reference docs" | One PR for all twenty pages again, Reference enumerates names and checks every one of them, the docs section links to the drawn diagrams rather than importing them, `plannedSidebar` becomes a checked invariant |
| D42D46 | §10, "How phase 9 took the screenshots" | The full rig behind the imagery, a neutral demo brand, the captures beside the claims, a committed and checked capture pipeline, the world dressed in the plugin repo's scaffolding |
---
@@ -508,6 +509,11 @@ same day. `src/components/app/Screenshots.astro` exists now, rendering nothing,
data change rather than a design task. Rejected: shipping the fourteen, and pulling phase 9's rig
forward into phase 5.
*Filled in phase 9:* six captures, from an emulator pointed at the same seeded deployment the web
screenshots came from, on the same day — see "How phase 9 took the screenshots" in §10. The
component now reads `src/data/screens.mjs` rather than a list of its own, which is what made it a
data change in the end.
**D27 — the public demo is the tester target, so the beta waits for it.** `ConnectScreen.kt` on
`Android-app` `main` is unambiguous — nothing in the app runs until a valid Runic Gateway site has
been entered and validated — so an installed app with no deployment behind it is a text field. The
@@ -670,7 +676,7 @@ Organised by what a reader is trying to do. A reader should never need to know t
| `/architecture/` | The system explained visually, for a technical evaluator deciding whether to run it |
| `/modules/` | What a module is, `module-uo` as the worked example, writing your own, the Integration Kit (draft-badged per D8) |
| `/integrations/` | Discord, mobile + ntfy push, SSO — with an explicit "not built" list |
| `/app/` | The Android app: what it does, the signed-APK download beside the beta CTA, and a screenshot slot **phase 9 fills** (D26 — the 14 existing screenshots are the wrong fourteen) |
| `/app/` | The Android app: what it does, the signed-APK download beside the beta CTA, and six phone captures **phase 9 filled** (D26 — the 14 existing screenshots were the wrong fourteen) |
| `/beta/` | The closed-beta signup (§8). The one page that handles its own POST (D28) |
| `/community/` | Discord (`discord.gg/t2Jav8yT4g`) as the front door, the Gitea org for code and contributions, the `brand.json` contact address for vulnerabilities (D13) — the split in §14 N3 |
| `/privacy/`, `/terms/` | §9 |
@@ -1012,6 +1018,121 @@ security boundary, and a set comparison alone would have passed it.
---
### How phase 9 took the screenshots
D4 said real screenshots from the review stack rather than placeholders, and left the how
open. Five decisions settled it, taken by the org lead before the rig was built.
**D42 — the full rig: a real shard, a real sidecar, a real site.** ServUO with the bridge
overlay on this machine, the Rust sidecar beside it, `website` `main` with `module-uo`
installed, and the demo database seeded on top for what a fresh shard cannot produce.
The alternatives were cheaper and both of them lie a little. Sidecar-only screenshots the
degraded state — a reachable bridge with nothing behind it. Everything-database-seeded
produces pages that look identical to the real thing and were produced by nothing: the
marketplace would be rows somebody typed. This is the one option where the marketplace rows
are player vendors the game actually holds, the atlas is parsed from the shard's own spawn
files, and "Candlewick House is now IDOC" happened.
**D43 — a neutral demo brand.** The deployment is "Runic Gateway Demo", not UOMysticmoon.
The screenshots show the platform rather than one private community, which is the same
instinct as D27's refusal to publicise a real shard — and §15's demo instance can wear this
identity the day it exists, so the imagery stays true rather than becoming a period piece.
The name says "Demo" deliberately: nobody should have to wonder whether they are looking at
a server they could join.
**D44 — the captures sit beside the claims they support, in two places.** A figure set on
`/features/`, one on the homepage, and inline shots on the phase-7 administration pages that
describe a screen in prose. Eleven web captures.
The administration pages are where a screenshot does the most work, because phase 7
described thirteen screens it could not show. A dedicated `/screenshots/` gallery was
rejected for the reason galleries usually are: a page nobody visits does less than a figure
sitting under the sentence it proves.
**D45 — the rig is committed, not remembered.** Three files rather than a folder of images:
`scripts/seedDemo.mjs` puts the content there by driving the site's own API,
`src/data/screens.mjs` declares every capture with its route, viewport, scroll offset and
caption, and `scripts/captureScreens.mjs` turns the second into files.
`scripts/checkScreens.mjs` is the ninth check script and runs in CI.
The argument is the same one D35 made for the install quickstart: the way real screenshots
rot is that the recipe for taking them lives in somebody's memory. Re-taking the set after a
redesign is now `npm run screens:capture`, and the check fails if an entry has no file, a
file is the wrong size, a file is orphaned, or a declared screen is rendered nowhere.
**Why the seed drives the API and never the database.** Every row it creates could have been
an `INSERT`, and every `INSERT` would be a second implementation of a rule the website owns —
how a body is sanitized, which excerpt is derived, how a password is hashed. A seed that
writes SQL produces a database the product could not have produced, and screenshots of that
database show a product that does not exist.
**D46 — the world gets dressed in `servuo-plugins/tools`.** `BridgeSeeder` builds a world at
realistic scale; it never needed the world to look like anything, so a vendor traded as
"Seed Shop 810" and a character was "Seed004A" — and every one of those strings travels the
whole bridge and lands on the marketplace, the guild roster and the housing pages.
`BridgeDemoDress` renames them in place and seeds nothing, drawing names from fixed tables
hashed off each object's serial, so a re-run reproduces the same world and a screenshot can
be retaken later and still match.
**What the rig found.** A screenshot rig is an integration test with a human in the loop, and
this one turned up five things nothing else had:
- **A fresh `module-uo` install pinned wire protocol 3 while the sidecar speaks 4**, so a new
deployment 409s on every shard read until an admin edits the number by hand. The protocol-4
cutover bumped `link`, `servuo-plugins` and `docs` and missed the module's own default.
Fixed upstream and released as `module-uo` **v1.0.2** — which is what this repository's own
facts check then noticed, since `platform.json` still said v1.0.1.
- **A renamed guild member never reaches the site.** The plugin folds name, abbreviation,
leader, member count and alliance into the signature it compares, and re-emits the roster
only when the member *set* changes — so renaming a member leaves the published roster stale
indefinitely.
- **A guild deleted while the shard is offline is a ghost row forever.** The "gone" pass
compares against a cache that is cleared on reconnect, so nothing emits `guild.remove`. The
demo's guild board was showing two guilds the world no longer had, a week after they went.
- **"Houses in danger" cannot show a house that was already collapsing.** The ingest writes
that column only from the `house.decay` transition feed, while the registry frame's stage is
deliberately left alone so the two cannot clobber each other. A house already in IDOC when
the site connects is therefore invisible — the page said none while the shard had two.
- **The Android news list prints raw ISO timestamps.** Found while choosing the phone
captures; the news screen was dropped from that set rather than shipping a picture of it.
The first is fixed. The rest are raised as product observations, with the rig working around
them: the guilds are built *after* the rename, and the IDOC staging is two passes with a wait
between them so the site watches the collapse happen. All of that is scaffolding under
`servuo-plugins/tools/`, which is never deployed.
**The emulator pass (D26), and the AVD that would not take it.** The six phone captures come
from an emulator pointed at the same deployment on the same day, signed in as an ordinary
player, reached through `adb reverse` — the app's debug network policy permits cleartext to
`localhost` only, which is a better default than the one that would have made `10.0.2.2`
work. The device is API 35 rather than the API 36 the plan named: the API 36 image on this
machine had 200 MB free and refused the install, and wiping somebody's development device to
take a screenshot is not a trade worth making.
The shard screen is the one worth having. It shows the two houses entering IDOC in its live
activity feed — the same event that reached `/uo/houses` in the browser, on the same rig, in
the same minute.
**One thing that did not happen: a character logged in.** The org lead asked for it, so the
scaffolding sets a known password on a seeded account and a client was driven as far as its
login screen. ClassicUO stores its password crypted and mangles a plaintext one from
`settings.json`, so auto-login fails; synthetic clicks reach the client but synthetic text
does not; and the remaining route — taking the foreground and typing — was tried once, missed
the client, and typed into the browser window the person at this machine was using. It was
not tried again. **Players online therefore reads 0, and the page is honest about a quiet
realm.** The client is left at its login screen: one manual sign-in and
`npm run screens:capture shard-status app-shard` retakes the two frames that would change.
**A layout decision worth recording.** The `/features/` figures are one-up at the column's
full width, not a two-column grid. Two-up was built first and is the obvious layout for a set
of figures — but these are screenshots of a dense interface, and halving the width puts the
product's own type at about a third of its real size, which reads as a thumbnail of something
rather than a picture of it. A long section of legible evidence beats a tidy grid of
unreadable tiles.
---
## 11. Visual direction
**"Modern infrastructure software with an arcane identity."** Dark-first. Marketing pages are
@@ -1110,6 +1231,13 @@ a mechanism rather than diligence:
disagreement. Two-directional, like `PLANNED_ROUTES`: 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 own first run found two stale entries.
- **`scripts/checkScreens.mjs`** — added in phase 9 for D45. `src/data/screens.mjs` is the one
list of what the site shows of itself, and this proves every entry has a file at the size the
markup declares, that nothing in `public/screens/` is orphaned, and that every declared
capture is rendered somewhere. The size half is the one that repays it: a re-capture taken at
the wrong viewport looks perfectly fine on its own and only reveals itself as a page that
reflows while it decodes. No browser and no game server — the capture tool is an authoring
script whose output is committed, exactly like the brand assets.
- **`scripts/checkTokens.mjs`** — no colour literal outside the token file (§7).
- `astro check` plus a production build, in CI on every PR.
@@ -1130,7 +1258,7 @@ a mechanism rather than diligence:
| **8** | Docs — builder and reference: Modules (8) + Architecture (5) + Reference (7) — twenty pages in one PR (D38), with Reference enumerating names and **checking every one of them** against its source (D39), and `plannedSidebar` becoming a checked invariant (D41) |
| **9** | Screenshots (D4): stand up the local review stack, seed presentable content, capture the admin panel, Teams, forums, marketplace, spawn atlas and shard console; build the screenshot components. **Plus an emulator pass against the same seeded stack** to fill `/app/`'s reserved slot (D26) |
| **10** | Polish: responsive, accessibility, SEO/OpenGraph/sitemap/robots, full-text search, CSP headers |
| **11** | Validation: `astro check`, production build, **all eight check scripts** (tokens, brand, links, facts, quickstart, data safety, reference, sidebar), mobile layout verified in a real browser, a signup walked end to end |
| **11** | Validation: `astro check`, production build, **all nine check scripts** (tokens, brand, links, facts, quickstart, data safety, reference, sidebar, screens), mobile layout verified in a real browser, a signup walked end to end |
| **12** | Delivery: Dockerfile, `docker-compose.yml` with both bind mounts documented, Gitea Actions workflow publishing to the registry, README, CONTRIBUTING with the AI-disclosure requirement, and an operator note covering DNS, TLS and the reverse proxy (D6) |
Phases 5 and 6 are deliberately adjacent and early: the beta cannot start without `/privacy`, and