feat(demo): explain the public demo's refusals and show its strip on Home #61

Merged
whitlocktech merged 1 commits from feat/demo-mode into edge 2026-10-26 09:18:15 -05:00
Member

Step 4 of docs/website/DEMO_MODE.md §10 (RunicGateway/website#222). Both build questions were answered by the org lead: DM19 (one snackbar, plus fixing the lines that blame the role) and DM20 (the banner only, no logins on the app's sign-in screen).

The refusal (DM19)

  • DemoLockInterceptor on the main OkHttp client peeks every 403 body (up to 8 KiB). On code: "DEMO_LOCKED" it reports the server's sentence to DemoNotices. The shell (RunicApp) shows it as one snackbar, the app's counterpart of the web's DemoToast, and a newer refusal replaces the one showing. Every API rides this client, so screens added later get it without doing anything. A module's own wording, such as a refused console command, is shown as sent.

  • ApiResult.HttpError gains code. safeApiCall reads it only on a 403, and peeks so the body is not consumed. isDemoLocked() checks it.

  • ErrorKind.DEMO_LOCKED is checked ahead of the status in toUiState and toShardUiState. Without that, a 403 on a shard read would read as "not published here". It gets no Retry button, like FEATURE_UNAVAILABLE.

  • Lines that blamed the role now say the demo sentence:

    • the four admin view models: Content, Dashboard, Moderation and Support. These said "You don't have permission for that action." to somebody signed in as demo-admin;
    • Characters' account creation, which said "signup is off";
    • the contact form's line, since the contact form is locked on the demo.

    Every other screen keeps its own failure line, with the snackbar underneath.

The strip (DM20)

  • SettingsDto.demo is kept raw, like theme, so a value of an unexpected shape cannot fail the whole settings decode. DemoInfo.from reads it field by field: a bad time or a malformed login drops that part, never the strip.
  • Home draws it above the status card, so it also shows while status is loading or has failed. The wording follows the web's, with the time in UTC and not converted (DM7). The logins sentence names the site's sign-in page for the passwords (demo.runicgateway.com/account/login) instead of listing them.
  • A real shard sends no demo block, so it draws nothing.

Tests

  • ./gradlew testDebugUnitTest lintDebug assembleDebug (the CI line) passes, with 783 unit tests.
  • New tests:
    • DemoLockedTest: body parsing, the code on HttpError, and that only a 403 body is read.
    • DemoLockInterceptorTest: a real OkHttp client; the refusal is reported and the body still reaches the caller; other answers are not reported.
    • DemoInfoTest: shape, forgiving parse, UTC time, the sign-in page label.
    • The DEMO_LOCKED mapping in UiStateTest.
    • One case each for the four admin view models, Characters, Contact, and Home (strip / no strip).
  • Found while testing: the fakes reuse one exception across calls, so reading the body for real consumed it on the screen's first load. That is why the body is now peeked, not read.

Checked on an emulator

Against website main (69998ec) with DEMO_MODE=true and npm run seed:demo on a scratch database (dropped afterwards):

  • Home: the strip read "Demo site — anything you change is wiped at 12:00 UTC. Sign-ins: demo-admin / demo-moderator / demo-player — passwords at 127.0.0.1:3271/account/login."
  • demo-player, Account → Change username: the server logged [demo] refused PATCH /auth/me/account/username. The screen showed "Couldn't change your username." and the snackbar "This is turned off on the demo site."
  • demo-admin, Dashboard → Switch to maintenance: refused at PUT /admin/site-mode. The line read "This is turned off on the demo site." in place of "You don't have permission for that action.", with the snackbar underneath.

Docs: RunicGateway/docs (DEMO_MODE.md DM19/DM20, §3.1, §6 and §10 step 4; android/PLAN.md §7).

  • AI-assisted: written with Claude Code (Claude Opus 5.5).

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

Step 4 of `docs/website/DEMO_MODE.md` §10 (RunicGateway/website#222). Both build questions were answered by the org lead: **DM19** (one snackbar, plus fixing the lines that blame the role) and **DM20** (the banner only, no logins on the app's sign-in screen). ## The refusal (DM19) - **`DemoLockInterceptor`** on the main OkHttp client peeks every `403` body (up to 8 KiB). On `code: "DEMO_LOCKED"` it reports the server's sentence to **`DemoNotices`**. The shell (`RunicApp`) shows it as one snackbar, the app's counterpart of the web's `DemoToast`, and a newer refusal replaces the one showing. Every API rides this client, so screens added later get it without doing anything. A module's own wording, such as a refused console command, is shown as sent. - **`ApiResult.HttpError` gains `code`.** `safeApiCall` reads it only on a `403`, and peeks so the body is not consumed. `isDemoLocked()` checks it. - **`ErrorKind.DEMO_LOCKED`** is checked ahead of the status in `toUiState` and `toShardUiState`. Without that, a `403` on a shard read would read as "not published here". It gets no Retry button, like `FEATURE_UNAVAILABLE`. - **Lines that blamed the role now say the demo sentence:** - the four admin view models: Content, Dashboard, Moderation and Support. These said "You don't have permission for that action." to somebody signed in as `demo-admin`; - Characters' account creation, which said "signup is off"; - the contact form's line, since the contact form is locked on the demo. Every other screen keeps its own failure line, with the snackbar underneath. ## The strip (DM20) - `SettingsDto.demo` is kept raw, like `theme`, so a value of an unexpected shape cannot fail the whole settings decode. `DemoInfo.from` reads it field by field: a bad time or a malformed login drops that part, never the strip. - Home draws it above the status card, so it also shows while status is loading or has failed. The wording follows the web's, with the time in UTC and not converted (DM7). The logins sentence names the site's sign-in page for the passwords (`demo.runicgateway.com/account/login`) instead of listing them. - A real shard sends no `demo` block, so it draws nothing. ## Tests - `./gradlew testDebugUnitTest lintDebug assembleDebug` (the CI line) passes, with **783 unit tests**. - New tests: - `DemoLockedTest`: body parsing, the code on `HttpError`, and that only a 403 body is read. - `DemoLockInterceptorTest`: a real OkHttp client; the refusal is reported and the body still reaches the caller; other answers are not reported. - `DemoInfoTest`: shape, forgiving parse, UTC time, the sign-in page label. - The `DEMO_LOCKED` mapping in `UiStateTest`. - One case each for the four admin view models, Characters, Contact, and Home (strip / no strip). - Found while testing: the fakes reuse one exception across calls, so reading the body for real consumed it on the screen's first load. That is why the body is now peeked, not read. ## Checked on an emulator Against `website` `main` (`69998ec`) with `DEMO_MODE=true` and `npm run seed:demo` on a scratch database (dropped afterwards): - **Home:** the strip read "Demo site — anything you change is wiped at 12:00 UTC. Sign-ins: demo-admin / demo-moderator / demo-player — passwords at 127.0.0.1:3271/account/login." - **`demo-player`, Account → Change username:** the server logged `[demo] refused PATCH /auth/me/account/username`. The screen showed "Couldn't change your username." and the snackbar "This is turned off on the demo site." - **`demo-admin`, Dashboard → Switch to maintenance:** refused at `PUT /admin/site-mode`. The line read "This is turned off on the demo site." in place of "You don't have permission for that action.", with the snackbar underneath. Docs: RunicGateway/docs (DEMO_MODE.md DM19/DM20, §3.1, §6 and §10 step 4; android/PLAN.md §7). - [x] AI-assisted: written with Claude Code (Claude Opus 5.5). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 1 commit 2026-10-26 06:29:58 -05:00
feat(demo): explain the public demo's refusals and show its strip on Home
All checks were successful
PR Checks / android-build (pull_request) Successful in 20m2s
aca358d72b
Step 4 of docs website/DEMO_MODE.md §10 (website#222).

- DemoLockInterceptor on the main client peeks every 403 body. On
  code DEMO_LOCKED it reports the server's sentence to DemoNotices, and the
  shell shows it as one snackbar, like the web's toast (DM19). A newer refusal
  replaces the one showing. Screens added later get it without doing anything.
- ApiResult.HttpError carries the body's code (read only on a 403, peeked so
  the body is not consumed); isDemoLocked(). ErrorKind.DEMO_LOCKED is mapped
  ahead of the status in toUiState and toShardUiState, and is not retryable.
- Screens whose 403 copy blames the role now say the demo sentence instead:
  the four admin view models, Characters' account creation, and the contact
  form's line (the contact form is locked on the demo).
- Home draws the strip from GET /public/settings' demo block: the wipe time in
  UTC, the published usernames, and the site's sign-in page for the passwords.
  The app does not list the passwords (DM20). A real shard sends no block, so
  it draws nothing.

Checked on an emulator against website main (69998ec) with DEMO_MODE=true and
seed:demo: the strip on Home; demo-player's username change refused with the
snackbar under the screen's own line; demo-admin's site-mode switch refused
with the demo sentence in place of "You don't have permission".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
whitlocktech merged commit ac60427a3e into edge 2026-10-26 09:18:15 -05:00
whitlocktech deleted branch feat/demo-mode 2026-10-26 09:18:16 -05:00
Sign in to join this conversation.
No description provided.