docs(modules): phase 8 as built, and the rig that finally put the sidecar where the design says it lives
Three documents: * `modules/rust/PLAN.md` §22 — the phase as built. Three org-lead decisions (D39-D41), what a player is told and what they are not, the refusals on a phone, and an honest limit the rig found: a rank can be live while every permission it carries resolves nowhere. * `rust-link/INSTALL_RIG.md` — new. The sidecar runs INSIDE the game container now, which is the shape R20 says the egg ships and which retires the firewall wall phases 6, 7 and 7b each stopped at. A container's 127.0.0.1 is genuinely private, so a stock plugin config and a stock sidecar find each other with nothing configured at all. Three things in the launcher are load-bearing and each is written down with the failure it prevents. * `rust-link/PLAYER_WALK.md` — the account walk on a phone, and a correction: 7b's "it needs a firewall rule on a development machine" is no longer true. `android/PLAN.md` gains M15. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSM
This commit is contained in:
@@ -1429,6 +1429,88 @@ push, and Play (M6–M8) follow the designed app.
|
||||
*configuration*, which the app consumes and does not edit. Identity and permissions are legs B
|
||||
and C (phases 8 and 11), and the map is leg D.
|
||||
|
||||
16. **M15 — the player's own Rust account** (post-v1; built 2026-09-22). `module-rust` phase 8, and
|
||||
R10's leg B: identity (phase 6) and the half of site-owned permissions (phase 7) a player is
|
||||
allowed to see. Every route existed and answered before a line of Kotlin was written except one,
|
||||
`GET /player/rust/permissions`, which this phase added on the website for exactly this screen.
|
||||
|
||||
**Design of record: [`../modules/rust/PLAN.md`](../modules/rust/PLAN.md)**, §22 for the phase as
|
||||
built and its three decisions. The contract is normative there; this entry records what the app
|
||||
does about it.
|
||||
|
||||
#### It is `CharactersScreen` for a different game, deliberately
|
||||
|
||||
The org lead's instruction was to mirror `module-uo`, and the mirror is exact: **one drawer row
|
||||
under the player group**, with the code card at the top of the screen it opens — the same shape
|
||||
UO has had since M4, where the `[link` card sits above the character rosters. Two alternatives
|
||||
were rejected for reasons the mirror makes obvious: a tab under one Rust server (a link is
|
||||
**fleet-wide** — one Steam account is one person on every server, while stats are per server and
|
||||
per wipe), and a section inside core's own Account screen (the app has no slot mechanism, so the
|
||||
module's data would be hard-wired into a core screen).
|
||||
|
||||
| Screen | Route | Reads |
|
||||
| --- | --- | --- |
|
||||
| **My Rust account** | `player/rust` | `GET /player/rust/links`, `POST /player/rust/link`, `DELETE …/links/{steamId}`, `GET /player/rust/permissions` |
|
||||
|
||||
#### The gate is `rust`, and `PLAYER` means staff too
|
||||
|
||||
`module-uo`'s five shard rows all hang on `shard`, and this hangs on `rust` for the same reason
|
||||
D16 gave: a capability answers *is the module there*, and core flattens every started module's
|
||||
capabilities into one list, so a surface word like `identity` is not something a row may hang
|
||||
on. `MenuAccess.PLAYER` is `isPlayer || isStaff`, which is right here — `/player/rust/*` is
|
||||
`requireAuth` with no role above it, and staff play the game as well.
|
||||
|
||||
#### Two reads, and neither blocks the other
|
||||
|
||||
The accounts and the entitlements load separately and fail separately. That is not tidiness: an
|
||||
entitlement is authored against the **website** account, so it exists before a Steam id does, and
|
||||
the person who has just been given something and has not linked yet is exactly the one who needs
|
||||
to see both halves at once. The screen says so in as many words when nothing is linked.
|
||||
|
||||
**The app does no scope arithmetic.** `*` never reaches a screen: each entry arrives carrying the
|
||||
servers its scope reaches, each already marked *has it* or *waiting*, because a second
|
||||
implementation of that rule is a second thing to keep true.
|
||||
|
||||
#### The refusals stay four pieces of advice
|
||||
|
||||
A refusal is chosen by **status** and rendered from a string resource, the convention every
|
||||
earlier milestone follows (the app is localized; the website's sentence is not). 400 is a spent
|
||||
code, 409 is a Steam account another website account holds — released with `/unlink` in game,
|
||||
never moved silently — 429 is the server's limiter, and 503 is a server that could not be
|
||||
reached. The last of those may **not** say "get a new code": the code is still good, and a player
|
||||
told otherwise goes back to the same unreachable server for another one.
|
||||
|
||||
The known cost, written down rather than discovered later: the website distinguishes three 503s
|
||||
by sentence (one server unreachable, the whole fleet down, no servers configured at all) and the
|
||||
app has one string for the status, written to be true of all three.
|
||||
|
||||
#### Verified
|
||||
|
||||
The app suite (**657 tests, 0 failures**), `lintDebug`, `assembleDebug`, and an emulator walk
|
||||
against a core with the module installed and a **live Oxide rig** behind it — the first Rust leg
|
||||
where the backend was talking to a real game server rather than a stand-in.
|
||||
|
||||
Walked directly: the row absent signed-out and absent on a UO site, present for a signed-in
|
||||
player; both reads; a code the live plugin genuinely refused, with its advice rendering **beside
|
||||
the button** rather than at the top of a long form; a rank marked *has it* and a grant marked
|
||||
*waiting* on the same screen, which is what the pushed ledger actually said; and the release.
|
||||
|
||||
#### The walk found one thing the suite did not
|
||||
|
||||
**The row said who, and not when or where.** The website's row has always read "linked just now
|
||||
on rust-oxide"; the app's carried the name and the Steam id and stopped. Neither fact is part of
|
||||
the identity — a link is fleet-wide — but which server minted the code is where a support
|
||||
conversation starts, and the module's own schema says so in a comment.
|
||||
|
||||
- **Excluded**, in the same class as M14's exclusions: the Rust **admin** permission surface.
|
||||
Authoring grants, groups and drift is `requireRole('admin')` on the website, the app has no
|
||||
admin user-detail screen to put it in, and it writes into a running game — the phone is where
|
||||
you read what you hold, not where you decide what somebody else holds.
|
||||
- **No deep link yet.** `/player/rust` is not in the app's web-path table, deliberately:
|
||||
`module-uo`'s player screens are not either, and that table is built from the *public* nav.
|
||||
Phase 10 is when it will matter, because a notification about an entitlement will want
|
||||
somewhere to land.
|
||||
|
||||
### Deferred (not a milestone)
|
||||
|
||||
- **Platform Teams in the app** — **deferred 2026-08-17, no app work scheduled.** The website is
|
||||
|
||||
Reference in New Issue
Block a user