feat(rust): the Rust server list and one server's page — M14 (module-rust phase 5, Android leg A) #47

Merged
whitlocktech merged 4 commits from feature/rust-p5-android-a into edge 2026-09-17 09:22:02 +00:00
Member

The app's half of module-rust's read path. R10 says each Android leg trails the website surface it consumes by one phase, so this is built against routes that exist and was walked against a running one.

Criterion met, and walked in an emulator rather than asserted: the app renders a Rust site it has never seen, and a UO site unchanged.

What it adds

Two screens, mirroring what phase 4 shipped:

  • /rust is the server list (D12) — not a hub above one. The module registers its pages with path: '' and core strips the trailing separator, so there is no landing page between the drawer row and the servers.
  • One server is one screen with four tabs (D13) — feed, leaderboard, online, wipes. Four questions about one thing; a reader moving between them is not navigating.

Both render entirely from the website's own tables, so the phase criterion holds here for the same reason it holds on the web: a fleet that is entirely off still shows its maps, seeds, wipe dates, killfeeds, leaderboards and last known presence.

What is new to the app rather than copied

A poll that is not a load (D17). The app has had exactly one shape for a read — blank, ask, replace. That is right for opening a screen and would clear the killfeed three times a minute here; the website hit the same wall one tier along, which is why module-rust bundles its own usePolled instead of using core's useAsync. PollWhileResumed + refreshInto are that hook's other half: a refresh is invisible when it succeeds and keeps the rows when it fails. repeatOnLifecycle(RESUMED) gives the phone's version of D14's Page Visibility gate — nothing runs while the app is away, and returning refreshes at once. Only the visible live panel is polled; the leaderboard and the wipe list never are, because a leaderboard that re-sorted itself under a finger every twenty seconds is worse than a stale one.

A second game module in the drawer. Capability.RUST, gating one row — and deliberately not servers/killfeed/leaderboard/presence/wipes. Every one of those names a surface, and core flattens all modules' capabilities into one list, so servers is a word another module could declare tomorrow and silently reveal these screens on a site with no Rust. Module-Rust#5 adds the identity string.

/rust in NavPaths, so an admin's nav override on that row — or an added link to it — opens natively instead of handing off to a browser.

A live player count on the drawer row (D19) — the phone's answer to D15's footer slot, which has no analogue here. It rides in the same NavigationDrawerItem badge slot the inbox's unread count uses, with the same screen-reader treatment. Zero renders nothing, a failed read keeps the last number, and it never polls.

Two things carried across rather than rediscovered

The website's own page walk found both, and this leg inherits them:

  • "last reported" reads lastSeenAt, never updatedAt — a failed poll moves the second, which is what made an offline server claim it had just checked in every thirty seconds for as long as it stayed down.
  • A feed row from another calendar day carries its date — or a row from a past wipe reads as this afternoon.

What the walk found

Three defects, none visible to a unit test — all fixed in the second commit:

  1. The drawer's live count resolved once per process. Keyed on the capability answer alone, so it was read at connect and never again. It now refreshes on resume.
  2. Every card's text sat flush against its edge. ShardCard carries no padding of its own; each caller pads its own content, and these four did not.
  3. A name touched its own kill count. Five numeric columns beside an equal-weight name column read as one field. The name now takes a wider share and ellipsizes, and the active sort is marked on the header rather than by tinting a column of numbers.

How it was proven

Against the phase-4 rig — a core with the module installed, one live server and one seeded fixture that has never reported:

  • The criterion, directly: with its server unreachable and reading Offline, the page still rendered its map, size, seed, wipe date, killfeed, both leaderboards, its last known presence board and its wipe history.
  • The other half: the same app pointed at the UO core showed Shard / Rules / Atlas / Leaderboards / Market and no Rust row.
  • refreshInto, against a genuinely dead backend — the core was stopped with the list on screen; a poll tick later the rows were unchanged with one quiet line above them.
  • R12's arithmetic on a phone: all-time 59 = 41 + 18 across two wipes, and a player who appears only in the older wipe drops out of it rather than reading zero.
  • Every describe branch rendered from real rows: a player kill with weapon/distance/grid, an NPC kill, a suicide, an environment death, chat with its colon in the join, a disconnect with reason and session, and "while sleeping".
  • The date rule: filtering to the August wipe rendered three rows six weeks old, each unmistakably dated.
  • The badge, showing a live count.

