feat(screens): phase 9 — real screenshots, from a real shard
All checks were successful
PR checks / checks (pull_request) Successful in 1m25s
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:
136
PLAN.md
136
PLAN.md
@@ -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**:
|
||||
| D30–D33 | §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 |
|
||||
| D34–D37 | §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 |
|
||||
| D38–D41 | §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 |
|
||||
| D42–D46 | §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
|
||||
|
||||
Reference in New Issue
Block a user