feat(beta): phase 5 — the app page and the closed-beta signup #8

Merged
whitlocktech merged 1 commits from feat/phase-5-beta into main 2026-08-24 08:55:02 +00:00
Member

Phase 5 of PLAN.md §13: /app/, /beta/, the signup handler, the SQLite store, rate limiting and the export CLI of §8 — plus this repository's first test suite.

Built against the decisions taken 2026-08-24, recorded in §8 under "How phase 5 built the app and the beta" as D26–D29.

What the repositories said when §8 and §10 were checked

Three things this plan had assumed turned out not to hold, and each changed what got built:

  • The "14 existing screenshots" are the wrong fourteen. docs/android/screenshots/ is a trusted-device and recovery-code smoke test from 2026-07-22 — captured against a development instance with no seeded content, before the theming work changed how every screen looks, and five of them are two-factor prompts. The home shot is an almost empty page.
  • The app targets no server of ours. ConnectScreen.kt on Android-app main: nothing in the app runs until a valid Runic Gateway site has been entered and validated. §8 never said what a tester would point it at.
  • /app/ can offer a download today. Every Android-app release attaches a signed APK and a SHA256SUMS. §8 and §10 were both written as though Play were the only delivery path.

The four decisions

Decision
D26 The screenshot slot ships empty, reserved for phase 9. Shipping the existing fourteen would break D4 and show an app that no longer looks like that. Phase 9 already stands up the review stack and seeds content, so it gains an emulator pass — and the phone shots then show the same deployment as the web shots. Screenshots.astro exists now rendering nothing, so filling it is a data change.
D27 The public demo is the tester target, so the beta waits on it. Declined: naming UOMysticmoon, which would open the beta immediately at the cost of publishing a private shard's address. Costs nothing today because the other gate — no closed test track — is open anyway.
D28 /beta handles its own POST; there is no /api/beta-signup. An endpoint answers with JSON (form works only with JavaScript) or a redirect (a mistyped address returns to a blank form with no explanation). §6's diagram is amended.
D29 The APK and the beta get equal billing — but the APK link is currently off. platform.json's androidApk.serviceable is false because the published v0.5.0 build does not work, so the panel states that plainly instead of linking it. The panel is not removed: a page that omitted sideloading would read, to somebody who knows the APK exists, as a page hiding it.

serviceable is the one value in platform.json with no authority behind it, deliberately — no fetch can tell whether an APK runs. Flipping it to true is the single edit that turns the link back on.

Three mechanisms the plan did not anticipate

  • liveBrand() — §7's boot rewrite patches files in dist/client, and an on-demand route's HTML never was a file, so /beta reading brand would show stock values forever. It reads the mounted brand.json itself behind an mtime guard. Strictly better where it applies: pasting the Play opt-in URL into the mount takes effect on the next request, no restart.
  • checkLinks.mjs learned what an on-demand route is. /beta is the first on-demand page and rule 1 resolves against the build, where it has no file. A PLANNED_ROUTES entry would have been wrong in the exact way that list's own comment warns about — 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. Routes are derived from prerender = false in the source instead; delete beta.astro and the links fail again.
  • A test suite, a first here. The five checks of §12 all read built output and none of this logic appears there — a honeypot can stop working entirely and produce an identical build. 25 tests over the honeypot, the signed form token, timing, the per-connection limit, the cap, validation, idempotent duplicates and removal.

One design conflict the tests caught

removeSignup erases the address rather than flagging it, so it cannot also recognise that address later — the two properties are incompatible and erasure is the one §9 actually promises. Keeping a hash so the form could say "you were removed" would mean retaining a derived identifier for the one person who asked not to be retained. A re-signup after removal is an ordinary new row, and a test now guards against somebody "fixing" that.

Verified end to end