One commit here is not phase 5, and says so

fix(ci): stop setup-android installing a package Google has removed. Included here on the org lead's call (2026-09-17), because it blocks verifying anything at all.

android-actions/setup-android@v3 is a floating tag and the action's packages input defaults to tools — an obsolete package Google has since removed from the SDK repository. The step runs sdkmanager tools, gets Warning: Failed to find package 'tools', exits 1, and CI fails in Set up Android SDK before a line of this repo is compiled. PR #46 passed on this same workflow yesterday; every Android PR fails now.

packages: '' turns that install off, and it was always redundant here — the next step installs exactly what the build targets (platform-tools, platforms;android-35, build-tools;35.0.0) precisely so the build never depends on what an action decided to fetch.

Not addressed, and worth its own decision: @v3 is a floating major tag, so the next upstream change can break CI the same way without warning.

Rig note worth keeping

The debug network_security_config.xml permits cleartext to 127.0.0.1 and localhost only — not 10.0.2.2. An emulator walk against a local core needs adb reverse tcp:<port> tcp:<port> and the loopback address, or every request fails with UnknownServiceException: CLEARTEXT communication to 10.0.2.2 not permitted.

The four navigation tests that moved

APP_MENU gained a row and the website's nav number line gained an index, so the merge's expected shapes shifted by one. Each now says which, and why both game modules appear in a table no single backend serves.

Base branch

This targets edge, per the org lead's decision to move the Rust workstream's repos and work onto edge (2026-09-16). pr-checks.yml already triggers on [main, edge].

AI disclosure

  • This contribution was AI-assisted (Claude Code).

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

