docs(rust): phase 11 plan of record — a Rust notification on a phone (PLAN.md §26, M16)
Four org-lead decisions (D69-D72): the linked-account check asks the site's own module and never gates switching off; a tickle is titled from the inbox item its ref names; /player/rust and ?tab= open natively; the relay hop is walked on ntfy.sh, closing M7's open QA item. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -1528,6 +1528,28 @@ 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
|
||||
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).
|
||||
|
||||
The inbox (engagement phase 8) and the preferences screen (engagement phase 3) are drawn from the
|
||||
wire, so every Rust trigger already arrives as a row, with no app change. This milestone covers
|
||||
the three places where the app still assumed it was talking to a UO shard:
|
||||
|
||||
- **The link check** behind the *Your game account* section asked `module-uo`'s
|
||||
`/player/shard/accounts`. On a Rust site that locks the raid alert in every channel. It will
|
||||
ask the site's own module, chosen by capability (`rust` → `/player/rust/links`). Switching a
|
||||
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:<id>` 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.
|
||||
- **`/player/rust` and `/rust/servers/<id>?tab=<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).
|
||||
|
||||
### Deferred (not a milestone)
|
||||
|
||||
- **Platform Teams in the app** — **deferred 2026-08-17, no app work scheduled.** The website is
|
||||
|
||||
@@ -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 | Android-app | A Rust notification arrives on a phone and can be switched off there |
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -3891,6 +3891,107 @@ the real `OnEntityDeath` path runs.
|
||||
- A Python heredoc halved a backslash in a JavaScript string again; the Edit tool is the safe path
|
||||
for anything with an apostrophe in it.
|
||||
|
||||
## 26. Phase 11 — Android leg C: a Rust notification on a phone (plan of record, 2026-09-23)
|
||||
|
||||
R10's third leg: the app half of phase 10. Almost nothing about it is Rust-specific, and that is
|
||||
the first finding. The inbox (engagement phase 8) and the preferences screen (engagement phase 3)
|
||||
are both drawn **from the wire**: every trigger phase 10 declared already appears as a row with its
|
||||
own channels, and an inbox item pointing at `/rust/servers/<id>` already opens the native server
|
||||
screen (phase 5 put that path in the app's web-path table). The phase is the three places where the
|
||||
app still assumed the site it talks to is a UO shard, plus the relay walk nobody has run yet.
|
||||
|
||||
Two repositories: [Android-app][aa] into `edge`, and this document into `main`. **Module-Rust is not
|
||||
expected to change**, and core is not: everything below is a client reading something the server
|
||||
already says.
|
||||
|
||||
### 26.0 The decisions this phase needed
|
||||
|
||||
All from the org lead, 2026-09-23.
|
||||
|
||||
| # | Decision |
|
||||
|---|---|
|
||||
| **D69** | **"Has a linked game account" is asked of the site's own module.** The app uses the capabilities it already reads (D16): `rust` asks `GET /player/rust/links`, and `shard` asks `GET /player/shard/accounts` as it does today. A site with neither has no link. One module per site (§24.5) makes that one call, never a merge. **Switching a channel off is never gated.** The link gates only switching **push on** for a `requiresLinkedAccount` item, which is the one thing §11 of the Android plan gave the flag. Rejected: dropping the gate entirely (the web has none, and the server enforces none, so this was a fair option), and having core report `linked` per item (a contract member and a `MODULE_API` bump to answer a question the app can ask itself). |
|
||||
| **D70** | **A tickle with a `notification:<id>` ref is titled from that inbox item, pulled over the authenticated API.** This is the wake-and-pull contract core's `pushChannel` states in as many words: the ref is there so the app can fetch the real item, and the relay never sees it. When the ref is null, the fetch fails, or the item is not on the page, the notification falls back to today's per-stream title. On the lock screen it shows only that fallback title. Rejected: titling from the catalogue's label (less specific, and it needs a cache that can go stale), and adding the four Rust ids to the app's fixed `PushStreams` list (it ties an app release to a module's trigger ids). |
|
||||
| **D71** | **Two links that already have a native screen stop opening the browser.** `/player/rust` (the *account linked* notice) goes to `PLAYER_RUST`. `/rust/servers/<id>?tab=<tab>` (the *new leader* notice) opens the server on that tab, **only** when the query is exactly `tab=` and the value is a tab the app has. That is the rule `?run=` already follows for events. `?tab=clans` still goes to the browser, because the app has no clans tab. |
|
||||
| **D72** | **The walk proves the relay hop on public `ntfy.sh`.** The org lead's instruction was to reuse whatever `module-uo` did first. It did nothing: the Android plan's §11 has listed *on-device delivery against a live ntfy* as M7's one open QA item since M7 landed, and no walk has closed it since. Core publishes only to a public HTTPS host (`pushDispatch.isAllowedEndpoint`), so a local relay on `127.0.0.1` is refused by design. What leaves the rig is `{ stream, ref }` on a random topic and nothing else, the same thing that leaves a production site. |
|
||||
|
||||
### 26.1 Facts the phase rests on
|
||||
|
||||
**From the app** (`Android-app`, `edge`):
|
||||
|
||||
- **`NotificationSettingsViewModel` decides "linked" by calling `PlayerShardRepository.accounts()`**,
|
||||
which is `module-uo`'s `/player/shard/accounts`. On a Rust site that route does not exist, the call
|
||||
fails, and a failure reads as *not linked*. The screen then disables the **whole row** of every
|
||||
personal item. The raid alert can't be switched on or off in any channel, including in-app, which
|
||||
the link has nothing to do with. The criterion's second half fails on this alone.
|
||||
- **`PushNotifier` titles every tickle from `PushStreams.titleRes`**: eight fixed ids from M7, all
|
||||
UO or core, and "New notification" for anything else. A raid alert arrives saying nothing.
|
||||
- **`PushService` handles tickles inside `collectLatest`.** A second tickle cancels whatever the
|
||||
handler for the first was still doing. That was harmless while the handler did not suspend. A pull
|
||||
(D70) suspends, so each one must run in its own job, or a burst (two authorised owners' alerts, a
|
||||
wipe and a restart) loses all but the last.
|
||||
- **Tapping a tickle with a `notification:*` ref opens the inbox** (`Routes.forTickle`, engagement
|
||||
phase 8), and the inbox opens the item's link natively when `resolveWebPath` knows it. Nothing
|
||||
here changes that. D71 only widens what `resolveWebPath` knows.
|
||||
- **The web page's tabs are `feed`, `leaderboard`, `online`, `wipes` and `clans`. The app has the
|
||||
first four.**
|
||||
|
||||
**From core** (`website`, `main`):
|
||||
|
||||
- **There is no single-item inbox read.** The inbox is a keyset-paged list, so the pull reads the
|
||||
first page and looks for the id. The item has just been written, so it is on that page. If it is
|
||||
not, D70's fallback applies.
|
||||
- **A GET of the inbox marks nothing read.** The pull leaves the badge as it was.
|
||||
- **`requiresLinkedAccount` is not enforced anywhere on the server**, and the web settings screen
|
||||
has no gate. The app's gate is a courtesy, not a boundary, and D69 keeps it one: what decides who
|
||||
is alerted is phase 10's recipient computation (D59).
|
||||
- **Push defaults off for engagement rules until a person opts in**, which §25.5 observed on the
|
||||
rig. The criterion's first half therefore needs the app's toggle to work before anything can
|
||||
arrive.
|
||||
|
||||
### 26.2 What the plan builds
|
||||
|
||||
**Android-app** (`feat/rust-phase-11-notifications`, from `edge`):
|
||||
|
||||
- **The link check (D69).** A small `LinkedAccountChecker` over `SiteCapabilitiesRepository`,
|
||||
`PlayerRustRepository` and `PlayerShardRepository`, pure enough to test without Compose.
|
||||
`itemSelectable` becomes a question about **one channel and one direction**: every channel is
|
||||
live, and a push-on for a `requiresLinkedAccount` item is refused with the existing hint when
|
||||
there is no link.
|
||||
- **The pull (D70).** `PushService` launches each tickle in its own job on the service scope rather
|
||||
than inside `collectLatest`. `PushNotifier` gains a suspend path: parse the `notification:<id>`
|
||||
ref, read the first inbox page under a short timeout, and post with the item's title and body, or
|
||||
with the per-stream title when anything is missing. `VISIBILITY_PRIVATE` with a public version
|
||||
carrying only the fallback title. The ref parser is prefix-exact, the same rule as `forTickle`.
|
||||
- **The two links (D71).** `resolveWebPath` learns `/player/rust`, and learns `tab` as the second
|
||||
query key it honours, on the Rust server path only. `RUST_SERVER` gains an optional `tab`
|
||||
argument, and the server screen opens on it.
|
||||
- **Tests** for each of the three, including the one that matters most: a Rust site with a link
|
||||
lets push be switched on for the raid alert, and a site with no link still lets every channel be
|
||||
switched off.
|
||||
|
||||
**docs:** this section as built, the phase row, and the Android plan's M16 and §11. §11 had
|
||||
said that nothing is fetched to show a notification, and it has to stop saying so.
|
||||
|
||||
### 26.3 Done when
|
||||
|
||||
The row's criterion: **a Rust notification arrives on a phone and can be switched off there.**
|
||||
Walked on an emulator against real core on `main` with this module installed, and the Oxide rig's
|
||||
sidecar behind it:
|
||||
|
||||
1. On a Rust site, the app's settings let a linked player switch the raid alert's push on. The same
|
||||
screen with no link leaves push refused and every other channel live.
|
||||
2. The device registers an endpoint on `ntfy.sh` (D72). A raid made with phase 10's rig helper
|
||||
(`RgRaidRig.cs`) is delivered as a **system notification titled from the item**. It shows the
|
||||
fallback title on the lock screen and opens the inbox when tapped. The inbox row opens the server
|
||||
natively.
|
||||
3. The player switches the raid alert's push off in the app. A second raid produces **no** system
|
||||
notification, while the in-app row still arrives if in-app is still on.
|
||||
4. A UO site is unchanged: its personal rows still gate on `/player/shard/accounts`.
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
[aa]: https://gitea.whitlocktech.com/RunicGateway/Android-app
|
||||
|
||||
Reference in New Issue
Block a user