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:
183
src/data/app.mjs
Normal file
183
src/data/app.mjs
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user