916921551feca0edaf1e49280ef70121845b37d6
19 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| b86f4cabf0 |
feat(compose): publish on every interface, so the host's own address answers
All checks were successful
PR checks / checks (pull_request) Successful in 9m56s
D55 bound the published port to 127.0.0.1, on the reasoning that TLS terminates
at a proxy on the same host and nothing else has business reaching the container.
That is right for the host this ends up on and wrong for every step before it: a
loopback binding cannot be opened from a browser on another machine, which is the
first thing an operator wants to do — look at the site on the VM's own address,
before DNS exists, before the proxy exists, from a desktop or a phone.
The port line is now "${SITE_BIND_ADDR:-0.0.0.0}:${SITE_HOST_PORT:-4321}:4321",
so http://<vm-ip>:4321 answers out of the box, the way a normal bridge publish
behaves. Which addresses it answers on is a variable rather than an edit:
SITE_BIND_ADDR narrows it to one interface, or back to loopback, without touching
a file that `docker compose pull` replaces. That also retires the "change the port
line yourself" instruction DEPLOY.md had to give a proxy running in another
container or on another machine.
What is given up, said plainly in DEPLOY.md §3.1: on a host with a public address
the default answers on port 4321 from the internet, plain HTTP beside the proxy's
443, with no proxy in the path to set X-Forwarded-For — so signups arriving that
way share one rate-limit bucket. There is no login and no secret behind it, so it
is untidy rather than dangerous, and both remedies are named (firewall the port,
or narrow the binding).
Verified by running it, not only by reading it: `docker compose config` accepts
both bindings and resolves host_ip 0.0.0.0 and 127.0.0.1 respectively; the stack
came up healthy, `docker compose port site 4321` reported 0.0.0.0:4321, and the
site answered 200 on both 127.0.0.1 and the machine's LAN address, still carrying
its own per-page CSP.
Recorded as D59, amending D55. Count of record fifty-nine.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| 92f00ab20a |
fix(image): ship node_modules in three layers, and count them before pushing
All checks were successful
PR checks / checks (pull_request) Successful in 9m47s
The merge that landed phase 12 built its image and could not publish it. `docker push` answered 413 Payload Too Large on one blob and stopped, so the registry stayed empty and `needs: build` meant the deploy never ran — the site was merged and undeployed, and the log said only that a digest was too large. The limit is Cloudflare's, not Gitea's: the instance is proxied, and Cloudflare refuses a request body over 100 MB below Enterprise. A push uploads each layer as one monolithic PUT, so the ceiling is per layer and the rejection happens at the edge, where Gitea never sees it and no Gitea setting can lift it. One layer was over, by nine megabytes: COPY node_modules at 108.8 MB compressed, in a 188.6 MB image whose next largest layer is the 47.6 MB Node base. @pagefind and @img (sharp's libvips) account for it and both are needed at run time — the boot rewrite re-indexes the site and re-derives the brand images — so what could move is where they land, not whether they ship. The build stage moves them aside after `npm prune` and the runtime stage copies them as their own layers: 46.8 + 50.5 + 11.5 MB, and the image is exactly the same total size, the same bytes divided differently. Moving rather than copying twice keeps the three disjoint, so a dependency added later needs no maintenance here. A split is a margin and not a guarantee, so the workflow now counts layers before it pushes: docker save, re-compress anything over 8 MB the way the push would, fail at 90 MB — not 100, the blob is not the only thing in the request — naming the layer and what would have happened. Tested in both directions; on the broken image it reports 108 MB, which is what the registry recorded. Verified by running the built container, not only by measuring it: healthy in ~25s, the mounted brand rewritten across 51 files, 50 pages re-indexed by pagefind, and the favicon served as `x-brand-source: derived:mount` — which is sharp resolving from its new layer. npm run verify is green, all eleven checks and both suites. Recorded as D58; DEPLOY.md gains the symptom and what it means for the host (nothing — the running container is untouched). Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| f2e59a2426 |
feat(delivery): phase 12 — the container, and the defect only a proxy could find
All checks were successful
PR checks / checks (pull_request) Successful in 9m46s
PLAN.md §13 phase 12, the last one. Four decisions of record, D54–D57, taking the count to fifty-seven; recorded in §6, "How phase 12 delivered it". A two-stage Dockerfile, a pull-only docker-compose.yml carrying both bind mounts, .env.example, the workflow that publishes and deploys, CONTRIBUTING.md, the community-health files this was the only repository of the ten to lack, and DEPLOY.md. D54 — a merge deploys, amending D6. build-image.yml pushes runicgateway-site:latest and :sha-<7>, then rolls the container over on the `rgcom` runner out of /opt/runicgateway.com, and waits for the container's own healthcheck rather than for `up -d` to return. D55 — the site runs on its own host behind a generic reverse proxy, so DEPLOY.md states the four requirements rather than one worked example, and the container binds 127.0.0.1 so the safe configuration is the default. D56 — @astrojs/node derives the request protocol from req.socket.encrypted and never reads x-forwarded-proto, so behind a TLS-terminating proxy the browser sends Origin: https://… while the container computes http://… and Astro's CSRF check compares them for equality. Every beta signup, from every visitor, was answered 403. serve.mjs now normalises both forwarded headers, unconditionally — the image should deploy and work. Two assertions in test/headers.test.mjs hold both halves. D57 — DEPLOY.md rather than a README section; SECURITY.md and CODE_OF_CONDUCT.md are pointers to the org's copies rather than copies, because a copy would hard-code the contact address D13 confines to brand.json. Verified: npm run verify green (eleven checks, 36 unit tests, 7 served tests, astro check 0 errors). The image was built and run with both mounts — a mounted brand reached 51 files and all 50 search pages, /brand/* fell back per file, a proxy-shaped signup reached the store, and the export CLI wrote both Play files to the host mount. docker compose config caught a YAML trap in the healthcheck: a block sequence reads the `: ` in `r.ok ? 0 : 1` as a mapping. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| de9d25bbe7 |
feat(validation): phase 11 — the walk that found what the checks could not
All checks were successful
PR checks / checks (pull_request) Successful in 1m26s
The checks were green before this phase started and are green now. What found anything was the part no script does: fifty pages at three widths in a real browser, a signup walked against its store, and a full brand mount applied and restarted. D51 — the chrome and the head follow the mount; the consent sentence does not. With a complete brand.json mounted, forty-nine pages came back rebranded and /beta did not. applyBrand.mjs rewrites files in dist/client and /beta renders per request, so its HTML never exists as a file to rewrite; liveBrand() was there for exactly that and was used for betaOptInUrl alone. Everything around the form — title, OG tags, header lockup, footer Source and Discord links — came from the shared chrome, and the shared chrome was baked. renderBrand() picks by Astro.isPrerendered, in one place, so the other forty-nine keep taking the value the boot rewrite will replace. CONSENT_TEXT stays a constant: it is stored verbatim in a person's row, so following a mounted name would change the recorded text of a consent already given. D52 — the documentation half gets phase 10's skip-link fix. Starlight's skip link targets the page <h1>, which is no more focusable than the <main> phase 10 fixed, so following it moved the viewport and not the focus on forty pages. A PageTitle override adds tabindex="-1". D53 — no twelfth check. The external-link sweep (73 of 74 alive) and the brand-mount walk stay throwaway scripts: one would make the build depend on other people's uptime and the other needs Chrome on the runner. Also recorded and deliberately not fixed: Starlight's heading anchor links measure under 24px at 390, and are exempt under SC 2.5.8's Equivalent clause because the mobile table of contents links to every one of the same anchors. npm run verify green — fourteen steps, both suites, all eleven checks. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| e71ff4acd4 |
feat(polish): phase 10 — search, accessibility, SEO and a real CSP
All checks were successful
PR checks / checks (pull_request) Successful in 9m36s
PLAN.md §13 phase 10, with four decisions of record — D47-D50, taking the count
to fifty. Three were straightforward; the CSP turned into the phase's real work,
because the thing meant to be a configuration flag was broken in a dependency and
broken silently.
D47 — search reaches the marketing pages, and the header gets a box.
Base.astro marks its <main> as a Pagefind body, so all ten join the index the
docs already query, and Search.astro opens it in a <dialog>. Nothing is fetched
until the dialog is opened (the bundle is 120 kB and these pages otherwise ship
almost no JavaScript). Pagefind titles a result from the first <h1>, and these
pages have editorial ones — "The app for a deployment you already use" — so the
index is given the page's short name instead. applyBrand.mjs now re-indexes after
a rewrite, closing a note phase 2 left for this phase.
D48 — the CSP is a real response header, sent by the container. Not a <meta>,
which ignores frame-ancestors, and not advice for someone's reverse proxy, which
puts the strictest promise in §6 outside what this repo tests. Three things
fought it, all the same shape — correct build, broken page, no error:
* Astro does not hash <script is:inline>, and Starlight ships six per docs
page, so the first build with CSP on had a strict header and a dead theme
switcher. The hashes are now generated into src/config/cspHashes.mjs and
checkCsp.mjs verifies every inline block against its own page's policy.
* Expressive Code writes ~3,700 inline style ATTRIBUTES, which cannot be
hashed, hence style-src-attr 'unsafe-inline' — scoped to that directive, so
script-src is untouched.
* @astrojs/node matched a request to a policy with pathname.includes(), a
substring test: /modules/ was served /docs/modules/building-a-module's
policy and rendered with its own stylesheet refused. scripts/serve.mjs keeps
the same _headers.json and matches by equality; test/headers.test.mjs starts
the server and reads the responses, because nothing that reads dist/ can see
this.
D49 — robots.txt allows everything and names the sitemap (there was no way to
find it: no robots.txt, and D9 rules out a search console). D50 — Organization
and SoftwareApplication, no ratings and no docs-wide Article markup.
checkA11y.mjs is the eleventh check: seven structural rules over all fifty pages,
verified by breaking each in turn. The walk at 390/768/1280 found no overflow
anywhere, the CSP violations above, a 17x17 consent checkbox (WCAG 2.2 SC 2.5.8
wants 24), and a skip link that moved the scroll but not the focus.
npm run verify is green: fourteen steps, both test suites, all eleven checks.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| 8b6efd5c0a |
feat(screens): retake the two frames a signed-in character changes
All checks were successful
PR checks / checks (pull_request) Successful in 1m13s
The org lead signed a character into the shard by hand — the step automation could not reach — so shard-status and app-shard were retaken. The shard page now reads 1 player online in Britain, and the app's card agrees. Two things worth noticing in the retake. Presence reaches the public page as counts and regions rather than names, which is the visibility framework working unprompted. And the guild board's online column did not move: it refreshes only when a guild's signature changes, which is the stale-roster defect wearing a different hat. A sixth defect, visible in the shipped phone capture: the app interpolates a count into a fixed plural and says "1 players online". Raised, not fixed — it wants a plurals resource in the app. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| c29ec94f46 |
feat(screens): phase 9 — real screenshots, from a real shard
All checks were successful
PR checks / checks (pull_request) Successful in 1m25s
D4 asked for screenshots of the review stack rather than placeholders. Seventeen
of them: eleven of the site in a browser, six of the app on a phone, all from one
demo deployment wired to a running ServUO shard over a real sidecar, captured on
one day (D42).
The deployment is branded "Runic Gateway Demo" rather than a real community (D43),
and the captures sit beside the claims they support — the homepage, /features/, and
five of the administration pages phase 7 could describe but not show (D44).
The rig is committed rather than remembered (D45):
scripts/seedDemo.mjs content, by driving the site's own API — never SQL,
because a row the product could not have produced is
a screenshot of a product that does not exist
src/data/screens.mjs every capture: route, viewport, scroll, alt, caption
scripts/captureScreens.mjs npm run screens:capture
scripts/checkScreens.mjs the ninth check script, in CI
Shard-side dressing is servuo-plugins' scaffolding (D46), never deployed.
The rig found five things nothing else had. One is fixed upstream — a fresh
module-uo install pinned wire protocol 3 against a sidecar speaking 4, released as
v1.0.2, which this repo's own facts check then caught in platform.json. Four are
raised as product observations and worked around in the rig: a renamed guild
member never reaches the site, a guild deleted while the shard is down is a ghost
row forever, "Houses in danger" cannot show a house that was already collapsing,
and the app's news list prints raw ISO timestamps.
Players online reads 0. Logging a character in needs a UO client driven by hand,
and that is where this stopped — PLAN.md §10 says exactly why, and how to retake
the two frames that would change.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| d89ce06bb8 |
docs(builder): phase 8 — modules, architecture and reference
All checks were successful
PR checks / checks (pull_request) Successful in 1m13s
Twenty pages completing the tree section 10 planned: Modules (8), Architecture
(5) and Reference (7). Four decisions, D38-D41, recorded in PLAN.md section 10.
D39 is the one that shaped the phase. Section 1 forbids re-specifying a
contract, and a Reference section is exactly where that rule is most tempting to
break, so the line is drawn at names: every environment variable, config key,
installer command, visibility rung and canonical document is listed with one
terse line saying what it is FOR, while shapes, semantics and every "why" stay
in the canonical document.
That is only safe because the names are checked. checkReference.mjs compares six
enumerations against the repositories that own them, over the Gitea API, as set
comparisons in BOTH directions -- and the second direction is the one that earns
its keep, because a reference page does not usually rot by describing something
that vanished, it rots by quietly not mentioning what was added since.
The check went green on its first run, which is the least trustworthy possible
outcome, so it was verified by breaking it: seven mutations, all caught. The one
worth keeping is the visibility ladder REORDERED with its membership unchanged
-- it is a security boundary, and a set comparison alone would have passed it.
D41 turns plannedSidebar from a checklist into a checked invariant, and finding
out why was the phase's first defect: it had already drifted, because phase 7
added the Content page under D37 and never updated the list. Nothing failed,
because nothing read it. checkSidebar.mjs now asserts the two trees agree on
groups, labels and order -- order because the order of Getting started IS the
installation path.
Two more things the writing found. PLAN.md's page count was wrong and had been
since section 10 was written ("roughly 38, 37 planned" for a tree of forty).
And module.json's `mounts` and the SPA's paths are different mechanisms that no
single document stated plainly -- module-uo declares admin: ["/shard",
"/uo-link"] while its screen lives at /admin/uo/link, because API routes are
deliberately NOT namespaced while SPA routes are. That is precisely the
distinction the installer got wrong in v0.1.0, and it now has a named home.
D40: the docs link to /architecture/'s drawn diagrams rather than importing
them. Those components carry marketing chrome and depend on diagram.css, which
Starlight does not load; the docs use text diagrams, which paste into an issue.
npm run verify green: 40 pages across 5 groups agree with plannedSidebar, 2390
internal links resolve, 123 repository links point at a branch, 19 facts, 59
quickstart checks, 22 reference enumerations, astro check 0 errors, 36 tests.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| e8cb6061fe |
fix(facts): installer v0.1.1 is released, so the note names a version
All checks were successful
PR checks / checks (pull_request) Successful in 9m22s
Recovering installer's orphan tag published v0.1.1, which moved the platform
under this branch and turned checkFacts red -- the check working exactly as it
should, since a version this site quotes had changed.
FAIL release installer
platform.json says : v0.1.0
installer releases/latest says : v0.1.1
The 500 that orphaned the tag was a race with the tag push one second earlier,
not a structural failure: re-running the workflow took the built-in orphan-tag
recovery path and published all four assets unchanged.
So the stale-path Aside stops saying "v0.1.0 is still the current download",
which is no longer true, and says the durable thing instead -- v0.1.0 prints
the old path, v0.1.1 prints the real one -- which stays correct however many
releases follow. platform.json and the PLAN.md version table move to v0.1.1,
and the phase 7 findings record the pipeline defect as a fourth finding.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| a993b872ac |
fix(docs): clear the quickstart drift the upstream fixes caused
All checks were successful
PR checks / checks (pull_request) Successful in 1m1s
The three defects phase 7 found are fixed and merged: website#163 (SECRET_ENC_KEY missing from the root .env.example, plus BOT_INTERNAL_KEY in the README's "set at least" list) and installer#22 + docs#174 (the handoff printing /admin/shard). website#163 turned checkQuickstart red here, which is precisely what the declaration was built to do -- it fails the moment a declared key appears upstream, so the note describing the omission cannot outlive the defect. The SECRET_ENC_KEY entry is deleted and notInUpstreamEnvExample is now empty; the export stays so the next divergence gets an entry rather than passing quietly. The stale-path Aside on Connect a game server is pinned to v0.1.0 rather than calling the installer permanently wrong, and now says WHY the old path is worse than a 404: the SPA has no route for it, so it redirects to the dashboard and the link looks like it worked. v0.1.0 is still the current download, and not only because releases lag. The release run for installer#22 built every artifact and pushed tag v0.1.1, then took a 500 creating the release -- so the tag is orphaned and no binaries were published. Raised on installer; nothing is worked around here. This also recovers |
|||
| 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> |
|||
| 5f09b66742 |
docs(plan): D13 settles the contact address, and Phase 0 is done
The domain still has no mailbox. Rather than delay the beta on Cloudflare Email Routing, the org lead elected to publish the existing address. - D13 recorded: whitlocktech@gmail.com is the published contact, held only in brand.json so a later privacy@/security@ is a file copy, not a rebuild. - Phase 0 PR 0.10 withdrawn -- SECURITY.md already carries that address. - checkFacts.mjs gains the rule that keeps the promise true: no email address in the source outside brand-default/brand.json. - Phase 0 marked complete (docs#172, docs#173, .profile#4, Module-uo#19). Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| fce3444c6c |
docs(plan): record the org lead's decisions and design against them
Revision 1 ended in five open questions. All are now answered, plus the domain and Gitea-registration facts, so the plan states decisions instead of asking. New section 5 holds the twelve decisions of record. Sections 6-9 design against them: the Astro + Node adapter runtime, branding resolved out of a bind mount against baked defaults, the Android closed-beta signup, and the legal pages the beta cannot ship without. Re-verified every version against the Gitea API and corrected three things revision 1 got wrong: the bundle manifests live at the root of the bundles branch rather than under bundles/, the website publishes no releases at all, and the Android app targets no server of ours - the user enters the address, which is load-bearing for the privacy policy. Phase 0 grows from the four operator-facing documentation fixes to all ten conflicts, plus retiring the personal Gmail that SECURITY.md publishes as the public vulnerability contact. Two design notes worth flagging. Play does not notify testers on the email-list path either, so a site that sends no mail looked blocked - but the closed-testing opt-in URL only works for addresses already on the list, so it is safe to publish on the confirmation screen, with Discord carrying the announcement. And the CSV export is a CLI against the bind mount rather than an admin page, so the site keeps no authenticated surface at all. The demo instance is recorded as planned and out of scope: a Proxmox VM running the full stack including ServUO, restricted and reset hourly. The site reserves its slot now so it costs one line in a bind-mounted file later. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 96f77a87e1 |
docs(plan): the design of record for runicgateway.com
Reconnaissance across the nine RunicGateway checkouts, and the plan for the public site + documentation experience built on top of them. The site is a presentation layer over existing project knowledge: normative contracts stay canonical in docs/ and are linked, never copied. What the site authors is the journey — install, configure, administer, extend — which no existing document owns end to end, because the repos are organised by component and an operator is not. Two things worth reading before the rest: - Section 3 lists eleven places the repositories contradict each other, verified against origin/main rather than resolved quietly. The sharpest is that link/INTEGRATION.md documents the protocol-4 event kinds while its normative section 2 still declares X-UOLink-Version: 3 — one document disagreeing with itself. installer/INSTALL.md still hands operators "Protocol version 3", which is the value they paste into Admin -> Shard, and its own troubleshooting table says a stale number looks exactly like the shard going offline. - Section 4 makes fixing those a Phase 0 that lands before the site quotes anything: four PRs across docs and .profile, none of which touch code or a contract. They correct documentation to match what main already does. Section 1 also records a process failure and the rule taken from it. An earlier draft claimed the platform was on protocol 3 with 4 "not cut over". Every checkout was on a feature branch whose local main ref had never been fetched, so git show main:<path> answered from a months-old blob. Every fact in section 2 is now read from origin/main or the Gitea API, and section 7 turns that into a build-time check instead of a promise. Section 9 leaves five questions open rather than assuming answers. Co-Authored-By: Claude <noreply@anthropic.com> |