feat(events): the app's event screens — M13 (Phase 16b cutover, 4 of 6) #46

Merged
whitlocktech merged 4 commits from edge into main 2026-09-09 20:09:43 +00:00
Member

The Event System cutover, step 4 of 6 (EVENTS_PLAN.md Phase 16b; docs/android/PLAN.md M13). edgemain, a clean fast-forwardmain is 0 ahead. 2 commits, 39 files.

No v* tag with this merge (org lead, 2026-09-09). The release workflow fires on tags only, so this cuts the source over without cutting an APK; whether to publish a build is a separate decision once the released core is verified in the second half of 16b.

What lands

Phase 14b — the four event screens. The calendar, an event page, arcs, and the player's own participation history, over the same four public routes the browser uses.

The prerequisite fix, and the rule that governs it: absence of an answer is not an answer of absence. The screens are gated on the host advertising the events capability, and the three cases are not the same:

  • a successful /public/modules read that omits the string hides the rows;
  • a failed read keeps the host's last answer rather than blanking the app;
  • a host that has never answered leaves the gate open, because a new install must not be told a feature is missing when nothing has been asked yet.

Capability (per host) and feature (per viewer) are two gates that compose.

A second, older defect fixed here on the org lead's call: the app's website path → route table had been wrong since the module-system cutover on 2026-08-12. Core's NAV is 8 rows, not 16, and the nine shard rows moved to /uo/*. Two existing tests had been passing vacuously since that day.

The second commit is the inbox fix the emulator walk found — a retained view model showed one account another's participation history.

Verification

Walked on an emulator against the whole rig (real ServUO → sidecar on protocol 7 → core with module-uo installed from a release-shaped bundle), which is where all three of Phase 14b's defects were found — 563 green tests had found none of them. The ungated player game-data rows, a Long score field against a DECIMAL(18,4) column that made a 200 render as a server error (latent on the public results table for every visitor), and the retained view model above.

Phase 16a's walk re-confirmed the calendar end to end: /site/events shows a live run as Happening now in the browser and in the app — the app consumes the same route, so the calendar defect fixed in website#198 was breaking it too.

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

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

The Event System cutover, step 4 of 6 (`EVENTS_PLAN.md` Phase 16b; `docs/android/PLAN.md` **M13**). **`edge` → `main`, a clean fast-forward** — `main` is 0 ahead. 2 commits, 39 files. **No `v*` tag with this merge** (org lead, 2026-09-09). The release workflow fires on tags only, so this cuts the source over without cutting an APK; whether to publish a build is a separate decision once the released core is verified in the second half of 16b. ## What lands **Phase 14b — the four event screens.** The calendar, an event page, arcs, and the player's own participation history, over the same four public routes the browser uses. **The prerequisite fix, and the rule that governs it: *absence of an answer is not an answer of absence*.** The screens are gated on the host advertising the `events` capability, and the three cases are not the same: - a **successful** `/public/modules` read that omits the string hides the rows; - a **failed** read keeps the host's last answer rather than blanking the app; - a host that has **never** answered leaves the gate open, because a new install must not be told a feature is missing when nothing has been asked yet. Capability (per host) and feature (per viewer) are two gates that compose. **A second, older defect fixed here on the org lead's call:** the app's website path → route table had been wrong since the module-system cutover on 2026-08-12. Core's NAV is 8 rows, not 16, and the nine shard rows moved to `/uo/*`. Two existing tests had been passing **vacuously** since that day. The second commit is the inbox fix the emulator walk found — a retained view model showed one account another's participation history. ## Verification Walked on an emulator against the whole rig (real ServUO → sidecar on protocol 7 → core with `module-uo` installed from a release-shaped bundle), which is where all three of Phase 14b's defects were found — 563 green tests had found none of them. The ungated player game-data rows, a `Long` score field against a `DECIMAL(18,4)` column that made a 200 render as a server error (latent on the **public** results table for every visitor), and the retained view model above. Phase 16a's walk re-confirmed the calendar end to end: `/site/events` shows a live run as **Happening now** in the browser *and* in the app — the app consumes the same route, so the calendar defect fixed in `website#198` was breaking it too. - [x] AI-assisted: written with Claude Code (Claude Opus 5). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 4 commits 2026-09-09 15:38:09 +00:00
feat(events): the app's events screens, and the module rows that were never gated (Phase 14b)
All checks were successful
PR Checks / android-build (pull_request) Successful in 12m7s
e10e1f1617
Events Phase 14b, the app half — recorded as M13 in docs/android/PLAN.md.

