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>
195 lines
11 KiB
Markdown
195 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 # astro check
|
|
npm test # the beta signup's decision path
|
|
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.
|
|
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.
|
|
```
|
|
|
|
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
|