Against the built server, not the dev server:

  • Every outcome renders with no JavaScript — added, duplicate, honeypot, too-fast, stale token, rate-limited, invalid address, missing consent, cap reached, store unavailable.
  • Duplicate is byte-identical to added, so the form cannot be used to ask whether an address is enrolled (§8's rule).
  • Astro's checkOrigin refuses cross-origin POSTs (403) — a free CSRF layer §8 did not anticipate.
  • The honeypot costs no rate-limit budget, so it cannot be used to lock out everyone behind a shared address.
  • A mounted betaOptInUrl reached the running server with no restart, replacing the waiting state with the real link.
  • The export CLI round-trips: CSV record + .txt paste list, idempotent re-export, case-insensitive removal.
  • checkFacts gains the APK assets and minSdk, and learns that RFC 2606 reserved domains are not contact addresses — it had flagged placeholder="you@example.com". The D13 rule is otherwise unchanged, and it is still the check that would catch a real address.

npm run verify is green: tokens, brand, astro check, 25 tests, production build, 159 links, 19 facts.

🤖 This pull request was written with Claude Code.

Co-Authored-By: Claude noreply@anthropic.com

Phase 5 of `PLAN.md` §13: `/app/`, `/beta/`, the signup handler, the SQLite store, rate limiting and the export CLI of §8 — plus this repository's first test suite. Built against the decisions taken 2026-08-24, recorded in §8 under **"How phase 5 built the app and the beta"** as **D26–D29**. ## What the repositories said when §8 and §10 were checked Three things this plan had assumed turned out not to hold, and each changed what got built: - **The "14 existing screenshots" are the wrong fourteen.** `docs/android/screenshots/` is a trusted-device and recovery-code smoke test from 2026-07-22 — captured against a development instance with no seeded content, before the theming work changed how every screen looks, and five of them are two-factor prompts. The home shot is an almost empty page. - **The app targets no server of ours.** `ConnectScreen.kt` on `Android-app` `main`: nothing in the app runs until a valid Runic Gateway site has been entered and validated. §8 never said what a tester would point it at. - **`/app/` can offer a download today.** Every `Android-app` release attaches a signed APK and a `SHA256SUMS`. §8 and §10 were both written as though Play were the only delivery path. ## The four decisions | | Decision | |---|---| | **D26** | The screenshot slot ships **empty**, reserved for phase 9. Shipping the existing fourteen would break D4 and show an app that no longer looks like that. Phase 9 already stands up the review stack and seeds content, so it gains an emulator pass — and the phone shots then show the same deployment as the web shots. `Screenshots.astro` exists now rendering nothing, so filling it is a data change. | | **D27** | The **public demo is the tester target**, so the beta waits on it. Declined: naming UOMysticmoon, which would open the beta immediately at the cost of publishing a private shard's address. Costs nothing today because the other gate — no closed test track — is open anyway. | | **D28** | **`/beta` handles its own POST**; there is no `/api/beta-signup`. An endpoint answers with JSON (form works only with JavaScript) or a redirect (a mistyped address returns to a blank form with no explanation). §6's diagram is amended. | | **D29** | The APK and the beta get **equal billing** — but the APK link is currently **off**. `platform.json`'s `androidApk.serviceable` is `false` because the published `v0.5.0` build does not work, so the panel states that plainly instead of linking it. The panel is not removed: a page that omitted sideloading would read, to somebody who knows the APK exists, as a page hiding it. | `serviceable` is the one value in `platform.json` with no authority behind it, deliberately — no fetch can tell whether an APK runs. Flipping it to `true` is the single edit that turns the link back on. ## Three mechanisms the plan did not anticipate - **`liveBrand()`** — §7's boot rewrite patches files in `dist/client`, and an on-demand route's HTML never was a file, so `/beta` reading `brand` would show stock values forever. It reads the mounted `brand.json` itself behind an mtime guard. Strictly better where it applies: pasting the Play opt-in URL into the mount takes effect on the **next request**, no restart. - **`checkLinks.mjs` learned what an on-demand route is.** `/beta` is the first on-demand *page* and rule 1 resolves against the build, where it has no file. A `PLANNED_ROUTES` entry would have been wrong in the exact way that list's own comment warns about — 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. Routes are derived from `prerender = false` in the source instead; delete `beta.astro` and the links fail again. - **A test suite, a first here.** The five checks of §12 all read built output and none of this logic appears there — a honeypot can stop working entirely and produce an identical build. 25 tests over the honeypot, the signed form token, timing, the per-connection limit, the cap, validation, idempotent duplicates and removal. ## One design conflict the tests caught `removeSignup` erases the address rather than flagging it, so it **cannot** also recognise that address later — the two properties are incompatible and erasure is the one §9 actually promises. Keeping a hash so the form could say "you were removed" would mean retaining a derived identifier for the one person who asked not to be retained. A re-signup after removal is an ordinary new row, and a test now guards against somebody "fixing" that. ## Verified end to end Against the built server, not the dev server: - Every outcome renders **with no JavaScript** — added, duplicate, honeypot, too-fast, stale token, rate-limited, invalid address, missing consent, cap reached, store unavailable. - **Duplicate is byte-identical to added**, so the form cannot be used to ask whether an address is enrolled (§8's rule). - Astro's `checkOrigin` refuses **cross-origin POSTs** (403) — a free CSRF layer §8 did not anticipate. - The honeypot costs no rate-limit budget, so it cannot be used to lock out everyone behind a shared address. - A **mounted `betaOptInUrl` reached the running server with no restart**, replacing the waiting state with the real link. - The export CLI round-trips: CSV record + `.txt` paste list, idempotent re-export, case-insensitive removal. - `checkFacts` gains the APK assets and `minSdk`, and learns that RFC 2606 reserved domains are not contact addresses — it had flagged `placeholder="you@example.com"`. The D13 rule is otherwise unchanged, and it is still the check that would catch a real address. `npm run verify` is green: tokens, brand, `astro check`, 25 tests, production build, 159 links, 19 facts. <!-- AI disclosure --> 🤖 This pull request was written with [Claude Code](https://claude.com/claude-code). Co-Authored-By: Claude <noreply@anthropic.com>
wtclaude added 1 commit 2026-08-24 08:51:34 +00:00
feat(beta): phase 5 — the app page and the closed-beta signup
All checks were successful
PR checks / checks (pull_request) Successful in 1m5s
1313e748ae
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>
whitlocktech merged commit 29c96d0b21 into main 2026-08-24 08:55:02 +00:00
whitlocktech deleted branch feat/phase-5-beta 2026-08-24 08:55:03 +00:00
Sign in to join this conversation.
No description provided.