diff --git a/android/PLAN.md b/android/PLAN.md index 6ce6396..95e1f64 100644 --- a/android/PLAN.md +++ b/android/PLAN.md @@ -229,7 +229,8 @@ Verified green: `:app:testDebugUnitTest` (18 new JVM tests — notifications DTO parse incl. malformed, topic/URL building, stream→route map + personal gating) + `:app:lintDebug` + `:app:assembleDebug`; backend 250 tests (+3 for `push.ntfyUrl`) and `npm run swagger` clean. The foreground-service tradeoff (§11) and the POST_NOTIFICATIONS runtime permission are implemented as -planned; an on-device delivery pass against a live ntfy is the one open QA item. +planned; an on-device delivery pass against a live ntfy is the one open QA item. *(Closed by M16 on +2026-09-23: a raid alert delivered through `ntfy.sh` to an emulator, 19 seconds end to end.)* **Part 2 (original plan) — the Android app.** UnifiedPush receiver + device registration against the merged Part-1 contract, a Notifications settings screen, and notification-tap deep-links. @@ -313,7 +314,10 @@ Work items: `GET …/streams` with a per-stream toggle bound to `GET/PUT …/subscriptions`; a **personal** stream (`requiresLinkedAccount`) is greyed with a "link a game account" hint until the user has a linked account — reuse the linked-accounts signal already fetched for M4's player surface - (`PlayerShardRepository`), not a fresh source of truth. Toggling to a non-empty set triggers the + (`PlayerShardRepository`), not a fresh source of truth. *(M16 amends both halves: the signal is now + asked of the site's own module by capability (`LinkedAccountRepository`), because on a Rust site + `PlayerShardRepository` answered no for everyone. It holds back only switching push **on**, and no + longer greys the whole row.)* Toggling to a non-empty set triggers the register flow (#5) and requests `POST_NOTIFICATIONS`; emptying the set unregisters. Each mutation folds its `ApiResult` into a section-scoped, localized banner (§7 parity with M4). 7. **Deep-links (resolves the §13 open item).** Tapping a notification opens the app to the stream's @@ -324,7 +328,8 @@ Work items: `label`) and deep-links — it does **not** pull `ref` content first; the content-free design means nothing needs decrypting to render the tap, and the target screen fetches fresh over the authenticated API on open. (Pulling `ref` for a richer inline notification is a possible later - enhancement, not v1.) + enhancement, not v1.) *(M16 did it for `notification:` refs: `PushContentResolver` pulls the + inbox row and titles the notification with it. Every other ref keeps the generic title.)* 8. **Menu.** Add a **Notifications** entry to the signed-in group in `ui/navigation/Menu.kt` (near My Account), visible once signed in. 9. **Permission UX.** Request `POST_NOTIFICATIONS` at the moment the user first enables a stream (API @@ -1528,7 +1533,7 @@ push, and Play (M6–M8) follow the designed app. once the fleet was widened to signed-in — the app's bearer session reaching the module's viewer check. Branch `fix/rust-presence-visibility`. -17. **M16 — a Rust notification on a phone** (post-v1; planned 2026-09-23). `module-rust` phase +17. **M16 — a Rust notification on a phone** (post-v1; built 2026-09-23). `module-rust` phase 11, R10's leg C: the app half of phase 10's notifications. **Design of record: [`../modules/rust/PLAN.md`](../modules/rust/PLAN.md) §26**, with its four decisions (D69–D72). @@ -1542,14 +1547,38 @@ push, and Play (M6–M8) follow the designed app. channel **off** is never gated, and only switching push **on** needs a link. - **A push is titled from the item it points at.** A `notification:` ref is pulled over the authenticated inbox API, and the relay still carries nothing. The per-stream title stays as the - fallback and as the lock-screen version. §11's "nothing is fetched to show a notification" - stops being true, and it gets rewritten when this lands. + fallback and as the lock-screen version. The M7 items that said nothing is + fetched to show a notification are amended in place. - **`/player/rust` and `/rust/servers/?tab=` open natively.** M15 deferred the first of these to exactly this phase. It also closes **M7's open QA item**. On-device delivery against a live ntfy has never been walked. It is walked here, on `ntfy.sh` (D72). + #### As built (2026-09-23) + + Branch `feat/rust-phase-11-notifications`. The app suite is **672 tests, 0 failures**, up from + 657, with `lintDebug` and `assembleDebug` green. It was walked on the `s22_ultra` emulator + against real core on `main`, with the module installed, the protocol-7 sidecar inside the Oxide + rig's container, and the relay on `ntfy.sh`: + + - **Linked (raidowner1):** the raid alert's push switched on, and the device registered + `https://ntfy.sh/`. A door raided with phase 10's rig helper reached the phone + **19 seconds** later as *"Your base is being raided — A door was destroyed in P16 on Oxide + rig."* The notification was `vis=PRIVATE`, with a public version carrying only the generic + title. Tapping it opened the inbox, and the row opened the Oxide rig's screen natively. + - **Switched off in the app:** push off, and the next raid enqueued in-app only. The inbox row + arrived and no system notification was posted. With nothing else on push, the device was + unregistered and the push service stopped. + - **Not linked (walkadmin):** push held back with the hint, while email and in-app stayed live. + In-app was switched off, stored as `off`, and switched back on. + - **D71:** a row linking `?tab=leaderboard` opened on the Leaderboard tab, and `/player/rust` + opened *My Rust account*. `?tab=clans` went to a Custom Tab. + + **Not walked:** a UO site (no UO core was running; the path is the call it always was, and a test + pins it), and the lock-screen rendering (the emulator has no screen lock; the public version was + read back from `dumpsys notification`). + ### Deferred (not a milestone) - **Platform Teams in the app** — **deferred 2026-08-17, no app work scheduled.** The website is @@ -1772,7 +1801,9 @@ of push. push streams, so `team.forum.post` would have landed on Home. `Routes.forTickle(stream, ref)` sends anything whose ref starts with `notification:` to the inbox and leaves every other tickle on the route it has always had. The ref is never decoded past that prefix and never rendered - it is a hint - that a row exists, and the contract stays wake-and-pull. + that a row exists, and the contract stays wake-and-pull. *(M16 decodes it once more, to the id, +and only to pull that row over the authenticated inbox API and title the notification with it. It is +still never rendered.)* **The settings screen** (`NotificationSettingsScreen` + `NotificationSettingsViewModel`) moved off `/notifications/subscriptions` onto `/notifications/channels`. Controls are rendered from the wire: diff --git a/modules/rust/PLAN.md b/modules/rust/PLAN.md index 4e04289..9a4eba6 100644 --- a/modules/rust/PLAN.md +++ b/modules/rust/PLAN.md @@ -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/` 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