feat(beta): phase 5 — the app page and the closed-beta signup
All checks were successful
PR checks / checks (pull_request) Successful in 1m5s
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>
This commit is contained in:
64
README.md
64
README.md
@@ -11,10 +11,11 @@ closed beta: **players**, who want the app.
|
||||
platform state, the org lead's decisions, the information architecture, and the build phases. Read
|
||||
it before changing anything here.
|
||||
|
||||
**Status: phase 1 of 12 — the foundation.** The scaffold, the token file, the typography, the layout
|
||||
shell and the two build-time checks are in place. The homepage is phase 3, the marketing pages
|
||||
phase 4, and the documentation — the installation path, which is the priority of the whole project —
|
||||
phase 7.
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
@@ -39,9 +40,12 @@ makes that would otherwise decay quietly.
|
||||
|
||||
```bash
|
||||
npm run check:tokens # no colour literal outside the token file
|
||||
GITEA_TOKEN=<token> npm run check:facts # every version agrees with its authority
|
||||
npm run check:brand # the branding pipeline's two quiet failures
|
||||
npm run check # astro check
|
||||
npm run verify # all of the above, then a production build
|
||||
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
|
||||
@@ -71,6 +75,44 @@ every literal `/brand/...` URL in the source through the route's own classifier,
|
||||
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
|
||||
@@ -114,14 +156,20 @@ src/
|
||||
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.
|
||||
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) and brand:assets (manual).
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user