From cc185db26babb5bbe4b1ceb8a06831e1bdd19e8a Mon Sep 17 00:00:00 2001 From: wtclaude Date: Thu, 24 Sep 2026 07:00:44 -0500 Subject: [PATCH] =?UTF-8?q?feat(rust):=20the=20rewards=20=E2=80=94=20tally?= =?UTF-8?q?,=20kit=20reward,=20chat=20and=20the=20news=20leg=20(phase=2013?= =?UTF-8?q?b,=20protocol=2010)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four event verbs and the announce leg, per PLAN.md §29: - rust.participation.open / .collect: the plugin counts who takes part (seconds, kills or both, in a zone this run opened or the whole server) and collect files them as the run's participants, keyed by Steam id. - rust.kit.entitle: the five recipient modes (D101), rows in the new rust_perm_run_grants (D84) unioned into the permission push, one extra use of the kit per reward as site-held credits on perm.sync (D103), and the rust.kit.entitled notice deferred from phase 10 (D64). - rust.announce: one server or every server (D105). - rust.chat announce leg, speaking only on servers whose new news switch is on (D104) - a card on Admin -> Rust visibility (D106). Budgets rust.grants and rust.announcements; the kit source and four fixed-choice sources (core has no enum param type). rust_perm_run_grants carries core's idempotency key so a revert of a lost answer can find its rows. Protocol 10. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- ci/bundle.json | 1 + client/src/routes/admin/Visibility.jsx | 50 +- engagement-triggers.json | 97 ++ server/db/purge.sql | 3 + server/db/schema.sql | 48 + server/engagement/emit.js | 31 + server/engagement/seeds.js | 21 +- server/engagement/triggers.js | 35 +- server/eventRewards.js | 854 ++++++++++++++++++ server/index.js | 44 +- server/model/permissions/permissions.db.js | 77 ++ server/model/permissions/permissions.model.js | 68 +- server/model/servers/servers.db.js | 3 +- server/model/servers/servers.model.js | 24 +- server/model/visibility/visibility.db.js | 12 +- server/model/visibility/visibility.model.js | 24 +- server/permSync.js | 4 + server/router/admin/visibility.controller.js | 4 +- server/router/admin/visibility.router.js | 5 +- server/sidecarClient.js | 33 +- server/swagger/doc.js | 24 + server/test/engagement.test.js | 5 +- server/test/entry.test.js | 44 +- server/test/permissions.test.js | 58 ++ server/test/rewards.test.js | 419 +++++++++ server/test/visibility.test.js | 25 + swagger-fragment.json | 128 ++- 27 files changed, 2096 insertions(+), 45 deletions(-) create mode 100644 server/eventRewards.js create mode 100644 server/test/rewards.test.js diff --git a/ci/bundle.json b/ci/bundle.json index c5d0c57..3df9ecf 100644 --- a/ci/bundle.json +++ b/ci/bundle.json @@ -34,6 +34,7 @@ "db", "engagement", "eventLeases.js", + "eventRewards.js", "eventWorld.js", "index.js", "ingest.js", diff --git a/client/src/routes/admin/Visibility.jsx b/client/src/routes/admin/Visibility.jsx index 428d2fc..041018e 100644 --- a/client/src/routes/admin/Visibility.jsx +++ b/client/src/routes/admin/Visibility.jsx @@ -19,6 +19,12 @@ // lists each server's clan board: a server whose clans cannot be read, one at // the game's 100-clan ceiling (D55), and one running the uMod Clans plugin, // whose clans are a separate system and never Teams (D47). +// +// Phase 13b adds a third (D104, D106): whether a published news post is also +// said in each server's in-game chat. Off by default, because core sends every +// post to every registered leg — without a switch, the day this module updated, +// every post would start appearing in every server's chat. It lives here because +// this is the one page that lists every server with a setting of its own. import { useCallback, useEffect, useState } from 'react' @@ -87,6 +93,7 @@ export default function Visibility() { const [fleet, setFleet] = useState('staff') const [clanRoster, setClanRoster] = useState('members') const [servers, setServers] = useState({}) + const [news, setNews] = useState({}) const [busy, setBusy] = useState(false) const [error, setError] = useState('') const [saved, setSaved] = useState(false) @@ -98,6 +105,7 @@ export default function Visibility() { setFleet(state.presence.fleet) setClanRoster((state.clans && state.clans.roster) || 'members') setServers(Object.fromEntries(state.presence.servers.map((s) => [s.id, s.override || INHERIT]))) + setNews(Object.fromEntries(((state.news && state.news.servers) || []).map((s) => [s.id, Boolean(s.on)]))) }, []) useEffect(() => { @@ -114,7 +122,9 @@ export default function Visibility() { const dirtyServers = rows.filter((s) => (servers[s.id] ?? INHERIT) !== (s.override || INHERIT)) const clans = data.clans || { audiences: [], roster: 'members', servers: [] } const dirtyClans = clanRoster !== clans.roster - const dirty = dirtyFleet || dirtyServers.length > 0 || dirtyClans + const newsRows = (data.news && data.news.servers) || [] + const dirtyNews = newsRows.filter((s) => Boolean(news[s.id]) !== Boolean(s.on)) + const dirty = dirtyFleet || dirtyServers.length > 0 || dirtyClans || dirtyNews.length > 0 const effective = (id) => servers[id] || fleet const widened = fleet !== 'staff' || rows.some((s) => effective(s.id) !== 'staff') @@ -131,6 +141,7 @@ export default function Visibility() { if (dirtyServers.length) { body.servers = Object.fromEntries(dirtyServers.map((s) => [s.id, servers[s.id] || null])) } + if (dirtyNews.length) body.news = Object.fromEntries(dirtyNews.map((s) => [s.id, Boolean(news[s.id])])) load(await api.adminVisibility.save(body)) setSaved(true) setReloads((n) => n + 1) @@ -225,6 +236,43 @@ export default function Visibility() { + +

+ When a news post is published, its title is said in the chat of every server switched on + here. A server that is down when a post is published is skipped rather than told late. +

+ {newsRows.length === 0 && ( +

No servers are configured yet.

+ )} + {newsRows.map((s) => ( + + ))} +
+