Four screens on the four routes Phase 14a shipped: the public calendar, an event
page carrying `?run=`, an arc, and participation history. One drawer row for the
history, at SIGNED_IN rather than PLAYER: the route is `requireAuth` alone and
self-scoped, and the website needed two mounts for it only because `RequirePlayer`
guards `/account` there.

The prerequisite fix is the larger half. The app read `/public/modules` nowhere
and mapped every `/public/shard/features` failure to "unknown", which `canSee`
treats as visible — so on a site with no `uo` module every shard row rendered and
every one of them 404'd. Absence of an answer is not an answer of absence: a
successful module list that omits `shard` hides the rows, a failed read keeps the
last answer the host gave, and a host that has never answered leaves the gate
open. Capability and feature compose as two gates and answer different questions:
whether the module is installed (per host) and whether this shard publishes the
surface to this viewer (per viewer).

Also corrects the website path → route table, wrong since the module-system
cutover on 2026-08-12: core's NAV is eight rows, not sixteen, and the nine shard
rows moved to `/uo/*`. A nav override on any shard row was ignored, an added link
to one handed off to a browser, and the sort-key line was wrong. Two existing
tests had been passing vacuously since that day.

An inbox link to an event now opens the app rather than a Custom Tab, through
`resolveWebPath` rather than a second mechanism — so its "a query hands off" rule
gains exactly one exception, `run` on an event page.

The emulator walk found three defects that 563 green tests did not:

- the three player game-data rows read `/player/shard/*` and were not gated, so
  they rendered and 404'd; the test meant to catch that asked whether every row
  *with a feature* declared the capability, and those three have none. It now
  asks by route.
- `score` is DECIMAL(18,4) and was declared an integer, so one `318.5` made
  kotlinx refuse the entire body and a 200 rendered as a server error — latent on
  the public results table for every visitor.
- a drawer route's view model outlives a sign-out, so signing in as a second
  account showed it the first account's participation history with no request
  made at all.

570 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Reviewed-on: #44
fix(notifications): reload the inbox and its settings when the account changes
All checks were successful
PR Checks / android-build (pull_request) Successful in 8m12s
aa055469a8
The defect Phase 14b found in `MyEventsViewModel` and flagged next door: the
notifications surface has the identical shape, and it leaks the same way.

A drawer route's view model outlives a sign-out. `navigateTopLevel` uses
`popUpTo(HOME) { saveState = true }` with `restoreState = true`, so the
`NavBackStackEntry` keeps its `ViewModelStore` and a view model that loaded only
in `init` never runs again. Signing out and back in as somebody else showed the
second account the FIRST account's inbox — titles and body text written for
another person — with no request made at all, while the badge above the list
showed the new account's real unread count, because the shell refreshes that on
every session change.

`InboxCache` was never the hole: it is keyed by (base URL, user id) and a snapshot
has never crossed an account. The hole was the in-memory state, which nothing
invalidated.

Both view models now key on the signed-in account id, so a resume revalidation
that returns the same user does not refetch. The inbox resets its state *before*
loading rather than after, because `load()` paints the cache only when there is no
`Success` on screen — otherwise the previous account's rows stay up for the whole
round trip.

The settings screen behind the inbox's gear is fixed with it, and there the stale
render is worse than disclosure: those controls are written from, so a screen
still showing the previous account's preferences would send this account's PUT
built out of them.

Walked on the emulator against a local website, before and after: two accounts
with deliberately different inboxes, signed out and in within one process. Before,
the second account saw the first's rows and the server logged no inbox fetch;
after, it logs the fetch and shows its own.

572 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Reviewed-on: #45
whitlocktech approved these changes 2026-09-09 20:09:26 +00:00
whitlocktech merged commit 5e61ee2bbb into main 2026-09-09 20:09:43 +00:00
Sign in to join this conversation.
No description provided.