The app's half of `module-rust`'s read path. R10 says each Android leg trails the website surface it consumes by one phase, so this is built against routes that exist and was walked against a running one. **Criterion met, and walked in an emulator rather than asserted:** the app renders a Rust site it has never seen, and a UO site unchanged. ## What it adds Two screens, mirroring what phase 4 shipped: - **`/rust` is the server list** (D12) — not a hub above one. The module registers its pages with `path: ''` and core strips the trailing separator, so there is no landing page between the drawer row and the servers. - **One server is one screen with four tabs** (D13) — feed, leaderboard, online, wipes. Four questions about one thing; a reader moving between them is not navigating. Both render entirely from the website's own tables, so the phase criterion holds here for the same reason it holds on the web: a fleet that is entirely off still shows its maps, seeds, wipe dates, killfeeds, leaderboards and last known presence. ## What is new to the app rather than copied **A poll that is not a load** (D17). The app has had exactly one shape for a read — blank, ask, replace. That is right for opening a screen and would clear the killfeed three times a minute here; the website hit the same wall one tier along, which is why `module-rust` bundles its own `usePolled` instead of using core's `useAsync`. `PollWhileResumed` + `refreshInto` are that hook's other half: **a refresh is invisible when it succeeds and keeps the rows when it fails.** `repeatOnLifecycle(RESUMED)` gives the phone's version of D14's Page Visibility gate — nothing runs while the app is away, and returning refreshes at once. Only the *visible* live panel is polled; the leaderboard and the wipe list never are, because a leaderboard that re-sorted itself under a finger every twenty seconds is worse than a stale one. **A second game module in the drawer.** `Capability.RUST`, gating one row — and deliberately **not** `servers`/`killfeed`/`leaderboard`/`presence`/`wipes`. Every one of those names a *surface*, and core flattens all modules' capabilities into one list, so `servers` is a word another module could declare tomorrow and silently reveal these screens on a site with no Rust. Module-Rust#5 adds the identity string. **`/rust` in `NavPaths`**, so an admin's nav override on that row — or an added link to it — opens natively instead of handing off to a browser. **A live player count on the drawer row** (D19) — the phone's answer to D15's footer slot, which has no analogue here. It rides in the same `NavigationDrawerItem` badge slot the inbox's unread count uses, with the same screen-reader treatment. Zero renders nothing, a failed read keeps the last number, and it never polls. ## Two things carried across rather than rediscovered The website's own page walk found both, and this leg inherits them: - **"last reported" reads `lastSeenAt`, never `updatedAt`** — a failed poll moves the second, which is what made an offline server claim it had just checked in every thirty seconds for as long as it stayed down. - **A feed row from another calendar day carries its date** — or a row from a past wipe reads as this afternoon. ## What the walk found Three defects, none visible to a unit test — all fixed in the second commit: 1. **The drawer's live count resolved once per process.** Keyed on the capability answer alone, so it was read at connect and never again. It now refreshes on resume. 2. **Every card's text sat flush against its edge.** `ShardCard` carries no padding of its own; each caller pads its own content, and these four did not. 3. **A name touched its own kill count.** Five numeric columns beside an equal-weight name column read as one field. The name now takes a wider share and ellipsizes, and the active sort is marked on the *header* rather than by tinting a column of numbers. ## How it was proven Against the phase-4 rig — a core with the module installed, one live server and one seeded fixture that has never reported: - The **criterion**, directly: with its server unreachable and reading `Offline`, the page still rendered its map, size, seed, wipe date, killfeed, both leaderboards, its last known presence board and its wipe history. - The **other half**: the same app pointed at the UO core showed Shard / Rules / Atlas / Leaderboards / Market and **no Rust row**. - **`refreshInto`, against a genuinely dead backend** — the core was stopped with the list on screen; a poll tick later the rows were unchanged with one quiet line above them. - **R12's arithmetic on a phone**: all-time 59 = 41 + 18 across two wipes, and a player who appears only in the older wipe **drops out** of it rather than reading zero. - Every `describe` branch rendered from real rows: a player kill with weapon/distance/grid, an NPC kill, a suicide, an environment death, chat with its colon in the join, a disconnect with reason and session, and "while sleeping". - The **date rule**: filtering to the August wipe rendered three rows six weeks old, each unmistakably dated. - The **badge**, showing a live count. ## One commit here is not phase 5, and says so `fix(ci): stop setup-android installing a package Google has removed`. **Included here on the org lead's call (2026-09-17)**, because it blocks verifying anything at all. `android-actions/setup-android@v3` is a floating tag and the action's `packages` input defaults to `tools` — an obsolete package Google has since removed from the SDK repository. The step runs `sdkmanager tools`, gets `Warning: Failed to find package 'tools'`, exits 1, and CI fails in *Set up Android SDK* before a line of this repo is compiled. PR #46 passed on this same workflow yesterday; every Android PR fails now. `packages: ''` turns that install off, and it was always redundant here — the next step installs exactly what the build targets (`platform-tools`, `platforms;android-35`, `build-tools;35.0.0`) precisely so the build never depends on what an action decided to fetch. **Not addressed, and worth its own decision:** `@v3` is a floating major tag, so the next upstream change can break CI the same way without warning. ## Rig note worth keeping The debug `network_security_config.xml` permits cleartext to `127.0.0.1` and `localhost` only — **not** `10.0.2.2`. An emulator walk against a local core needs `adb reverse tcp:<port> tcp:<port>` and the loopback address, or every request fails with `UnknownServiceException: CLEARTEXT communication to 10.0.2.2 not permitted`. ## The four navigation tests that moved `APP_MENU` gained a row and the website's nav number line gained an index, so the merge's expected shapes shifted by one. Each now says which, and why both game modules appear in a table no single backend serves. ## Base branch This targets **`edge`**, per the org lead's decision to move the Rust workstream's repos and work onto `edge` (2026-09-16). `pr-checks.yml` already triggers on `[main, edge]`. ## AI disclosure - [x] This contribution was AI-assisted (Claude Code). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 2 commits 2026-09-17 08:41:35 +00:00
The app's half of module-rust's read path — the leg R10 says trails the website
surface it consumes by one phase, so it is built against routes that exist.

Two screens, mirroring what phase 4 shipped: `/rust` is the server list (D12),
and one server is a single screen with four tabs (D13) rather than four
destinations. Both render entirely from the website's own tables, so the phase
criterion — a fleet that is entirely off still shows its maps, seeds, wipe
dates, killfeeds, leaderboards and last known presence — holds here for the same
reason it holds on the web.

What is new to the app rather than copied:

- **A poll that is not a load.** `PollWhileResumed` + `refreshInto` (D17): a
  refresh is invisible when it succeeds and KEEPS the rows when it fails. The
  app had one shape for a read — blank, ask, replace — which is right for opening
  a screen and would clear the killfeed three times a minute here. Gated on
  RESUMED, so a backgrounded app makes no requests at all and returning to it
  refreshes at once.
