The module had no page for its own server rows: they were written only
through PUT /admin/rust/servers/:id, and the README described an
"Admin → Rust" server form that did not exist. D133 (org lead) builds it:
add, edit, test and delete a server, with the D130 wipe schedule in the
same form. The token stays write-only and an edit sends the stored
protocol back rather than re-stamping the row.
The next wipe shows on the server list and in the server page's header,
in the reader's own clock, marked "rescheduled" for a one-off date.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
PLAN.md §30 as approved, plus D119/D120 from the build.
Server:
- rust_map_images (one row per server: picture as MEDIUMBLOB, geometry,
monuments, DERIVATION_VERSION) and rust_map_overrides; purge.sql pair.
- mapImages.js: D110. The board poll notices a new boot/wipe/seed/size and
asks map.info; a new key or hash from the free Rust+ cache (or a render
kept on disk) is fetched in slices, checked against its SHA-256 and stored
in one statement. One fetch per server, a backoff on failure, `stale`
abandons a fetch that straddles a map change. Render now (D109) is
admin-only and watched to completion.
- mapLive.js: D111. One map.live per server per 5 s whoever asks; positions
are held in memory only.
- model/map: four layers (world, events public; players, bases staff), a
fleet default plus per-server override (D114), the players layer capped by
presence (D113), own dot and online first-party clan mates for a linked
viewer (D115, D117, D118). A layer the viewer may not see is absent from
the answer, never sent and hidden.
- Routes: public /servers/:id/map, /map/image (immutable under its hash),
/map/live; admin /servers/:id/map/fetch and /render; the Map card on the
visibility PUT. Swagger fragment and frozen manifest regenerated.
Client:
- A Map tab: Leaflet over the picture in CRS.Simple, the game's own grid
(labels only when a cell is wide enough to hold one), a legend that lists
hidden layers with who can see them, polled every 10 s while visible.
- D120: Leaflet is a lazy split chunk beside entry.js, not in it. release.yml
copies every dist/*.js; checkExternals and build.test.js hold both ends.
- The Map card on Admin -> Rust visibility, with Fetch again and Render now.
Capability `map` declared for the Android app (phase 15).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
A first-party Rust clan is a Team (R5). This module becomes the site's
Team provider and answers core from the plugin's `clans` board. Design
of record: docs/modules/rust/PLAN.md §24, D47-D58.
- The store: rust_clans, rust_clan_members and rust_clan_boards. A clan's
identity is <serverId>:<clanId>:<createdMs> (D52), because the game
restarts clan ids whenever its clan database version changes.
- The provider (D53): getTeams is complete only when every server's
board is fresh, supported and untruncated. It is partial when some
are, and refuses when none are. Freshness is judged by the website's
clock, from when the board's `t` last advanced.
- Only a complete board may mark a clan gone. A board at the game's
100-clan ceiling (D55), or one with an unreadable row, proves nothing
about what it leaves out.
- Leadership is diffed board to board and published (D54). The five clan
events are published as team.* kinds, and written to the Team feed as
members-only lines (D49).
- Core only writes feed items for a Team it already holds. So the last 10
minutes of clan events are re-offered on each board refresh, deduped by
a sha1 key: core clamps a dedupeKey to 40 characters, and a readable key
would be truncated into collisions.
- projectRoster and the clan page share one audience rule (D48): the
clan's linked members and staff by default, re-read from the users row.
The setting lives on Admin > Rust visibility, which also warns about
uMod Clans (D47) and the ceiling.
- Public: GET servers/:id/clans (the list is public, D58) and
GET clans/:externalId. The client adds a Clans tab and
/rust/clans/:externalId, with three module slots for core's notify,
activity and forum contributions (D56).
- Linking and unlinking an account ask core to reconcile Teams (D57).
- The clan kinds are staff-class in the public feed allowlist.
- PROTOCOL_VERSION is now 6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Phase 4. `/rust` is the server list and the module's landing page (D12);
`/rust/servers/:id` is one server with four tabs — feed, leaderboard, who is
on, wipes (D13). Everything selectable lives in the URL, so any view of the
page is a link. The feed and the presence list poll every twenty seconds while
the tab is visible and not at all when it is not (D14); the leaderboard and the
wipe list load once. `site.footer.status` is filled with a live server and
player count (D15).
Nothing on these pages calls a game server. Every field comes from this
module's own tables, which is what the phase criterion is about: the site
renders the last thing each server said while every server is off.
Walking that criterion in a browser against a live rig found four defects, two
of them already shipped in phase 3:
* An unreachable refresh called `putState` — the whole-row write — with two
fields, so a host that rebooted lost its hostname, map, size, seed and wipe
id. The list then read "Offline" with nothing beside it, which is not "here
is what we know" but "we have never heard of it". `markUnreachable` now
moves three columns and mentions no others.
* "Last reported" read `updated_at`, which a FAILED poll writes too — so an
offline server claimed it had reported just now, every thirty seconds, for
as long as it stayed down. `last_seen_at` is the new column, moved only by a
frame that arrived.
* Feed rows showed a bare time of day, so three events from six weeks ago all
read as this afternoon once the feed was filtered to a past wipe.
* `/rust/servers/typo` rendered core's ErrorState under its own heading and
read "No such server / Something went wrong", sending a reader who mistyped
a URL looking for an outage.
Also: a detail route (`GET …/servers/:id`), because it is the only route under
that path that can say a server does not exist — the other four answer an empty
list for an id nobody configured, and each of those is a good answer to its own
question.
`useAsync` cannot poll: it blanks its data on every dependency change, so a
twenty-second refresh built on it would clear the killfeed and re-fill it four
times a minute. `hooks/usePolled.js` is the module's own, invisible when it
succeeds and keeping the rows when it fails.
The client test fake was *nearly* core — it prefixed routes without stripping
the trailing separator, so the first module to register an index route failed
the nav check for a link that works in a browser. It now copies core's line
character for character.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4