docs(rust): phase 11 as built and walked — §26.4-26.6, M16

The raid alert reached an emulator through ntfy.sh 19 seconds after a
rig raid, titled from its inbox row; switched off in the app, the next
raid enqueued in-app only. Closes M7's open QA item (on-device delivery
against a live ntfy), and amends the M7 and engagement phase 8 passages
D69 and D70 made stale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-23 15:40:02 -05:00
parent 83e57e6f47
commit f1b8329540
2 changed files with 110 additions and 8 deletions

View File

@@ -997,7 +997,7 @@ Each phase ends with its findings written down, as every workstream here does.
| 8 | **Android leg B** (R10), and the half of R2 a player may see. ✅ **Built 2026-09-22 — as built and findings in §22.** One drawer row under the player group shaped like `module-uo`'s own (the code card first, then what the code got them), gated on `rust` and `PLAYER`; the four refusals kept four pieces of advice; and **one new website route**, `GET /player/rust/permissions`, because phase 7 gave an operator every view of in-game privilege and a player none. Three org-lead decisions (§22.0), the scope arithmetic answered on the server so `inScope` exists once (§22.1), and **the rig moved the sidecar inside the game container** ([`INSTALL_RIG.md`](../../rust-link/INSTALL_RIG.md)), which retired the firewall wall phases 6, 7 and 7b each stopped at | Android-app + Module-Rust + docs | A player links from the app |
| 9 | **Teams from first-party clans** (R5). *Preceded by the presence fix (§23), which this phase's roster question produced.* ✅ **Built 2026-09-23 — plan, as built and findings in §24** (D47–D58). Walked on the Oxide rig end to end and on Carbon hook by hook; the game's 100-clan ceiling accepted (D55); **one module per site** became a core rule on the way (§24.5). Membership event-driven, leadership read off `LocalClan` at snapshot; **`declareModuleSlot` × 3** for core's `team.notify` / `team.activity` / `team.forum` | Module-Rust + 2 | The clan page is ours, core's contributions land in places we named, and every slot empty still reads correctly |
| 10 | **Notifications and engagement** (R7). ✅ **Built and walked 2026-09-23 — plan, as built and findings in §25 (D59–D68).** The criterion walked on the Oxide rig (two authorised owners alerted once each; bystander, raider and an unlinked owner told nothing), and the same plugin loaded on Carbon; a live walk found that every generic notice never said which server (§25.6). Streams, triggers with `ceiling` and `subjectKey`, audiences and engagement seeds — **the catalogue is §10, as corrected by §25.2**. The announce leg and the post hook are **not** in it (D62 amends R7), and protocol 7 widens the raid frame, which brings the two bridge repositories in | all 3 + docs | The offline raid alert reaches the player whose base it was, and nobody else |
| 11 | **Android leg C** (R10). Inbox and notification preferences for Rust triggers. **Plan of record in §26 (D69–D72)**: the link check asked of the site's own module, a tickle titled from the item it points at, two links that stop opening the browser, and the relay hop walked on `ntfy.sh` for the first time on any site | Android-app | A Rust notification arrives on a phone and can be switched off there |
| 11 | **Android leg C** (R10). ✅ **Built and walked 2026-09-23 — plan, as built and findings in §26 (D69–D72).** The link check behind personal streams asked `module-uo` and locked the raid alert on every Rust site; it now asks the site's own module and holds back only push-on. A tickle is titled from the inbox row it names, and two links stopped opening the browser. **The relay hop was walked on `ntfy.sh` for the first time on any site**: a rig raid reached the emulator in 19 seconds, and after push was switched off in the app the next raid enqueued in-app only | Android-app + docs | A Rust notification arrives on a phone and can be switched off there |
| 12 | **Events: budgets, option sources and the leases** (§9). [kit][kit] ch. 5's own ordering — leases before actions — and every key verified live before it is advertised | Module-Rust + 2 | A leased value is observed changing in the running game and restored, per key; `rust.group.membership` expires without core asking |
| 13 | **Events: the actions** (§9, R3, R16). `rust.kit.entitle` first, then `rust.prefab.place` and `rust.announce`; `reversible: 'ledger'`; the kit option source flags kits with no permission gate, plus **`reconcile()` and the boot-id watch calling `ctx.events.reconcile()`** (§11.1) | all 3 + docs | A reward granted at 03:00 is waiting in the kit menu when the player next logs in, and a revert withdraws it; a wipe reconciles the ledger instead of stranding it |
| 14 | **The live map** (R9). The map image over the bridge — request/reply, two-stage, one in flight, its own derivation version, no import on boot — plus the live layers and a per-layer public/players/admin switch built on **our own** visibility layer (§11.2 — `shardVisibility` is `module-uo`'s, not core's) | all 3 + docs | The map renders for the current wipe, and a player layer is invisible until an operator deliberately opens it |
@@ -3992,6 +3992,77 @@ sidecar behind it:
If the relay hop cannot be made on `ntfy.sh`, it is written down as a walk still to run, and the
rest is proven with the tickle injected by `adb`, the extras `PushNotifier` builds.
### 26.4 As built, 2026-09-23
**Android-app** (`feat/rust-phase-11-notifications`, into `edge`):
- **`LinkedAccountRepository`** asks `/player/rust/links` on a `rust` site and
`/player/shard/accounts` on a `shard` site. A host that has never answered gets both asked, and a
host that named neither module gets neither.
- **`pushNeedsLink` and `canSetMode`** replace `itemSelectable`. The row stays live, the hint names
the push control, and a held push switch that is already on can still be turned off.
- **`PushContentResolver`** pulls the first inbox page under a five-second timeout. It titles only
from a row whose id matches the ref **and** whose trigger matches the tickle's stream. The
notification is `VISIBILITY_PRIVATE`, with a public version carrying the generic title.
`PushService` launches each tickle in its own job.
- **`resolveWebPath`** learns `/player/rust`, and `tab` on a Rust server path. `RustTab` gained its
wire names, and `RUST_SERVER` an optional `tab` argument.
The suite is **672 tests, 0 failures**, up from 657, with `lintDebug` and `assembleDebug` green.
**Module-Rust and core: no change**, as planned. This is the fifth Rust phase in a row that core did
not need to change for.
**The walk.** Real core on `main` with this module installed, the protocol-7 sidecar inside the Oxide
rig's container, the `s22_ultra` emulator, and `NTFY_PUBLIC_URL`/`NTFY_BASE_URL`/`NTFY_ALLOWED_ORIGINS`
all set to `https://ntfy.sh` (D72).
1. **Linked (raidowner1):** the raid alert's push switch was live. Switching it on registered
`https://ntfy.sh/<topic>` as the device's endpoint and stored the subscription. With no link
(walkadmin), push was held with the hint, while email and in-app stayed live. In-app was switched
off, stored as `off`, and switched back on.
2. **Raid, then the phone:** `rgraid.door` on the live rig, attacker `…09`, authorising raidowner1's
Steam id. Core dispatched to **two** channels (in-app and push). **Nineteen seconds** after the
console command, the phone posted *"Your base is being raided — A door was destroyed in P16 on
Oxide rig."* It was `vis=PRIVATE`, with a public version. The tap opened the inbox, and the row
opened the Oxide rig's server screen natively.
3. **Switched off on the phone:** push off in the app. The server then held no subscription and no
device, since nothing else had push on. The next raid dispatched to **one** channel. The inbox
row arrived (*"A door was destroyed in R15"*), no system notification was posted, and the push
service had stopped.
4. **D71:** rows seeded with the module's own URLs. `?tab=leaderboard` opened on the Leaderboard tab,
`/player/rust` opened *My Rust account*, and `?tab=clans` went to a Custom Tab.
**The criterion is met: a Rust notification arrived on a phone and was switched off there.**
### 26.5 What the walk found
Nothing the suite had missed. That's the first leg of this workstream to say so, and the reason is
worth keeping: every defect this phase fixed was found **by reading** before any code was written
(§26.1). The UO-shaped link check was visible in one line of the view model. The generic title was
visible in `PushStreams`. The cancelled pull was a property of `collectLatest` that nobody had needed
to think about until a handler suspended.
Two things are worth writing down:
- **The first real relay hop on any site went cleanly.** M7 built the distributor in 2026-07 and
nobody had delivered a tickle through a live ntfy to a device since. It worked the first time,
including registration against core's endpoint validator and the unregister when the last push
preference went off.
- **Two channels, then one, is the proof that "off" was honoured by the server, not by the phone.**
The app never saw the second raid's push: core did not enqueue it. A phone that merely stopped
showing notifications would have looked the same on the screen and different in the log.
### 26.6 What is not proven here
- **A UO site, walked.** No UO core was running. The UO branch is the same call it always was, and
`aUoSiteStillAsksTheShardModuleAndNeverTheRustOne` pins it.
- **The lock screen, seen.** The emulator has no screen lock. The public version was read back from
`dumpsys notification`, not looked at.
- **A burst.** Each tickle now runs in its own job, which is what a burst needs, but the walk sent
one raid at a time.
- **A real raid by a real player**, for §25.7's reason: the rig has one Steam account.
---
[aa]: https://gitea.whitlocktech.com/RunicGateway/Android-app