Commit Graph

12 Commits

Author SHA1 Message Date
bfe9b5b18d feat(rust): Dangerous Treasures — feed, map, Treasure Hunter, Start a Dangerous Treasures event (PLAN_REDESIGNS §14.3.4)
All checks were successful
PR Checks / client-build (pull_request) Successful in 27s
PR Checks / frozen-manifest (pull_request) Successful in 46s
PR Checks / server-tests (pull_request) Successful in 7m23s
Stage 5 of the §14.3 integration plan, on protocol 14 (docs
rust-link/PROTOCOL.md §20.8).

- dt.started and dt.won are public kinds, dt.ended a staff kind. The new
  dangerousTreasures.js reads each row per viewer: grid and invaderCount for
  everyone, the winner's name for the presence audience (D376), the position,
  the step's key, the winner's Steam id and the invaders for staff.
- Feed lines in the World events filter: "A Dangerous Treasures event has
  started · K12", "Alice won the treasure · K12 · 3 players fought for it".
- Triggers rust.dt.started (with byEvent) and rust.dt.won, public, cooldown
  subject the server, names only.
- eventDangerousTreasures.js: rust.dt.start, "Start a Dangerous Treasures
  event" (risk change, reversible none, budget rust.dt.events), server and
  difficulty. Refused at verify where the server said DT is not loaded or
  console.plugins is false (Rust-Plugins#34). dt-not-ready answers "Dangerous
  Treasures is not ready yet" with retryAfterMs 600000 (D365, D371);
  dt-no-spot retries on core's delay; DT's own refusals are permanent.
- Treasure Hunter (D375): treasuresWon -> treasures_won, the title category
  treasureswon, and a sortable Treasures column shown where the server
  reports DT or a row counts one.
- The map draws a running chest on the world layer; the server settings'
  plugin line names DT and whether a step can start one, from the
  integrations read's new `console`.
- coreApi ^1.15.0, ci/core-ref.json pinned to website main at #215's merge.
  ci/bundle.json ships the two new files; swagger fragment and engagement
  manifest regenerated; README lists DT.

Server 627/627 (16 new in test/dangerousTreasures.test.js) and every check;
client 92/92, build and externals. Booted against the pinned core: 45
triggers, 13 actions, 11 budgets, 21 option sources; 89 routes unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-15 06:45:30 -05:00
549b3a5ffd feat(rust): Dynamic PVP's zones on the feed and the map (PLAN_REDESIGNS §14.3.3)
All checks were successful
PR Checks / client-build (pull_request) Successful in 25s
PR Checks / server-tests (pull_request) Successful in 31s
PR Checks / frozen-manifest (pull_request) Successful in 44s
Stage 4 of the §14.3 integration plan, on protocol 14 (docs
rust-link/PROTOCOL.md §20.7).

- pvp.zone.created is a public kind, pvp.zone.deleted a staff kind. Each row
  is read per viewer by the new dynamicPvp.js: grid for everyone, the event's
  name only for Dynamic PVP's own vanilla events, position and zone id for
  staff only.
- The feed's world filter carries the opening: "A PvP zone opened around the
  downed patrol helicopter · G7 · for 10 min". It names nobody.
- The map gains a fifth layer, `pvp` (PvP zones), public by default with its
  own audience in Admin → Rust visibility, drawn as a red ring with its
  event's name.
- Server settings' plugin line names Dynamic PVP; the swagger schemas and
  route descriptions carry the layer and the integration (fragment
  regenerated); ci/bundle.json ships dynamicPvp.js; README lists it.

Tests: server 611/611 (dynamicPvp.test.js new, catalogue and map extended)
and every check; client 89/89 (feed and map tooltip), build and externals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-15 06:12:07 -05:00
e7d5c8ed1a feat(rust): Rust's own world events — feed, credit rule, triggers, Call a world event (PLAN_REDESIGNS §14.3.1)
All checks were successful
PR Checks / client-build (pull_request) Successful in 31s
PR Checks / server-tests (pull_request) Successful in 36s
PR Checks / frozen-manifest (pull_request) Successful in -1m32s
The site half of stage 1, on protocol 14:

- server/worldEvents.js: the 24 types, D372's four credit rules, the grid
  from the map's geometry (none off the map), and the per-viewer reading.
- The feed serves `world.event` (a public kind): an unknown type is staff's;
  the event and its grid are public; names only for the presence audience
  (D376); Steam ids and positions only for staff (D353). A downed one carries
  `credited`/`creditedCount` by the server's rule. Client lines for all 24
  types and a World events filter.
- rust_servers.credit_rule (blow, last, most, all; default blow), set on the
  server's settings. Ingest counts heli_kills (patrol helicopters and
  Chinooks, D379) and apc_kills from `world.event` by that rule, so the line
  and Rotorbane / Tankbreaker agree. A protocol-13 tally's heliKills/apcKills
  still add as before.
