a993b872acfb47969b7ce792b3cb1d8b61845e6e
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| f499f2b72b |
docs(journey): phase 7 — the installation path and administration
All checks were successful
PR checks / checks (pull_request) Successful in 9m25s
Twenty documentation pages: Getting started (7) and Administration (13), the journey no existing document owns end to end because the repositories are organised by component and an operator is not. Four decisions of record, taken before anything was written (D34–D37, PLAN.md §10 "How phase 7 built the documentation journey"): - D34 one PR for all twenty pages. - D35 the install page is SELF-CONTAINED: it prints a complete Compose file and a complete .env that an operator copies without visiting another repository. That is a copy of somebody else's file, so it is checked rather than trusted — scripts/checkQuickstart.mjs re-reads website main:docker-compose.yml and main:.env.example over the Gitea API and fails on any disagreement, in both directions: a value that drifts fails, and a service or variable that appears upstream fails until it is either included or recorded as deliberately omitted with a reason. Its first run found two stale entries. - D36 every Administration screen was walked on a real deployment before it was described — the rig being the quickstart itself, against the published image, so one run proved the install page and produced the detail the admin pages needed. - D37 a thirteenth Administration page, Content, so that every admin nav row has a home without organising the docs by the app's menu. What the live deployment disproved, all three now documented: - The documented Compose deploy does not boot. SECRET_ENC_KEY is required in production (utils/secretBox.js throws at require time) and is missing from website's ROOT .env.example — the file Compose reads. It is present in server/.env.example, which is why dev never hits it. The quickstart carries it, declared as an upstream omission so the check fails the day it is fixed. - The installer points operators at a screen that no longer exists: it prints <site>/admin/shard, and INSTALL.md §5 repeats it, but since the module cutover the screen is /admin/uo/link. Both the binary and the guide are stale. - The admin Restart button opens a window.confirm whose text is the honest warning that a deployment with no supervisor does not come back — which is why `restart: unless-stopped` is called out as load-bearing rather than left as boilerplate. And the defect only a look found, three phases running: the .env block's prose promised that every highlighted line must be changed, while `mark` given the variable names highlighted the names alone and left the values unmarked. Every check passed on a page that was wrong about its own highlighting. verify green: 890 internal links, 52 branch links, 19 facts, 59 quickstart checks, 0 astro-check errors. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| a2faf07104 |
feat(legal): phase 6 — the privacy policy and the terms
All checks were successful
PR checks / checks (pull_request) Successful in 55s
PLAN.md §9. Builds /privacy and /terms, links them from the footer on every page,
and generates the Play Data Safety notes from the same inventory the policy renders.
Four decisions taken by the org lead before either page was written, recorded in
§9 under "How phase 6 built the legal pages":
D30 DNS-only records, so the reverse proxy on the host keeps the only access
log. Described qualitatively — the retention belongs to the proxy, and a
policy that quotes a number the deployment does not enforce is worse than
one that does not.
D31 Eighteen or older. Above the children's-consent threshold everywhere in the
EEA, so consent works with no parental-consent machinery this form could not
honestly operate. Four surfaces render it from src/data/legal.mjs, and every
one says plainly that nothing verifies it.
D32 No governing-law clause. Nothing of value is contracted for here.
D33 PLAY_DATA_SAFETY.md is generated from src/data/collection.mjs and checked in
CI, so the published policy and the answers given to Google cannot drift.
/privacy is three separately-scoped sections because "we" means three different
parties: this site (one form, no cookies, no third-party requests), the Android app
(we operate no server it talks to — the rows are what the DEVICE holds), and a
self-hosted deployment (the operator is the controller, not us). Every row names the
file it was read out of, because a policy is the document most likely to be written
from a template and least likely to be re-read against the software.
/terms governs only what we run: this site, the beta list, and the APK we publish.
The software is governed by its licence, and a community's deployment by that
community — a terms page claiming authority over every install of a GPL program is
the thing a generated template gets wrong.
Also here:
- the age clause changed CONSENT_TEXT, so CONSENT_VERSION gained a suffix; rows
written from now on carry the new sentence and older rows keep theirs
- PLANNED_ROUTES is now empty — these were its last two entries, and its reverse
check is what forced the deletion; the list stays for phases 7 and 8
- test/legal.test.mjs asserts the structural promises no build check can see,
including that every mapped Play row still answers "not collected, not shared"
- --check normalises line endings: the repo has no .gitattributes and Windows
checkouts are CRLF, so a byte comparison would fail for every Windows developer
while passing in CI
Verified: npm run verify green end to end (tokens, brand, data safety, astro check,
36 tests, build, 214 links, 19 facts), both pages walked in a browser, and neither
overflows at 390px. One defect the checks could not see and a look could: the
retention line was being pushed to the foot of the tallest card in its row, opening
a void in the middle of the short ones.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| 1313e748ae |
feat(beta): phase 5 — the app page and the closed-beta signup
All checks were successful
PR checks / checks (pull_request) Successful in 1m5s
Builds `/app/` and `/beta/`, the SQLite signup store, the rate limiting and the export CLI of PLAN.md §8, and adds this repository's first test suite. Four decisions of record, D26–D29 (§8, "How phase 5 built the app and the beta"): - D26 — the screenshot slot ships empty, reserved for phase 9. §10 promised `/app/` "the 14 existing screenshots"; they are a July trusted-device smoke test against an unseeded dev instance, captured before the theming work, and five of the fourteen are two-factor prompts. Shipping them would break D4. Phase 9 already builds the rig, so it gains an emulator pass. - D27 — the public demo is the tester target. `ConnectScreen.kt` gates the whole app on a validated deployment address, so a tester needs somewhere to point it. The beta therefore waits on the demo VM, and the page says so. - D28 — `/beta` handles its own POST; there is no `/api/beta-signup`. An endpoint cannot report a validation error without JavaScript. §6's diagram is amended. - D29 — the APK and the beta get equal billing, and the APK link is off: `androidApk.serviceable` is false because the published v0.5.0 build does not work. The panel stays and states that plainly rather than being removed. Three mechanisms the plan did not anticipate: - `liveBrand()` — a server-rendered page never passes through the boot rewrite, so `/beta` reads the mounted brand.json itself. Pasting the Play opt-in URL in takes effect on the next request rather than the next restart. - `checkLinks.mjs` derives on-demand routes from `prerender = false` in the source. A PLANNED_ROUTES entry would have been wrong: its reverse check fires when a route has been built, and an on-demand route never produces a file, so the entry could never rot out. - `npm test` — the five existing checks all read built output, and none of this logic appears there. A honeypot can stop working and leave the build identical. Also: `checkFacts.mjs` gains the APK assets and `minSdk`, and learns that RFC 2606 reserved domains are not contact addresses; the D13 rule is otherwise unchanged. Verified end to end against the built server: every outcome renders with no JavaScript, cross-origin POSTs are refused, a mounted opt-in URL appears without a restart, and the export CLI round-trips. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 2d19ee4220 |
feat(marketing): phase 4 — the marketing pages
All checks were successful
PR checks / checks (pull_request) Successful in 9m9s
PLAN.md §13 phase 4: /features/, /architecture/, /modules/, /integrations/, and /community/ — plus the two scope items the phase table never assigned to anyone. Six decisions taken by the org lead before coding, recorded in PLAN.md §10 as D20-D25: - D20 /features/ is the homepage's list with a `detail` line, not a second list. One data file, two renderings, so they cannot disagree about what exists. - D21 /architecture/ draws reasons, not reference: three new inline SVGs, one per boundary. No endpoint tables, no config keys — those are phase 8's and stay canonical in docs/. - D22 The deliberate absences of §2 become one tagged data file, rendered on the three pages that promise them. - D23 Phase 4 absorbs /community/ (specified in §10 and §14 N3, linked from the header since phase 1, built by no phase) and checkLinks.mjs. - D24 `needsModule`: writing the Teams detail exposed a false claim phase 3 shipped. Teams are module-sourced only — teams.module_id is NOT NULL, there is no create route, sync is gated on providerModuleId() — so the Community group no longer says a bare core does all of it. - D25 The per-capability demo affordance brand.json had promised since phase 2 is a deep link, filled at boot from data-demo-path. checkLinks.mjs reads the built HTML rather than src/, because half these links are assembled from data files and template literals. Its PLANNED_ROUTES list is checked in both directions, so it cannot rot into a permanent exemption. applyBrand.mjs gained a pass that recomputes deep links from their immutable path, making it idempotent and reversible; checkBrand.mjs lifts that pattern out and runs it against the stock markup so the two cannot drift. Both proved against a real mount, in both directions. Fixes a cascade bug the checks could not see: [data-demo-url=''] and a scoped component class are both specificity 0,1,0, so .demo-link's `display` beat the hide rule and twelve links to a nonexistent demo rendered, each resolving to the current page. The rule is now !important. The four diagrams' shared SVG vocabulary moved to src/styles/diagram.css. Verified from a clean checkout: npm ci, all five checks, astro check (0 errors), production build, and a live browser pass at desktop and 390px. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 556dee7355 |
feat(home): phase 3 — the homepage
All checks were successful
PR checks / checks (pull_request) Successful in 49s
Replaces phase 1's scaffold with the real homepage: hero, the data path as
inline SVG, the self-hosted argument, all five capability groups, and the
get-started CTA. Three decisions the org lead took first are recorded in
PLAN.md as D17-D19.
The data path is drawn generically and captioned specifically (D17): the nodes
say "your game server" and "sidecar", the sub-labels and caption name ServUO and
uo-link. The SVG is aria-hidden because the four numbered steps beside it carry
the same path in prose — one telling, not two.
The capability list is data with a check behind it (D18). Every Game-intelligence
item names the module-uo capability slug it comes from, and the build fails if
the page and platform.json disagree either way. That needed a fifteenth fact in
checkFacts.mjs: §12 named the capability list as an externally-sourced fact and
nothing re-read it, so the chain rested on someone remembering. It also found
that the site was omitting two of the module's eight capabilities — guilds and
city governors are now listed, in the page and in §10.
The hero leads with the emblem (D19), derived from whichever logo.png is in
force so one file still changes the hero, header, tab icon and app icon
together.
Also here, both found by standing the build up rather than by review:
- checkBrand.mjs now enforces the demo slot's markup contract. applyBrand.mjs
reveals the demo link by replacing an exact pair of empty attributes; an
attribute inserted between them produces a build where the mount sets a demo
URL, the boot log says nothing and the link never appears. Both halves are
checked and the literal is derived from the expression applyBrand.mjs uses,
so they cannot drift.
- The header nav overflowed at 390px — four links plus the lockup measured
433px against a 390px viewport, so every phone got a horizontally scrolling
page. Phase 1 left this to phase 3 expecting a disclosure control; it got a
wrap instead, because with four links there is nothing to disclose and a
hamburger costs state, script and duplicate markup.
Verified on a clean checkout of this commit: all four checks, astro check, a
production build, a live /brand/* smoke, and a demo URL mounted and reverted.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| fe4abe0ebf |
feat(brand): phase 2 — the branding pipeline
All checks were successful
PR checks / checks (pull_request) Successful in 9m9s
PLAN.md §7: swapping a logo or recolouring the site is a file copy and a container restart, never a rebuild. Phase 2 builds the mechanism and the checks that keep it true. GET /brand/* resolves every file against the mount first and the baked-in defaults second, per file, at stable unhashed URLs with an ETag and a five minute TTL. Nothing goes through Vite, which would fingerprint the names out of the mount's reach. An X-Brand-Source header says which step answered. Three decisions were taken with the org lead (recorded as D14-D16 in §7): D14 — one raster in, every size out. brand-default holds a single logo.png; the header mark at three pixel ratios, both install icons, the apple-touch icon, the favicons and a real multi-resolution favicon.ico are derived on request from whichever logo.png is in force, cached, and limited to an allowlist of sizes. Shipping fifteen precomputed files would have meant an operator producing fifteen to change a mark — and getting a new header with the old favicon. D15 — brand text is applied at boot. Pages are prerendered, so §7's promise about the site name, tagline and links could not hold at render time. npm start now runs scripts/applyBrand.mjs first, rewriting the built HTML from what it last applied to what the mount says. It rewrites from a record in dist/.brand-applied.json rather than from the defaults, because the naive version works exactly once and then silently ignores every later edit. An empty mount is a no-op; removing a mount restores the stock build byte for byte. Verified both ways, plus a second rename. D16 — the header shows the real emblem, replacing phase 1's placeholder glyph, so the site, the product and the Android launcher icon are one mark. It is raster art, so theme.css cannot recolour it; replacing logo.png is how the mark changes. Two defects found and fixed while proving it: The mounted theme.css did not win. Astro emits its own stylesheet after the head markup, so linking the operator's last was not enough and every override was silently a no-op. tokens.css now lives in @layer tokens and the mounted file is unlayered, which takes order out of the mechanism entirely. The documentation was a different site. Starlight builds its own head, so the docs linked a Starlight default /favicon.svg that does not exist here, carried no manifest or OG card, and never loaded the brand stylesheet — a mounted theme recoloured the marketing pages and left the docs stock. A Head override fixes it; half a rebrand looks like a product bug rather than a missed step. brand-default/wordmark.svg and og-image.png are generated by scripts/buildBrandAssets.mjs from the emblem and Cinzel's outlines and are committed, so CI needs neither the artwork nor a font. Type is converted to paths, because an SVG in an <img> can see neither the page's @font-face rules nor fontconfig — the same isolation that broke currentColor in phase 1. Its glyphs are drawn at the origin and translated: opentype.js emits NaN coordinates at a non-zero origin for some glyphs, and a path parser stops at the first malformed command, so the first lockup read "Runic Gate" and looked like a typo rather than a bug. scripts/checkBrand.mjs is the mechanism for the two failures that are otherwise silent: it puts every literal /brand/... URL in the source through the route's own classifier, so a size that is not on the allowlist fails the build instead of 404ing in a browser, and it rejects a brand string short enough that a blind replacement at boot could corrupt a page. Negative-tested three ways before being trusted. It runs in CI ahead of the type check. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| b287728c19 |
ci(facts): use the existing org-level REGISTRY_TOKEN
All checks were successful
PR checks / checks (pull_request) Successful in 9m11s
checkFacts needs to read link, servuo-plugins, website and installer, and the automatic per-run token is scoped to this repo alone. Rather than mint a new secret, the workflow uses REGISTRY_TOKEN, which already exists at the org level with the right permissions. The secret is named for the registry and the script reads GITEA_TOKEN; the mapping stays in the workflow so the script keeps asking for what it actually wants -- a Gitea token -- rather than this org's secret name. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 66187dde5d |
feat(site): phase 1 — the foundation
Some checks failed
PR checks / checks (pull_request) Failing after 4m19s
Astro 7 with the Node adapter, Starlight mounted at /docs, the token file, both self-hosted typefaces, the layout shell, and the two build-time checks from §12. The palette's gold and cyan are sampled from runic-emblem.png rather than guessed, per §11: 494,059 opaque pixels binned by hue, each value annotated with its measured contrast against the ground, and restricted rather than brightened where a ratio fails. - checkTokens.mjs fails the build on any colour literal outside tokens.css, which is what keeps §7's "recolouring is a file copy" promise true. - checkFacts.mjs re-reads all 14 externally-sourced facts from their authorities over the Gitea API and fails on disagreement. It also enforces D13: no email address in the source outside brand-default/brand.json. - Both were negative-tested; neither has ever been allowed to pass by default. §6 asks for output:'server' with per-page prerender=true. Astro 7 expresses the same runtime shape as output:'static' with an adapter, opting individual routes out — so the default is static rather than accidentally server-rendered. Co-Authored-By: Claude <noreply@anthropic.com> |