docs(android): the app's events screens, and the module rows that were never gated (Phase 14b)

Events Phase 14b, docs half. `docs/android/PLAN.md` gains **M13** — written before
the code, as the plan requires — and `EVENTS_PLAN.md` and `EVENTS.md` record 14b
as built.

M13 records the six org-lead decisions, the two defects the milestone fixes, and
the three the emulator walk found.

The prerequisite fix is the larger half of the milestone: the app could not tell a
module that is not installed from a lookup that failed, 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 the
string 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 are two
gates that compose and answer different questions.

A second, older defect is recorded and fixed: the website path → route table had
been wrong since the module-system cutover on 2026-08-12, because core's `NAV` is
eight rows rather than sixteen and the nine shard rows moved to `/uo/*`. Two tests
in the app's suite had been passing vacuously since that day.

§5 gains the capability gate beside the feature gate, with the two failure
directions spelled out; §6.1 gains the three public event reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
2026-09-08 13:09:02 -05:00
parent b6fefc5466
commit 0a61915e50
3 changed files with 221 additions and 12 deletions

View File

@@ -1736,21 +1736,51 @@ a clock — and the third was a screen the reviewing admin could not open at all
`RequirePlayer` guards `/account` and the route behind it is role-agnostic. Engagement Phase 7 had
already solved that one; the fix is its `notificationPaths.js` mapping gaining a third entry.
#### Phase 14b — the app (`android-app` + `docs`)
#### Phase 14b — the app (`android-app` + `docs`) — **built**
The app's events screens, and **one prerequisite fix that is in this phase and not after it.** The
Android app hardcodes `api/v1/public/shard/stream` — a *module* path — reads `/public/modules`
nowhere, and `ShardFeaturesRepository` maps a failed `/public/shard/features` lookup to "unknown",
which `canSee` treats as visible. On a site with no `uo` module installed that route 404s, so every
shard entry in the drawer renders and every one of them 404s. Failing open is right *within* a module
that exists and wrong for one that is not installed; the fix gates the group on a **capability
string** — `shard`, which module-uo declares — rather than on a module id, and gates the new events
entry on core's `events`.
`android-app#44`, `docs#225`. **TWO repos**, and `MODULE_API_VERSION` is untouched. Recorded as
**M13** in `docs/android/PLAN.md`, which gained the milestone before the code was written.
`docs/android/PLAN.md` gains the milestone before the code is written.
The app's events screens — calendar, event page, arc, participation history — and **the prerequisite
fix, which is in this phase and not after it.** The app read `/public/modules` nowhere, and
`ShardFeaturesRepository` mapped a failed `/public/shard/features` lookup to "unknown", which `canSee`
treats as visible. On a site with no `uo` module that route 404s, so every shard entry rendered and
every one of them 404d. Failing open is right *within* a module that exists and wrong for one that is
not installed.
Six org-lead decisions. Four up front — the failure direction, the screen scope, where history lives,
and native deep links; and two on the scope of the second defect below. **One departed from the
recommendation:** an inbox link to `/site/events/<slug>?run=<id>` **opens the app** rather than a
Custom Tab, reusing `resolveWebPath` rather than adding a second link-routing mechanism. That forces
the one rule that function was written to enforce — *a query hands off* — to gain an exception, and it
is exactly one key on exactly one path.
**Absence of an answer is not an answer of absence**, which is the distinction the whole fix rests on:
a *successful* `/public/modules` read that does not name `shard` **hides** the rows; a *failed* read
keeps the last answer the host gave; a host that has never answered leaves the gate open. The old code
collapsed all three into one `null`. Capability and feature are a second and third gate that compose:
capability asks *is the module installed* (per host), feature asks *does this shard publish it to this
viewer* (per viewer).
**A second, older defect was found while scoping and fixed here** (org lead): the app's website
path → route table had been wrong since the module-system cutover on 2026-08-12. It quoted a
sixteen-row `NAV`; core's is now **eight** and the nine shard rows moved to `/uo/*`, because core
mounts a module's pages at `/<id>/<path>`. Three live consequences: a nav override on any shard row
was ignored, an added link to a shard page handed off to a browser instead of opening natively, and
the sort-key number line was wrong. Two tests in the existing suite had been passing **vacuously**
since that day — they asserted a section came out empty, and it did, because the item never joined it.
**Ships:** the calendar, event pages and participation history in the app.
**Verify:** the app suite; an emulator walk.
**Verify:** the app suite (**570 tests, 0 failures**); an emulator walk at three rungs.
**The walk found three defects and the suite found none of them.** The rig's own site had no `uo`
module, which is what made two of them visible. The three player game-data rows read `/player/shard/*`
and were not gated, so they rendered and 404d — and the test meant to catch that asked whether every
row *with a `feature`* declared the capability, which those three do not have. `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. And 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. See `docs/android/PLAN.md` M13.
---