- The tally's cratesHacked / dropsLooted → crates_hacked / drops_looted;
  title categories Safecracker and Scavenger (D356); leaderboard columns
  Crates hacked and Drops looted, sortable (D377).
- 24 engagement triggers rust.world.<event> (D355): ceiling everyone,
  cooldown subject the server and event (crates add where), names only.
- server/eventWorldCall.js: rust.world.call, "Call a world event"
  (risk change, reversible none, budget rust.world.calls). A position for an
  airdrop or a patrol helicopter only; the Chinook takes none (D378).

Server 585/585 (22 new in test/worldEvents.test.js), client 84/84, every
check; booted against the pinned core (43 triggers, 12 actions, 10 budgets,
20 option sources), routes.manifest unchanged at 89. Engagement manifest and
swagger fragment regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-15 00:39:00 -05:00
e4591af906 feat(rust): Backpacks — staff and player views, the Bigger backpack step (PLAN_REDESIGNS §11.3)
All checks were successful
PR Checks / server-tests (pull_request) Successful in 37s
PR Checks / frozen-manifest (pull_request) Successful in 1m0s
PR Checks / client-build (pull_request) Successful in 7m23s
The site half of §11.3.2, on protocol 14:

- `rust.backpack.size` ("Bigger backpack"): a kit reward with a different
  permission (D337) — one rust_perm_run_grants row per website user with
  backpacks.size.N, no kit, no credit; the perm mirror pushes it and the run's
  revert deletes it. risk change, reversible ledger, a new `rust.backpacks`
  budget, rust.kit.entitle's recipient modes including `linked`. Refused with
  "Backpacks is not installed on this server" in the dry run too (D199), and a
  size the server does not offer is refused by name. Its message is never
  silent: empty is the take-back warning (D338).
- `rust.options.backpacksizes`: each server's sizes above its smallest, from
  the last hello, grouped by server.
