diff --git a/.gitea/workflows/pr-checks.yml b/.gitea/workflows/pr-checks.yml
index bce64bd..1e547dd 100644
--- a/.gitea/workflows/pr-checks.yml
+++ b/.gitea/workflows/pr-checks.yml
@@ -78,6 +78,17 @@ jobs:
# No token, no network, no build — so it runs early and fails fast.
run: npm run check:sidebar
+ - name: Screenshots
+ # PLAN.md §12, phase 9 (D45). src/data/screens.mjs is the one list of what the site
+ # shows of itself: every entry must have a file, at the size the markup declares, and
+ # every file must have an entry. The size half is the one that repays the check —
+ # 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, so CI only reads what it produced.
+ run: npm run check:screens
+
- name: Production build
run: npm run build
diff --git a/PLAN.md b/PLAN.md
index f76daca..80e01da 100644
--- a/PLAN.md
+++ b/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
diff --git a/package-lock.json b/package-lock.json
index 8ba171e..9533e8f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"devDependencies": {
"@astrojs/check": "^0.9.10",
"opentype.js": "^2.0.0",
+ "puppeteer-core": "^23.11.1",
"typescript": "^6.0.3",
"yaml": "^2.8.1"
},
@@ -2254,6 +2255,164 @@
"win32"
]
},
+ "node_modules/@puppeteer/browsers": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz",
+ "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "extract-zip": "^2.0.1",
+ "progress": "^2.0.3",
+ "proxy-agent": "^6.5.0",
+ "semver": "^7.6.3",
+ "tar-fs": "^3.0.6",
+ "unbzip2-stream": "^1.4.3",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "browsers": "lib/cjs/main-cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@puppeteer/browsers/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/tar-fs": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz",
+ "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0",
+ "tar-stream": "^3.1.5"
+ },
+ "optionalDependencies": {
+ "bare-fs": "^4.0.1",
+ "bare-path": "^3.0.0"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/tar-stream": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz",
+ "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "bare-fs": "^4.5.5",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/yargs": {
+ "version": "17.7.3",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
+ "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz",
@@ -2618,6 +2777,13 @@
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"license": "MIT"
},
+ "node_modules/@tootallnate/quickjs-emscripten": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
+ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
@@ -2721,6 +2887,17 @@
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@ungap/structured-clone": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
@@ -2853,6 +3030,16 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/ajv": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
@@ -2991,6 +3178,19 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/ast-types": {
+ "version": "0.13.4",
+ "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
+ "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/astring": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz",
@@ -3115,6 +3315,21 @@
"node": ">= 0.4"
}
},
+ "node_modules/b4a": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
+ "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "react-native-b4a": "*"
+ },
+ "peerDependenciesMeta": {
+ "react-native-b4a": {
+ "optional": true
+ }
+ }
+ },
"node_modules/bail": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
@@ -3125,6 +3340,91 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/bare-events": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz",
+ "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "bare-abort-controller": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-fs": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.1.tgz",
+ "integrity": "sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-events": "^2.5.4",
+ "bare-path": "^3.0.0",
+ "bare-stream": "^2.6.4",
+ "bare-url": "^2.2.2",
+ "fast-fifo": "^1.3.2"
+ },
+ "engines": {
+ "bare": ">=1.28.0"
+ },
+ "peerDependencies": {
+ "bare-buffer": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-buffer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz",
+ "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/bare-stream": {
+ "version": "2.13.3",
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz",
+ "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "b4a": "^1.8.1",
+ "streamx": "^2.25.0",
+ "teex": "^1.0.1"
+ },
+ "peerDependencies": {
+ "bare-abort-controller": "*",
+ "bare-buffer": "*",
+ "bare-events": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ },
+ "bare-buffer": {
+ "optional": true
+ },
+ "bare-events": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-url": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.2.tgz",
+ "integrity": "sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-path": "^3.0.0"
+ }
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3145,6 +3445,16 @@
],
"license": "MIT"
},
+ "node_modules/basic-ftp": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz",
+ "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/bcp-47": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.1.tgz",
@@ -3234,6 +3544,16 @@
"ieee754": "^1.1.13"
}
},
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/ccount": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
@@ -3321,6 +3641,30 @@
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC"
},
+ "node_modules/chromium-bidi": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz",
+ "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "mitt": "3.0.1",
+ "zod": "3.23.8"
+ },
+ "peerDependencies": {
+ "devtools-protocol": "*"
+ }
+ },
+ "node_modules/chromium-bidi/node_modules/zod": {
+ "version": "3.23.8",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
+ "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/ci-info": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz",
@@ -3564,6 +3908,16 @@
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
"license": "CC0-1.0"
},
+ "node_modules/data-uri-to-buffer": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
+ "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -3624,6 +3978,21 @@
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
"license": "MIT"
},
+ "node_modules/degenerator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
+ "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ast-types": "^0.13.4",
+ "escodegen": "^2.1.0",
+ "esprima": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -3676,6 +4045,13 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/devtools-protocol": {
+ "version": "0.0.1367902",
+ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz",
+ "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/diff": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz",
@@ -3941,6 +4317,63 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/escodegen": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/escodegen/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/estree-util-attach-comments": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz",
@@ -4032,6 +4465,16 @@
"@types/estree": "^1.0.0"
}
},
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -4047,6 +4490,16 @@
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT"
},
+ "node_modules/events-universal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
+ "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-events": "^2.7.0"
+ }
+ },
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -4074,6 +4527,27 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
+ "node_modules/extract-zip": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "get-stream": "^5.1.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "extract-zip": "cli.js"
+ },
+ "engines": {
+ "node": ">= 10.17.0"
+ },
+ "optionalDependencies": {
+ "@types/yauzl": "^2.9.1"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -4081,6 +4555,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fast-string-truncated-width": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
@@ -4122,6 +4603,16 @@
"fast-string-width": "^3.0.2"
}
},
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -4241,6 +4732,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-tsconfig": {
"version": "5.0.0-beta.4",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz",
@@ -4256,6 +4763,21 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
+ "node_modules/get-uri": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
+ "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "basic-ftp": "^5.0.2",
+ "data-uri-to-buffer": "^6.0.2",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@@ -4711,6 +5233,34 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/i18next": {
"version": "26.3.6",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz",
@@ -4777,6 +5327,16 @@
"integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
"license": "MIT"
},
+ "node_modules/ip-address": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz",
+ "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/iron-webcrypto": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
@@ -4835,6 +5395,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-hexadecimal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
@@ -6357,6 +6927,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
@@ -6418,6 +6995,16 @@
"node": ">= 10"
}
},
+ "node_modules/netmask": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz",
+ "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/nlcst-to-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz",
@@ -6597,6 +7184,40 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/pac-proxy-agent": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
+ "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/quickjs-emscripten": "^0.23.0",
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "get-uri": "^6.0.1",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.6",
+ "pac-resolver": "^7.0.1",
+ "socks-proxy-agent": "^8.0.5"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/pac-resolver": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
+ "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "degenerator": "^5.0.0",
+ "netmask": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/package-manager-detector": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz",
@@ -6683,6 +7304,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/piccolore": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz",
@@ -6834,6 +7462,16 @@
"node": ">=18.0.0"
}
},
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/property-information": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
@@ -6844,6 +7482,43 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/proxy-agent": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
+ "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "http-proxy-agent": "^7.0.1",
+ "https-proxy-agent": "^7.0.6",
+ "lru-cache": "^7.14.1",
+ "pac-proxy-agent": "^7.1.0",
+ "proxy-from-env": "^1.1.0",
+ "socks-proxy-agent": "^8.0.5"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/proxy-agent/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
@@ -6854,6 +7529,24 @@
"once": "^1.3.1"
}
},
+ "node_modules/puppeteer-core": {
+ "version": "23.11.1",
+ "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz",
+ "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@puppeteer/browsers": "2.6.1",
+ "chromium-bidi": "0.11.0",
+ "debug": "^4.4.0",
+ "devtools-protocol": "0.0.1367902",
+ "typed-query-selector": "^2.12.0",
+ "ws": "^8.18.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/radix3": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz",
@@ -7223,6 +7916,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -7576,6 +8279,17 @@
"npm": ">=10.8.2"
}
},
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
"node_modules/smol-toml": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz",
@@ -7588,6 +8302,36 @@
"url": "https://github.com/sponsors/cyyynthia"
}
},
+ "node_modules/socks": {
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz",
+ "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ip-address": "^10.1.1",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
+ "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "socks": "^2.8.3"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
@@ -7631,6 +8375,18 @@
"integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==",
"license": "MIT"
},
+ "node_modules/streamx": {
+ "version": "2.28.0",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz",
+ "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "events-universal": "^1.0.0",
+ "fast-fifo": "^1.3.2",
+ "text-decoder": "^1.1.0"
+ }
+ },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -7788,6 +8544,33 @@
"node": ">=6"
}
},
+ "node_modules/teex": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
+ "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "streamx": "^2.12.5"
+ }
+ },
+ "node_modules/text-decoder": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
+ "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "b4a": "^1.6.4"
+ }
+ },
+ "node_modules/through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
@@ -7861,8 +8644,8 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD",
- "optional": true
+ "devOptional": true,
+ "license": "0BSD"
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
@@ -7876,6 +8659,13 @@
"node": "*"
}
},
+ "node_modules/typed-query-selector": {
+ "version": "2.12.2",
+ "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
+ "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/typesafe-path": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/typesafe-path/-/typesafe-path-0.2.2.tgz",
@@ -7919,6 +8709,17 @@
"integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==",
"license": "MIT"
},
+ "node_modules/unbzip2-stream": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
+ "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.2.1",
+ "through": "^2.3.8"
+ }
+ },
"node_modules/uncrypto": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
@@ -8706,6 +9507,28 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
+ "node_modules/ws": {
+ "version": "8.21.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
+ "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/xxhash-wasm": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz",
@@ -8812,6 +9635,17 @@
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ },
"node_modules/yocto-queue": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
diff --git a/package.json b/package.json
index 0d46eae..21bf522 100644
--- a/package.json
+++ b/package.json
@@ -22,11 +22,13 @@
"check:quickstart": "node scripts/checkQuickstart.mjs",
"check:reference": "node scripts/checkReference.mjs",
"check:sidebar": "node scripts/checkSidebar.mjs",
+ "check:screens": "node scripts/checkScreens.mjs",
"play:datasafety": "node scripts/playDataSafety.mjs",
"beta": "node scripts/beta.mjs",
"test": "node --test test/beta.test.mjs test/legal.test.mjs",
"brand:assets": "node scripts/buildBrandAssets.mjs",
- "verify": "npm run check:sidebar && npm run check:tokens && npm run check:brand && npm run check:datasafety && npm run check && npm test && npm run build && npm run check:links && npm run check:facts && npm run check:quickstart && npm run check:reference"
+ "screens:capture": "node scripts/captureScreens.mjs",
+ "verify": "npm run check:sidebar && npm run check:screens && npm run check:tokens && npm run check:brand && npm run check:datasafety && npm run check && npm test && npm run build && npm run check:links && npm run check:facts && npm run check:quickstart && npm run check:reference"
},
"dependencies": {
"@astrojs/node": "^11.1.4",
@@ -40,7 +42,8 @@
"devDependencies": {
"@astrojs/check": "^0.9.10",
"opentype.js": "^2.0.0",
- "yaml": "^2.8.1",
- "typescript": "^6.0.3"
+ "puppeteer-core": "^23.11.1",
+ "typescript": "^6.0.3",
+ "yaml": "^2.8.1"
}
}
diff --git a/public/screens/admin-appearance.webp b/public/screens/admin-appearance.webp
new file mode 100644
index 0000000..5b5d7ba
Binary files /dev/null and b/public/screens/admin-appearance.webp differ
diff --git a/public/screens/admin-dashboard.webp b/public/screens/admin-dashboard.webp
new file mode 100644
index 0000000..5db7f52
Binary files /dev/null and b/public/screens/admin-dashboard.webp differ
diff --git a/public/screens/admin-modules.webp b/public/screens/admin-modules.webp
new file mode 100644
index 0000000..a81ed3f
Binary files /dev/null and b/public/screens/admin-modules.webp differ
diff --git a/public/screens/admin-shard.webp b/public/screens/admin-shard.webp
new file mode 100644
index 0000000..9c3ee3d
Binary files /dev/null and b/public/screens/admin-shard.webp differ
diff --git a/public/screens/admin-users.webp b/public/screens/admin-users.webp
new file mode 100644
index 0000000..8c259c8
Binary files /dev/null and b/public/screens/admin-users.webp differ
diff --git a/public/screens/app-account.webp b/public/screens/app-account.webp
new file mode 100644
index 0000000..4b5f028
Binary files /dev/null and b/public/screens/app-account.webp differ
diff --git a/public/screens/app-drawer.webp b/public/screens/app-drawer.webp
new file mode 100644
index 0000000..7800ddb
Binary files /dev/null and b/public/screens/app-drawer.webp differ
diff --git a/public/screens/app-home.webp b/public/screens/app-home.webp
new file mode 100644
index 0000000..ab08798
Binary files /dev/null and b/public/screens/app-home.webp differ
diff --git a/public/screens/app-market.webp b/public/screens/app-market.webp
new file mode 100644
index 0000000..dfa5450
Binary files /dev/null and b/public/screens/app-market.webp differ
diff --git a/public/screens/app-shard.webp b/public/screens/app-shard.webp
new file mode 100644
index 0000000..75d01cf
Binary files /dev/null and b/public/screens/app-shard.webp differ
diff --git a/public/screens/app-wiki.webp b/public/screens/app-wiki.webp
new file mode 100644
index 0000000..8101948
Binary files /dev/null and b/public/screens/app-wiki.webp differ
diff --git a/public/screens/guilds.webp b/public/screens/guilds.webp
new file mode 100644
index 0000000..3e07ec7
Binary files /dev/null and b/public/screens/guilds.webp differ
diff --git a/public/screens/houses.webp b/public/screens/houses.webp
new file mode 100644
index 0000000..07b0800
Binary files /dev/null and b/public/screens/houses.webp differ
diff --git a/public/screens/marketplace.webp b/public/screens/marketplace.webp
new file mode 100644
index 0000000..eb8212f
Binary files /dev/null and b/public/screens/marketplace.webp differ
diff --git a/public/screens/news.webp b/public/screens/news.webp
new file mode 100644
index 0000000..b8efbc9
Binary files /dev/null and b/public/screens/news.webp differ
diff --git a/public/screens/shard-status.webp b/public/screens/shard-status.webp
new file mode 100644
index 0000000..7923977
Binary files /dev/null and b/public/screens/shard-status.webp differ
diff --git a/public/screens/spawn-atlas.webp b/public/screens/spawn-atlas.webp
new file mode 100644
index 0000000..d82b851
Binary files /dev/null and b/public/screens/spawn-atlas.webp differ
diff --git a/scripts/captureScreens.mjs b/scripts/captureScreens.mjs
new file mode 100644
index 0000000..63ababc
--- /dev/null
+++ b/scripts/captureScreens.mjs
@@ -0,0 +1,189 @@
+#!/usr/bin/env node
+/**
+ * captureScreens.mjs — retakes the screenshots in `src/data/screens.mjs`.
+ *
+ * PLAN.md §13 phase 9, D4 / D45.
+ *
+ * node scripts/captureScreens.mjs # every web screen
+ * node scripts/captureScreens.mjs shard-status admin-users
+ * RG_DEMO=http://localhost:3000 node scripts/captureScreens.mjs
+ *
+ * ---------------------------------------------------------------------------------------
+ * AN AUTHORING TOOL, LIKE buildBrandAssets.mjs — NOT A CHECK
+ * ---------------------------------------------------------------------------------------
+ * This never runs in CI and CI never needs it: its output is committed, because the site
+ * must build from a clean checkout with no game server, no database and no browser. What
+ * CI runs is `checkScreens.mjs`, which only reads the files this produced.
+ *
+ * It exists because D4 asks for real screenshots of a real deployment, and the way real
+ * screenshots rot is that the recipe for taking them lives in somebody's memory. The rig
+ * is written down in PLAN.md §13; the framing — route, viewport, scroll offset, whether to
+ * sign in — is written down in `screens.mjs`; and this turns the two into files.
+ *
+ * ---------------------------------------------------------------------------------------
+ * WHY puppeteer-core AND NOT puppeteer
+ * ---------------------------------------------------------------------------------------
+ * `puppeteer` downloads its own Chromium — a hundred-odd megabytes fetched on every clean
+ * install of a repository that needs a browser once per redesign. `puppeteer-core` drives
+ * a Chrome that is already on the machine, which every machine that can look at this site
+ * has. Point `RG_CHROME` at it if it is somewhere unusual.
+ *
+ * ---------------------------------------------------------------------------------------
+ * WHY IT SIGNS IN THROUGH THE API RATHER THAN THE LOGIN FORM
+ * ---------------------------------------------------------------------------------------
+ * The administration screens need a session, and typing into the login form is the part of
+ * a browser script most likely to break on a redesign — a moved field, a renamed button, a
+ * React input that ignores synthetic typing. The session cookie is the only thing actually
+ * wanted, so this asks the API for one from inside the page and lets the browser store it.
+ * If that call stops returning 200 the script says so and stops, rather than quietly
+ * screenshotting a login screen twelve times.
+ */
+
+import { existsSync, mkdirSync, readdirSync } from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import sharp from 'sharp';
+
+import { screens, screensOf, WEB } from '../src/data/screens.mjs';
+
+const HERE = path.dirname(fileURLToPath(import.meta.url));
+const OUT = path.join(HERE, '..', 'public', 'screens');
+
+const BASE = (process.env.RG_DEMO || 'http://localhost:3000').replace(/\/+$/, '');
+const USER = process.env.RG_ADMIN_USER || 'demoadmin';
+const PASS = process.env.RG_ADMIN_PASS || 'DemoReview!2026';
+
+/** Where Chrome usually is, per platform. First hit wins; `RG_CHROME` beats all of them. */
+const CHROME_CANDIDATES = [
+ process.env.RG_CHROME,
+ 'C:/Program Files/Google/Chrome/Application/chrome.exe',
+ 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
+ '/usr/bin/google-chrome',
+ '/usr/bin/chromium',
+].filter(Boolean);
+
+const wanted = process.argv.slice(2).filter((arg) => !arg.startsWith('-'));
+const todo = screensOf('web').filter((shot) => wanted.length === 0 || wanted.includes(shot.id));
+
+if (todo.length === 0) {
+ const known = screens.map((shot) => shot.id).join(', ');
+ console.error(`Nothing to capture. Known ids: ${known}`);
+ process.exit(1);
+}
+
+const chrome = CHROME_CANDIDATES.find((candidate) => existsSync(candidate));
+
+if (!chrome) {
+ console.error(
+ 'No Chrome found. Set RG_CHROME to the browser executable — this script drives an\n' +
+ 'installed Chrome rather than downloading one (see the header).',
+ );
+ process.exit(1);
+}
+
+const puppeteer = (await import('puppeteer-core')).default;
+
+mkdirSync(OUT, { recursive: true });
+
+const browser = await puppeteer.launch({
+ executablePath: chrome,
+ headless: 'new',
+ defaultViewport: { ...WEB.viewport, deviceScaleFactor: WEB.scale },
+ // Scrollbars are the browser's furniture, not the product's, and a colour profile that
+ // is not sRGB makes the palette in a screenshot disagree with the palette on the page.
+ args: ['--hide-scrollbars', '--force-color-profile=srgb'],
+});
+
+/**
+ * One page per privilege level rather than signing in and out around each shot: signing
+ * out is the step that gets forgotten, and a public page captured with an admin session
+ * shows a navigation bar the public never sees.
+ */
+const anon = await browser.newPage();
+const admin = await browser.newPage();
+
+await admin.goto(BASE, { waitUntil: 'domcontentloaded' });
+
+const status = await admin.evaluate(
+ async (username, password) => {
+ const res = await fetch('/api/v1/auth/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({ username, password }),
+ });
+ return res.status;
+ },
+ USER,
+ PASS,
+);
+
+if (status !== 200) {
+ console.error(
+ `Could not sign in as "${USER}" at ${BASE} (HTTP ${status}).\n` +
+ 'Seed the demo first — see PLAN.md §13 phase 9 and scripts/seedDemo.mjs.',
+ );
+ await browser.close();
+ process.exit(1);
+}
+
+let failures = 0;
+
+for (const shot of todo) {
+ const page = shot.admin ? admin : anon;
+ const url = BASE + shot.route;
+
+ try {
+ await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 });
+
+ if (shot.scrollY) {
+ await page.evaluate((y) => window.scrollTo(0, y), shot.scrollY);
+ }
+
+ // Live pages settle after their first paint: a shard panel fills in from an event
+ // stream, a list re-sorts once its data lands. A second is cheap and the difference
+ // between a screenshot of the product and a screenshot of its loading state.
+ await new Promise((resolve) => setTimeout(resolve, 1200));
+
+ const png = await page.screenshot({ type: 'png' });
+ const file = path.join(OUT, `${shot.id}.webp`);
+
+ // Quality 82 is where UI text stops visibly softening; the files land near 150 KB,
+ // which is what makes a page with five of them still a page and not a download.
+ await sharp(png).webp({ quality: 82 }).toFile(file);
+
+ const meta = await sharp(file).metadata();
+
+ if (meta.width !== WEB.width || meta.height !== WEB.height) {
+ console.error(
+ ` ! ${shot.id}: got ${meta.width}x${meta.height}, expected ${WEB.width}x${WEB.height}`,
+ );
+ failures++;
+ continue;
+ }
+
+ console.log(` + ${shot.id.padEnd(18)} ${shot.route.padEnd(20)} ${meta.width}x${meta.height}`);
+ } catch (err) {
+ console.error(` ! ${shot.id}: ${err.message}`);
+ failures++;
+ }
+}
+
+await browser.close();
+
+// A file left behind by a screen that has since been renamed or dropped is a file the
+// site still ships and nothing points at. Say so; do not delete somebody's work silently.
+if (wanted.length === 0) {
+ const declared = new Set(screensOf('web').map((shot) => `${shot.id}.webp`));
+ const phones = new Set(screensOf('phone').map((shot) => `${shot.id}.webp`));
+ const orphans = readdirSync(OUT).filter((name) => !declared.has(name) && !phones.has(name));
+
+ if (orphans.length > 0) {
+ console.log(`\nNot declared in screens.mjs, left alone: ${orphans.join(', ')}`);
+ }
+}
+
+console.log(`\n${todo.length - failures} captured, ${failures} failed.`);
+process.exit(failures > 0 ? 1 : 0);
diff --git a/scripts/checkScreens.mjs b/scripts/checkScreens.mjs
new file mode 100644
index 0000000..01c607b
--- /dev/null
+++ b/scripts/checkScreens.mjs
@@ -0,0 +1,171 @@
+#!/usr/bin/env node
+/**
+ * checkScreens.mjs — the screenshots agree with what the pages say about them.
+ *
+ * PLAN.md §12, §13 phase 9, D45.
+ *
+ * node scripts/checkScreens.mjs
+ *
+ * ---------------------------------------------------------------------------------------
+ * WHAT IT PROVES, AND WHY EACH ONE IS WORTH A CHECK
+ * ---------------------------------------------------------------------------------------
+ * 1. EVERY DECLARED SCREEN HAS A FILE. A missing image is invisible in review — the page
+ * still builds, still lays out, and only a reader sees the broken frame.
+ *
+ * 2. EVERY FILE IS THE DECLARED SIZE. `width` and `height` reach the markup as intrinsic
+ * attributes, and an attribute that disagrees with the file is a page that jumps as the
+ * image decodes. It also catches a re-capture taken at the wrong viewport, which looks
+ * fine on its own and wrong beside the others.
+ *
+ * 3. NOTHING IN public/screens IS ORPHANED. A capture that stopped being referenced is a
+ * file the container still ships and nobody looks at — and, worse, one that never gets
+ * retaken, so it silently becomes the oldest thing in the repository.
+ *
+ * 4. EVERY DECLARED SCREEN IS ACTUALLY USED. The mirror of 3: an entry in `screens.mjs`
+ * that no page renders is a capture being maintained for nothing. Usage is a literal
+ * search for the id across `src/`, which is how both readers of the data refer to one —
+ * `
The autumn patch is live. The headline change is that every player ' + + 'vendor on the shard is now searchable from this site — the marketplace ' + + 'page reads the same live feed the game does, so a listing appears within a minute ' + + 'of being priced.
We also fixed the house decay timer resetting when a ' + + 'co-owner logged in. That bug had been quietly keeping condemned houses alive since ' + + 'spring.
Full notes are on the wiki.
', + published: true, + }, + { + category: 'news', + title: 'The Harvest Moon festival opens this weekend', + excerpt: + 'Three days of gatherings at the crossroads, with a champion spawn on the last ' + + 'night. Everyone is welcome, no signup needed.', + body: + 'The Harvest Moon festival runs from Friday evening to Sunday night at the ' + + 'crossroads north of town. There is no signup and no entry fee — turn up.
' + + 'Saturday is the market day; bring anything you want to sell and we will set out ' + + 'extra vendor stalls. Sunday night closes with a champion spawn, which will be ' + + 'announced in game and on the shard status page here.
', + published: true, + }, + { + category: 'news', + title: 'Scheduled maintenance, Tuesday 03:00 UTC', + excerpt: + 'About twenty minutes of downtime for a server restart and a world save. The site ' + + 'stays up throughout.', + body: + 'We are restarting the shard on Tuesday at 03:00 UTC for a world save and a ' + + 'server update. Expect about twenty minutes of downtime.
This site stays up ' + + 'while the shard is down — the status panel will simply show the shard as offline, ' + + 'and the marketplace and atlas will show their last known state.
', + published: true, + }, + { + category: 'five-on-friday', + title: 'Five on Friday: the ones who keep the roads clear', + excerpt: + 'Five players who spent the week doing unglamorous work, and what they were up to.', + body: + 'Five people who made the week better for everybody else:
A quiet, productive month. The vendor search shipped, the wiki gained four ' + + 'guides, and the guild boards now update on the site within a minute of a change in ' + + 'game.
Next month we are looking at the champion boards and at making the ' + + 'atlas easier to read on a phone.
', + published: true, + }, +]; + +// ── The wiki ─────────────────────────────────────────────────────────────────────────── +// +// One category and four pages, because the wiki index screenshot needs a category with +// enough in it to look like a wiki rather than a placeholder. +const WIKI_CATEGORY = { + slug: 'guides', + title: 'Guides', + description: 'How things work here, written by the people who play here.', +}; + +const WIKI_PAGES = [ + { + slug: 'getting-started', + title: 'Getting started', + excerpt: 'What to install, how to connect, and the first hour.', + body: + 'You need a game client and an account. Make the ' + + 'account on this site — the shard accepts accounts created here, and it saves you ' + + 'typing your password into a chat window.
Start in ' + + 'town, take the newcomer quest, and do not sell your starting tools. If you get ' + + 'stuck, ask in Discord: somebody is usually around.
', + }, + { + slug: 'player-vendors', + title: 'Player vendors', + excerpt: 'How to hire one, how to price, and how the site search finds you.', + body: + 'Any house you own or co-own can hold vendors. Hire one ' + + 'from an innkeeper and place it inside.
Everything a ' + + 'vendor holds is published to the marketplace on this site within about a minute, ' + + 'including the price and the house it stands in. If a listing looks stale, the ' + + 'shard was probably down when you priced it — it will correct itself on the next ' + + 'sweep.
', + }, + { + slug: 'housing-and-decay', + title: 'Housing and decay', + excerpt: 'Placement rules, the decay timer, and what IDOC actually means here.', + body: + 'Houses can be placed anywhere the client allows, with the ' + + 'usual clearance rules. There is no lottery.
A house decays if ' + + 'nobody with access logs in for long enough. The site lists houses approaching ' + + 'collapse on the housing page, which is the same data the game uses — not a ' + + 'prediction.
', + }, + { + slug: 'community-rules', + title: 'Community rules', + excerpt: 'The short version: do not be the reason somebody stops playing.', + body: + 'Every moderation action can be appealed from your account page. ' + + 'An appeal is read by somebody who was not involved in the original action.
', + }, +]; + +// ── HTTP plumbing ────────────────────────────────────────────────────────────────────── +// +// One cookie jar, because the session is a cookie and `fetch` has no jar of its own. Only +// the value of the auth cookie matters, so this keeps exactly that. + +let cookie = ''; +let created = 0; +let existed = 0; + +function keepCookies(res) { + const raw = res.headers.getSetCookie?.() ?? []; + for (const line of raw) { + const [pair] = line.split(';'); + if (pair.trim()) cookie = pair.trim(); + } +} + +async function call(method, path, body) { + const res = await fetch(`${API}${path}`, { + method, + headers: { + 'Content-Type': 'application/json', + ...(cookie ? { Cookie: cookie } : {}), + }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }); + keepCookies(res); + const text = await res.text(); + let data = null; + try { + data = text ? JSON.parse(text) : null; + } catch { + data = text; + } + return { ok: res.ok, status: res.status, data }; +} + +function say(mark, what) { + console.log(` ${mark} ${what}`); + if (mark === '+') created += 1; + if (mark === '=') existed += 1; +} + +function fail(what, res) { + console.error(`\n ! ${what} failed — HTTP ${res.status}`); + console.error(` ${JSON.stringify(res.data)?.slice(0, 400)}`); + process.exitCode = 1; +} + +// ── The steps ────────────────────────────────────────────────────────────────────────── + +async function login() { + const res = await call('POST', '/auth/login', { username: ADMIN_USER, password: ADMIN_PASS }); + if (!res.ok) { + console.error( + `\nCould not log in as "${ADMIN_USER}". Create the admin first, from the website repo:\n` + + ` cd website/server && DB_NAME=- Captured against a real deployment with real content, not mocked up. The app takes - its colours, type and navigation from the site it is connected to, so these show one + Captured against a real deployment with real content, not mocked up. The app takes its + colours, type and navigation from the site it is connected to, so these show one community's app rather than a neutral one.
@@ -72,10 +47,10 @@ const shots: Shot[] = []; {shots.map((shot) => (What it looks like
++ A demo deployment with a real shard behind it. The gold supply, the uptime and the online + count are read from the game server over the bridge — none of it is typed in, and the page + is honest about a quiet realm. +
+ +