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>
201 lines
11 KiB
Markdown
201 lines
11 KiB
Markdown
# runicgateway.com
|
|
|
|
The public marketing and documentation site for [Runic Gateway][org] — the platform that puts a
|
|
private game server's live state on a public website without ever exposing the game to the internet.
|
|
|
|
Two audiences: **server administrators** who want to understand and install the platform, and
|
|
**developers** who want to build modules and integrations for it. A third arrives with the Android
|
|
closed beta: **players**, who want the app.
|
|
|
|
**The design of record is [`PLAN.md`](PLAN.md).** It is not a sketch — it carries the verified
|
|
platform state, the org lead's decisions, the information architecture, and the build phases. Read
|
|
it before changing anything here.
|
|
|
|
**Status: phase 5 of 12 — the app and the beta.** The foundation, the branding pipeline, the
|
|
homepage and the five marketing pages are built, and `/app/` and `/beta/` now join them: a signed
|
|
APK beside the closed-test signup, backed by a SQLite store on a bind mount and an export CLI. Next
|
|
are the legal pages (phase 6) and then the documentation — the installation path, which is the
|
|
priority of the whole project — in phases 7 and 8.
|
|
|
|
---
|
|
|
|
## Running it
|
|
|
|
```bash
|
|
npm install
|
|
npm run dev # http://localhost:4321
|
|
```
|
|
|
|
```bash
|
|
npm run build # → dist/ (prerendered pages + the Node server entry)
|
|
npm start # serve the built site
|
|
```
|
|
|
|
Node 22 LTS or newer.
|
|
|
|
## The checks, and why they are not optional
|
|
|
|
Two of them, both from `PLAN.md` §12. Neither is a linter; each one enforces a promise the site
|
|
makes that would otherwise decay quietly.
|
|
|
|
```bash
|
|
npm run check:tokens # no colour literal outside the token file
|
|
npm run check:brand # the branding pipeline's two quiet failures
|
|
npm run check:datasafety # the Play declaration still matches /privacy
|
|
npm run check # astro check
|
|
npm test # the beta signup's decision path, and the policy data
|
|
npm run build && npm run check:links # every internal link resolves (reads the build)
|
|
GITEA_TOKEN=<token> npm run check:facts # every version agrees with its authority
|
|
npm run verify # all of the above, in that order
|
|
```
|
|
|
|
**`checkFacts.mjs`** re-reads every version, protocol number and bundle tag in
|
|
`src/data/platform.json` from its source of truth over the Gitea API — the sidecar's
|
|
`PROTOCOL_VERSION`, the overlay's `overlay.toml`, the website's `MODULE_API_VERSION`, the installer's
|
|
published bundle manifest, and each repository's latest release — and fails on any disagreement.
|
|
|
|
When the platform moves, this repository goes red so that someone updates the site. **That failure is
|
|
the feature**, the same mechanism and the same intent as the Integration Kit's `checkCoreApi.js`. §1
|
|
of the plan records what it is guarding against: an earlier draft confidently stated the platform was
|
|
on protocol 3, because every checkout in the workspace sat on a feature branch whose local `main` had
|
|
never been fetched.
|
|
|
|
It needs a token — anonymous raw fetches fail on this Gitea instance, and a check that silently skips
|
|
itself is worse than no check at all. It must be able to read the other repositories in the
|
|
organisation, not just this one. CI already has this: the workflow maps the org-level
|
|
`REGISTRY_TOKEN` secret into `GITEA_TOKEN` for that step.
|
|
|
|
**`checkTokens.mjs`** fails the build if a colour literal appears anywhere in `src/` outside
|
|
`src/styles/tokens.css`. §7 promises that recolouring the site is a file copy and a container
|
|
restart; a mounted `theme.css` can only redefine custom properties, so a literal in a component is a
|
|
piece of the site an operator can never reach. Without the check, "one CSS file changes the
|
|
appearance" becomes "one CSS file changes most of the appearance".
|
|
|
|
**`checkBrand.mjs`** guards the two things about the branding pipeline that fail quietly. It puts
|
|
every literal `/brand/...` URL in the source through the route's own classifier, so a template
|
|
asking for a size that is not on the allowlist fails the build rather than 404ing in a browser; and
|
|
it refuses a brand string short enough that replacing it blindly at boot could corrupt a page.
|
|
|
|
**`checkLinks.mjs`** reads `dist/client` rather than `src/`, because half the links these pages
|
|
carry are assembled from data files and template literals and a source scan sees an expression. It
|
|
also refuses a commit permalink into any org repository — those stop tracking the document they name
|
|
without ever 404ing, which is the failure a link checker would otherwise call healthy.
|
|
|
|
**`npm test`** is the one check that reads none of the above. Everything else inspects built output,
|
|
and the beta signup's logic does not appear there: a honeypot can stop working entirely and produce
|
|
a build identical to one where it works. It covers the honeypot, the signed form token, the timing
|
|
window, the per-connection rate limit, the global cap, address validation, idempotent duplicates and
|
|
removal. Run the file by name — `node --test test/` fails on Node 22, which is what CI uses.
|
|
|
|
## The closed-beta signup
|
|
|
|
`/beta` is the only page that renders per request and the only one that writes anything. It handles
|
|
its own POST, so the form works with JavaScript disabled and every outcome renders in the real
|
|
layout. The store is SQLite on the `data/` bind mount; **the raw IP address is never recorded**,
|
|
only a salted hash used to rate-limit.
|
|
|
|
There is no admin page, by design — the tester list is managed from a shell:
|
|
|
|
```bash
|
|
npm run beta -- stats # counts, and where the store lives
|
|
npm run beta -- export # a CSV record + a .txt to paste into Play; marks rows exported
|
|
npm run beta -- export -- --all # everything, including already-exported rows
|
|
npm run beta -- remove someone@example.com
|
|
```
|
|
|
|
| Variable | Default | What it does |
|
|
|---|---|---|
|
|
| `DATA_DIR` | `./data` | The bind mount holding `beta.sqlite` and `exports/` |
|
|
| `BETA_IP_SALT` | random per process | Salts `ip_hash`. Unset means rate limits reset on restart |
|
|
| `BETA_FORM_KEY` | random per process | Signs the form token, so a script must fetch the page before posting |
|
|
| `BETA_TOTAL_CAP` | `500` | Rows above which the form closes and says so |
|
|
| `BETA_PER_HOUR` / `BETA_PER_DAY` | `3` / `24` | Attempts one connection may make |
|
|
|
|
Neither random default is a placeholder to be replaced by a constant: a hard-coded salt would make
|
|
every deployment's hashes identical and therefore reversible by anyone holding this repository.
|
|
|
|
## Branding is bind-mounted data
|
|
|
|
`brand-default/` is baked into the image and always complete. `brand/` is the bind mount and may be
|
|
empty, partial or full. **Every file resolves against the mount first and the defaults second, per
|
|
file**, so overriding only `theme.css` leaves every logo stock and an empty mount produces exactly
|
|
the stock site. Nothing here goes through Vite, which would fingerprint the filenames into the build
|
|
and put them out of the mount's reach.
|
|
|
|
**Rebranding is one file.** `brand-default/` holds a single raster — `logo.png` — and `GET /brand/*`
|
|
derives every size the site asks for from whichever `logo.png` is in force: the header mark at three
|
|
pixel ratios, the install icons, the apple-touch icon, the favicons and a real multi-resolution
|
|
`favicon.ico`. Drop in one file, restart, and the browser tab and the installed icon change with the
|
|
header.
|
|
|
|
**Brand text is applied at boot.** Pages are prerendered, so the site name, tagline and links are
|
|
baked into HTML that a mounted file cannot reach. `npm start` runs `scripts/applyBrand.mjs` first,
|
|
which rewrites the built HTML from what it last applied to what the mount now says — recorded in
|
|
`dist/.brand-applied.json`, so the second edit works as well as the first. An empty mount makes it a
|
|
no-op.
|
|
|
|
**A mounted `theme.css` wins by cascade layer, not by link order.** `tokens.css` is inside
|
|
`@layer tokens`; the mounted stylesheet is unlayered and therefore beats it wherever the browser
|
|
encounters it. Do not "fix" this by reordering the links — Astro emits its own stylesheet after the
|
|
head markup, which is what made the ordering approach silently useless.
|
|
|
|
To try it: put a `theme.css`, a `logo.png` or a `brand.json` in `brand/`, run `npm run build` and
|
|
`npm start`. `curl -I` any `/brand/*` URL and the `X-Brand-Source` header says which of mount,
|
|
defaults or derivation answered.
|
|
|
|
Regenerating the stock assets is a separate, manual step — `npm run brand:assets` — because it reads
|
|
the emblem and the Cinzel outlines from the sibling checkouts in the workspace. Its output is
|
|
committed so that CI never needs either.
|
|
|
|
## Layout
|
|
|
|
```
|
|
src/
|
|
data/platform.json Every externally-sourced fact. No version is written in prose.
|
|
data/collection.mjs What is collected, in three scopes. /privacy renders it and the
|
|
Play Data Safety notes are generated from it — one inventory.
|
|
data/legal.mjs The values /terms, /privacy and the consent sentence must share.
|
|
styles/tokens.css THE token file — the only place a colour literal may appear.
|
|
styles/global.css The layout shell, built entirely from tokens.
|
|
styles/starlight.css Restates our tokens as Starlight's, so the docs cannot drift.
|
|
layouts/, components/ The marketing chrome.
|
|
pages/ Marketing routes.
|
|
pages/beta.astro The signup. Renders AND handles its own POST — runs per request.
|
|
content/docs/docs/ Documentation. The extra level mounts Starlight at /docs.
|
|
pages/brand/ GET /brand/* — the mount, resolved and derived. Runs per request.
|
|
lib/brand.mjs The single accessor for brand text, plus liveBrand() for the two
|
|
routes that render per request and so miss the boot rewrite.
|
|
lib/brandAssets.mjs Mount-first resolution and on-demand derivation.
|
|
lib/betaStore.mjs The SQLite store: schema, dedupe, rate-limit window, cap, removal.
|
|
lib/betaSignup.mjs Everything between a POST body and a row. Never throws.
|
|
lib/tokens.mjs Reads tokens.css at build time, for the few values that leave CSS.
|
|
config/sidebar.mjs The documentation journey, and the planned tree behind it.
|
|
brand-default/ The stock brand, baked into the image and always complete.
|
|
scripts/ The build-time checks, plus applyBrand (boot), brand:assets (manual)
|
|
and beta.mjs (the tester-list CLI).
|
|
test/ node --test. The logic the other checks cannot see.
|
|
PLAY_DATA_SAFETY.md GENERATED. The answers to Google Play's Data Safety form, from
|
|
src/data/collection.mjs. Edit the data, run npm run play:datasafety.
|
|
```
|
|
|
|
Two directories are bind mounts at runtime and are **not** in the repository: `brand/` overrides
|
|
anything in `brand-default/` per file, and `data/` holds the beta signup store. See `PLAN.md` §6
|
|
and §7.
|
|
|
|
## Contributing
|
|
|
|
Branch from `main` (`feature/…`, `fix/…`, `docs/…`, `chore/…`) and use
|
|
[Conventional Commits](https://www.conventionalcommits.org/). Run `npm run verify` before opening a
|
|
pull request.
|
|
|
|
**AI-assisted contributions must be disclosed**, per org policy: tick the box in the pull request
|
|
template naming the tool, and mark AI-authored commits with a trailer such as
|
|
`Co-Authored-By: Claude <noreply@anthropic.com>`. Undisclosed AI-generated contributions may be
|
|
closed.
|
|
|
|
## Licence
|
|
|
|
GPL-3.0-or-later, in common with every repository in the organisation. See [LICENSE](LICENSE).
|
|
|
|
[org]: https://gitea.whitlocktech.com/RunicGateway
|