- GET /player/rust/backpacks (own links only, 20 reads a minute) and
  GET /admin/users/:id/rust/backpacks (that user's links only). Live, never
  stored; a stranger's id is a 404 and never reaches the game.
- Client: a shared Backpack panel (pick server and account, open, read once)
  on the account page and the admin user page; the grid draws pages of
  pageSize, each slot named by the game (D340, no icons), blueprints by what
  they make (D341), overflow marked. Backpacks joins the server's mod line.

Server 548/548 (13 new in test/backpacks.test.js), client 79/79 (5 new),
routes.manifest regenerated against the pinned core (89 routes), swagger
fragment regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-12 14:35:55 -05:00
845caef817 feat(rust): Economics — pay coins, balances, staff adjust, the richest (PLAN_REDESIGNS §11.2)
All checks were successful
PR Checks / server-tests (pull_request) Successful in 31s
PR Checks / client-build (pull_request) Successful in 29s
PR Checks / frozen-manifest (pull_request) Successful in -1m29s
The site half of §11.2.2, on protocol 14:

- `rust.coins.pay` ("Pay coins"): irreversible, reversible none (D335). The
  recipient modes of rust.kit.entitle including `linked`, one payment per
  website user (D103's rule), an optional message with {amount}, a new
  `rust.coins` budget. Refused with "Economics is not installed on this
  server" in the dry run too (D199). Core's key goes to the plugin, so a
  retried step answers `repeat` and pays nobody twice.
- `rust_player_wipe_stats.balance`: the tally's balance, written as a value.
  A nameless balance frame (an offline player) moves nobody's last-seen.
  `economics.wiped` clears the wipe's balances, except at a new save.
- The richest: a Coins leaderboard column on one wipe of a server that
  reports Economics, `sort=balance`, and a `richest` title category
  (Magnate); both rank only above the starting balance (D336).
- GET /player/rust/economics (live, falling back to the last known), and
  GET/POST /admin/users/:id/rust/economics for staff — scoped to the user's
  own links, keyed per submission, an activity row with before and after.
- Client: the account page's Coins, the admin user page's Coins panel with
  its form, the leaderboard column, and Economics in the server's mod line
  (saying when its config could not be read).

Server 535/535 (14 new in test/economics.test.js), client 74/74,
routes.manifest regenerated against the pinned core (87 routes), swagger
fragment regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-12 07:07:05 -05:00
5162496d74 feat(rust): event NPCs named on the map, and a Place NPCs step's progress line (runicnpc stage 8)
All checks were successful
PR Checks / client-build (pull_request) Successful in 20s
PR Checks / server-tests (pull_request) Successful in 25s
PR Checks / frozen-manifest (pull_request) Successful in -1m18s
- rust.npc.place's progress() (MODULE_API 1.12.0, D304). It reports the
  step's NPCs still standing, by its key, of how many it placed, with
  the profile's label: "Bandit: 2 of 3 left". A boss step is its
  health, "Walk Juggernaut: 59%", and a boss's adds are not counted.
  The data comes from the map's live answer, so it costs the game no
  extra ask. A reader who may not see the map's events layer gets no
  line.
- The map (D302, D303):
  - an NPC's tooltip is its name and its event's title, read from
    core's public calendar, which lists only announced events;
  - a boss is bigger and in its own colour, and an add says so;
  - a boss outside any event is in the world layer;
  - a boss's health is removed from what a viewer is sent.
- The world layer's visibility hint mentions those bosses.

coreApi stays ^1.11.0: an older core ignores the optional member, and
the action works without a progress line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-06 08:23:13 -05:00
7eae4a1e62 feat(rust): NPC profiles and placements pages, the profile leaderboard (runicnpc stage 4)
Admin: Rust NPC profiles (the form RunicNPC reads, per server, shared or
fleet, each server's push state and refusals, replaced profiles with
Restore) and Rust NPC placements (the live map: click to place, pins for
every placement; edit, rename, respawn, remove). The live map takes a pick
handler and pins, unchanged for the public page.

Public: the leaderboard ranks by a profile's kills (D250), and opening a
row shows that player's kills by profile (D252); the killfeed names a
RunicNPC NPC by its own name. Player: your own kills by profile. Titles: a
rule on a profile's kills picks the profile. npcs.test.js covers the
profile checks, adoption, the push, the picker and verb, the triggers, kill
crediting, titles and placements (481 server tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-30 04:42:05 -05:00
1b70cef5be feat(rust): chat titles, BetterChat group styles, the voice and popups (phase 17)
PLAN.md §33, D134-D143. Protocol 12.

- Chat titles (D135-D137): per-server rules (stat, top N, text, colour)
  that rank the current wipe, and a mode (first | all | up to N). Worked
  out once in model/titles and read three ways: pushed whole to the game by
  a new titleSync loop (on change, restart or wipe), and on every
  leaderboard row as `titles`. Admin: PUT /servers/:id/titles.
- Group styles (D138, D139): a site group may carry all twelve BetterChat
  fields (rust_perm_group_chat). They ride perm.sync with `expect` from the
  pushed ledger, which gains a value column; a field changed in game is a
  `chat-field` drift row with the game's value, adopted into the style or
  put back. A withdrawn style is one `chat-group` retirement, never for
  `default`, cleared from the ledger only once BetterChat removed it.
- The voice (D140): one fleet setting naming a styled group; news and
  rust.announce chat lines carry its format and the plugin says them with
  no sender. Admin: GET/PUT /voice.
- Popups (D141, D142): rust.announce gains `delivery` (still version 1,
  from rust.options.delivery); each server gains news_delivery beside the
  news switch; `popup-unavailable` is not retried.
- GET /servers/:id/integrations reads, live, which optional mods a server
  has loaded. README lists BetterChat and PopupNotifications as optional.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-25 17:48:22 -05:00
0cb9bdd1f0 feat(rust): the live map (phase 14, protocol 11)
All checks were successful
PR Checks / server-tests (pull_request) Successful in 28s
PR Checks / client-build (pull_request) Successful in 27s
PR Checks / frozen-manifest (pull_request) Successful in -1m9s
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
2026-09-25 01:06:10 -05:00
c94271104f feat(rust): Teams from first-party clans (phase 9, protocol 6)
All checks were successful
PR Checks / server-tests (pull_request) Successful in 24s
PR Checks / frozen-manifest (pull_request) Successful in 46s
PR Checks / client-build (pull_request) Successful in 8m3s
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
2026-09-23 05:14:18 -05:00
be44839896 fix(rust): nothing names who is online by default
All checks were successful
PR Checks / client-build (pull_request) Successful in 27s
PR Checks / frozen-manifest (pull_request) Successful in 51s
PR Checks / server-tests (pull_request) Successful in 8m6s
The org lead's rule, settled 2026-09-22: who is online is always the
narrowest audience - staff - unless an operator deliberately widens it,
and a count is fine where a list of names is not.

The public site broke that in three places since phase 4. The Online
tab named every player, the feed carried joins, respawns, deaths, chat
and tallies, and the leaderboard's lastSeen - refreshed every minute by
a gather tally - said who was on as plainly as either. All three now
sit behind one setting:

* PRESENCE_KINDS, a subset of the public allowlist, gated per request.
  Below the audience the feed keeps the server's own story (wipe, start,
  shutdown) and says presenceHidden rather than looking quiet.
* the Online route answers { players: [], hidden, count, audience } -
  same shape, so an older client renders empty rather than breaking.
* rungs staff / signed_in / public, fleet-wide default in a new
  rust_settings table with an optional per-server override on
  rust_servers; an unknown stored word narrows to staff.
* the viewer's standing is RE-READ from the users row (ctx.users.getById),
  not taken from the token, so a demotion or a ban applies on the next
  request. Walked: a moderator demoted mid-session lost the roll call on
  the same cookie.
* per-viewer answers are Cache-Control: private, no-store.
* GET/PUT /admin/rust/visibility (requireRole admin) and an admin page,
  Rust visibility; every save is one activity-log row.

The browser walk also found every empty state in this module rendering
as a blank box. Core's EmptyState renders children only; this module
passed title/message (the shape the Integration Kit template teaches)
and React dropped both without a word. Fixed module-side with a small
Empty wrapper - nothing core or module-uo renders changes - and a client
test that refuses a titled EmptyState or a PageHeader subtitle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-09-23 00:30:08 -05:00
22fd8c5da7 feat: the first pages, and what a browser walk found behind them
All checks were successful
PR Checks / client-build (pull_request) Successful in 15s
PR Checks / frozen-manifest (pull_request) Successful in 36s
PR Checks / server-tests (pull_request) Successful in 7m58s
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
2026-09-16 21:40:28 -05:00