- **A second game module in the drawer.** `Capability.RUST`, gating one row. It
  deliberately does not gate on `servers`/`killfeed`/`leaderboard`/`presence`/
  `wipes`: those name surfaces, core flattens every module's capabilities into
  one list, and another module declaring `servers` would reveal these screens on
  a site with no Rust. Module-Rust#5 adds the identity string.
- **`/rust` in NavPaths**, so an admin's nav override or an added link opens
  natively instead of handing off to a browser (D19).
- **A live player count on the drawer row** (D19) — the phone's answer to D15's
  footer slot, in the same badge slot the inbox count uses, with the same
  screen-reader treatment. Zero renders nothing; a failed read keeps the last
  number; it never polls.

Two things carried across from the website's own page walk rather than
rediscovered: "last reported" reads `lastSeenAt` and never `updatedAt` (a failed
poll moves the second), and a feed row from another calendar day carries its
date, or a row from a past wipe reads as this afternoon.

The four navigation tests that moved did so because APP_MENU gained a row and
the website's nav number line gained an index; each now says which.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
fix(rust): what the emulator walk found
Some checks failed
PR Checks / android-build (pull_request) Failing after 2s
a6677d5bf9
Three things, none of which a unit test could have seen.

**The drawer's live count resolved once per process.** It was keyed on the
capability answer alone, so it was read at connect and never again — which is
not what "live" means on a row somebody opens the drawer to look at. It now
refreshes on resume, beside the inbox's unread badge and for the same reason:
coming back to the app is exactly when a stale number would be noticed. Still
never on a timer, still nothing at all on a site without the module.

**Every card's text sat flush against its edge.** `ShardCard` is the themed
`Card` and carries no padding of its own — each caller pads its own content, and
these four did not. On a phone the first glyph of each line read as clipped.

**A name touched its own kill count.** Five numeric columns beside an
equal-weight name column left "Brannock" and "50" reading as one field. The name
now takes a wider share and ellipsizes, and the ACTIVE SORT is marked on the
header rather than by tinting a column of numbers — the header is the control,
and tinting the values says "these are special" instead of "this is what the
table is ordered by".

Walked against the phase-4 rig: a core with the module installed, one live
server and one that has never reported. Both halves of the phase criterion hold
on a phone — the Rust site renders every panel with its server unreachable, and
the same app against the UO core shows its five shard rows and no Rust row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-17 08:50:10 +00:00
fix(ci): stop setup-android installing a package Google has removed
Some checks failed
PR Checks / android-build (pull_request) Failing after 18m49s
daf483f514
**Unrelated to this PR's feature work**, and fixed here because it blocks
verifying it (org lead, 2026-09-17). PR #46 passed on this workflow yesterday;
every Android PR fails now.

`android-actions/setup-android@v3` is a floating tag and the action's `packages`
input defaults to `tools` — an obsolete package Google has since removed from the
SDK repository. So the step runs `sdkmanager tools`, gets `Warning: Failed to
find package 'tools'`, exits 1, and CI fails in **Set up Android SDK**, before a
line of this repo is compiled.

`packages: ''` turns that install off. It was always redundant here: the very
next step installs exactly what the build targets — `platform-tools`,
`platforms;android-35`, `build-tools;35.0.0` — precisely so the build never
depends on what some action decided to fetch.

Not addressed here, and worth its own decision: `@v3` is a floating major tag, so
the next upstream change can break CI the same way without warning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-17 09:10:50 +00:00
chore(ci): re-run
All checks were successful
PR Checks / android-build (pull_request) Successful in 11m23s
4b22ab3756
Run 76 hung in `compileDebugKotlin` for thirteen minutes and was failed with no
error in its log, where the last good run finished that task in two. Nothing
about that reads as a compile error, and this repo's Gitea has no rerun
endpoint — so this empty commit is the re-run, to tell a transient runner
problem from a real one before bisecting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
whitlocktech merged commit 37a828736e into edge 2026-09-17 09:22:02 +00:00
whitlocktech deleted branch feature/rust-p5-android-a 2026-09-17 09:22:03 +00:00
Sign in to join this conversation.
No description provided.