Files
runicgateway.com/src/data/app.mjs
wtclaude 1313e748ae
All checks were successful
PR checks / checks (pull_request) Successful in 1m5s
feat(beta): phase 5 — the app page and the closed-beta signup
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>
2026-08-24 03:50:51 -05:00

184 lines
8.0 KiB
JavaScript

/**
* app.mjs — what the Android client does, as data. PLAN.md §10 `/app/`, phase 5.
*
* ---------------------------------------------------------------------------------------
* WHY THIS IS NOT capabilities.mjs
* ---------------------------------------------------------------------------------------
* `capabilities.mjs` describes what a DEPLOYMENT does, and it is checked against
* `module-uo`'s manifest because the module declares its capabilities in a machine-readable
* file. The app declares nothing of the kind: what it does is a set of screens in
* `Routes.kt`, and there is no manifest to diff against. So these are written from that
* file and carry the route names, which is the closest thing to a citation available — a
* reader who wants to check a claim here has a file to open.
*
* That is also why the app's list is shorter than the platform's rather than a mirror of
* it. The app is a client for the parts of a deployment a person uses on a phone; the parts
* it does not have are not missing, they are the parts nobody wants on a phone.
*
* ---------------------------------------------------------------------------------------
* "GATED BY WHAT THE DEPLOYMENT PUBLISHES" IS LOAD-BEARING, NOT A DISCLAIMER
* ---------------------------------------------------------------------------------------
* Almost everything here is conditional on the site the app is pointed at: shard screens
* appear only when the deployment runs a game module and has that visibility feature
* turned on, staff screens only for a staff account, push only where the operator runs an
* ntfy. A features list that omitted that would be describing an app nobody will see,
* because there is no default deployment — see `requiresDeployment` below.
*/
/**
* The single most misunderstood thing about this app, stated once and rendered prominently.
*
* `ConnectScreen.kt` on `Android-app` `main`: "First-run 'Connect to your shard's website'
* screen. Nothing else in the app runs until a valid Runic Gateway site is entered and
* validated." Not a soft default that can be changed later in settings — the gate on
* everything else. An install with no deployment behind it is a text field, and a page that
* let somebody find that out after downloading would have wasted their time on purpose.
*/
export const requiresDeployment = {
title: 'It is a client. It ships pointed at nothing.',
body:
'The first screen asks for the web address of a site running Runic Gateway and checks ' +
'it before anything else in the app will open. There is no default server, no ' +
'directory of servers, and no account with us — the app talks to the deployment you ' +
'name and to nothing else. If you do not run one and are not a member of a community ' +
'that does, the app has nothing to show you yet.',
};
/**
* @typedef {object} AppFeature
* @property {string} title
* @property {string} body
* @property {string} [gate] What the deployment must provide for this to appear at all.
*/
/** @type {{ heading: string, blurb: string, items: AppFeature[] }[]} */
export const appFeatures = [
{
heading: 'The shard, live',
blurb:
'The same feed the website shows, on a phone. Every one of these appears only when ' +
'the deployment runs a game module and the operator has published that surface — ' +
'the visibility settings are per-feature and default to off.',
items: [
{
title: 'Status, and the boards',
body:
'Whether the game server is up, plus champion spawns, guilds, city governors and ' +
'houses as the shard reports them.',
gate: 'A game module, and the matching visibility feature',
},
{
title: 'The player marketplace',
body:
'Player-run vendors and what is on their shelves, down to a single vendor. Item ' +
"names arrive as the game's own string ids and are resolved against its string " +
'table, so they read the way they read in the client.',
gate: 'A game module publishing the market',
},
{
title: 'Rules, leaderboards and the spawn atlas',
body:
"The deployment's published ruleset, its points and loyalty boards, and the " +
'creature atlas — what spawns where, and what it drops.',
gate: 'A game module, per feature',
},
],
},
{
heading: "The site's content",
blurb: 'News, wiki and pages, read natively rather than in a browser frame.',
items: [
{
title: 'News, by category',
body:
"Announcements and posts in the categories the site defines, and a link from " +
'anywhere on the web opens the matching tab rather than the top of the list.',
},
{
title: 'The wiki and the site pages',
body:
'Wiki articles and whatever pages the operator has written in the admin panel, ' +
'including the ones they added to the navigation themselves.',
},
],
},
{
heading: 'Your account',
blurb:
'Staff are players too — the self-service screens are role-agnostic, and an ' +
'administrator sees their own characters on the same screen everybody else does.',
items: [
{
title: 'Signing in, including two-factor',
body:
'A native sign-in with an authenticator code or a single-use recovery code, or ' +
"single sign-on handed off to the deployment's own provider in a browser tab. " +
'Trusting a device skips the code for thirty days, and that trust is revocable ' +
'per device from the app.',
},
{
title: 'Your characters, vendors and houses',
body:
'Character sheets, the vendors you run and the houses you own, visible to the ' +
'account they belong to and to nobody else.',
gate: 'A game module',
},
],
},
{
heading: 'Notifications, on infrastructure the operator owns',
blurb:
'Opt-in, per stream, and delivered without a third party — which is unusual enough ' +
'to be worth spelling out.',
items: [
{
title: 'Self-hosted push',
body:
"Push arrives over the deployment's own ntfy server, not Firebase. The app has " +
'no Google messaging dependency at all, which is why it works on a device with ' +
'no Play Services and why no notification passes through anyone else on its way ' +
'to the phone.',
gate: 'An ntfy server the operator runs',
},
{
title: 'The message carries no content',
body:
'What is pushed is which stream fired and an opaque reference — never the ' +
'subject, the sender or the text. The app opens the right screen and fetches the ' +
'actual content over the authenticated API, so a notification sitting on a lock ' +
'screen discloses nothing.',
},
],
},
{
heading: 'Staff work, if you are staff',
blurb:
'Gated by role in the menu and re-checked against the database on every request, so ' +
'a demoted account loses the screens immediately rather than at next sign-in.',
items: [
{
title: 'Moderation, support and content',
body:
'The dashboard, the moderation queue, support requests and content editing — ' +
'enough to answer a report from a phone. The surfaces that would let somebody ' +
'reconfigure the deployment stay on the web.',
gate: 'A staff role on the deployment',
},
],
},
{
heading: 'It looks like the deployment it is pointed at',
blurb: '',
items: [
{
title: 'The theme comes down the wire',
body:
"The palette, the type, the corner radii, the logo, the hero image and even the " +
"navigation order are read from the site's own appearance settings. Two " +
'communities running this app do not see the same app, and neither of them had ' +
'to build one.',
},
],
},
];