feat(rust): slash commands and the next wipe, server half (phase 16)

Five read-only commands registered with api.registerSlashCommands:
/status, /wipe, /top, /online and /clan (D126). Every refusal is private,
and any answer narrower than public (online names, a clan roster) goes
to the caller alone (D127). No command asks a sidecar.

The next wipe (D128, D130): six nullable columns on rust_servers, a pure
nextWipe(row, now) with the zone arithmetic through Intl, computed on
every read. The public server shape gains nextWipe; the admin shape
gains the stored schedule; PUT /admin/rust/servers/:id takes the six
fields and writes them only when wipeRule is present.

server/commands joins ci/bundle.json, which checkBundle caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-25 13:23:11 -05:00
parent cf4d183181
commit 0670341198
22 changed files with 1985 additions and 31 deletions

View File

@@ -16,6 +16,7 @@
const core = require('../../core')
const db = require('./servers.db')
const { nextWipe } = require('./nextWipe')
const log = core.logger('servers')
@@ -136,6 +137,27 @@ function shapePublic(row, state, now) {
lastSeenAt: lastSeenAt ? lastSeenAt.toISOString() : null,
updatedAt: updatedAt ? updatedAt.toISOString() : null,
stale,
// Phase 16 (D130): `{ at, source }` or null, computed from the operator's
// schedule on every read — never stored, so it cannot go stale after a wipe.
// Public: a wipe date is announced to bring players back, not kept secret.
nextWipe: nextWipe(row, now),
}
}
/**
* The schedule as stored, for the admin form: what the operator typed, not what
* it computes to. A one-off date that has passed is still returned, and the form
* shows it as past (§32.4 reading 5) rather than silently dropping it.
*/
function scheduleOf(row) {
const once = row.wipeOnceAt ? new Date(row.wipeOnceAt) : null
return {
rule: row.wipeRule || 'none',
day: row.wipeDay == null ? null : Number(row.wipeDay),
time: row.wipeTime || null,
tz: row.wipeTz || null,
anchor: row.wipeAnchor || null,
onceAt: once && !Number.isNaN(once.getTime()) ? once.toISOString() : null,
}
}
@@ -184,6 +206,7 @@ async function listForAdmin(now = Date.now()) {
reachable: Boolean(state && state.reachable),
bootId: (state && state.bootId) || null,
sidecarProtocol: state && state.protocol != null ? Number(state.protocol) : null,
schedule: scheduleOf(row),
}
})
}
@@ -204,5 +227,6 @@ module.exports = {
getPublic,
listForAdmin,
shapePublic,
scheduleOf,
encryptToken,
}