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

@@ -24,6 +24,8 @@ async function listServers({ enabledOnly = false } = {}) {
return core.query(
`SELECT id, name, sidecar_base_url AS sidecarBaseUrl, sidecar_token_enc AS sidecarTokenEnc,
protocol, enabled, sort_order AS sortOrder, announce_news AS announceNews,
wipe_rule AS wipeRule, wipe_day AS wipeDay, wipe_time AS wipeTime, wipe_tz AS wipeTz,
DATE_FORMAT(wipe_anchor, '%Y-%m-%d') AS wipeAnchor, wipe_once_at AS wipeOnceAt,
created_at AS createdAt, updated_at AS updatedAt
FROM ${SERVERS}
${enabledOnly ? 'WHERE enabled = 1' : ''}
@@ -34,7 +36,10 @@ async function listServers({ enabledOnly = false } = {}) {
async function getServer(id) {
const rows = await core.query(
`SELECT id, name, sidecar_base_url AS sidecarBaseUrl, sidecar_token_enc AS sidecarTokenEnc,
protocol, enabled, sort_order AS sortOrder, created_at AS createdAt, updated_at AS updatedAt
protocol, enabled, sort_order AS sortOrder,
wipe_rule AS wipeRule, wipe_day AS wipeDay, wipe_time AS wipeTime, wipe_tz AS wipeTz,
DATE_FORMAT(wipe_anchor, '%Y-%m-%d') AS wipeAnchor, wipe_once_at AS wipeOnceAt,
created_at AS createdAt, updated_at AS updatedAt
FROM ${SERVERS}
WHERE id = ?`,
[id],
@@ -71,6 +76,26 @@ async function upsertServer({ id, name, sidecarBaseUrl, sidecarTokenEnc, protoco
)
}
/**
* Write one server's wipe schedule (phase 16, D130), all six columns at once.
*
* Its own statement rather than six more columns on `upsertServer`, because the
* schedule is written only when a save CARRIES one: a client that predates the
* schedule and posts the rest of the row must not reset it to `none`.
*
* `wipeOnceAt` is a Date or null. The pool negotiates the session's zone
* (`timezone: 'auto'` in core), so a Date written here reads back as the same
* instant.
*/
async function setSchedule(id, { wipeRule, wipeDay, wipeTime, wipeTz, wipeAnchor, wipeOnceAt }) {
await core.query(
`UPDATE ${SERVERS}
SET wipe_rule = ?, wipe_day = ?, wipe_time = ?, wipe_tz = ?, wipe_anchor = ?, wipe_once_at = ?
WHERE id = ?`,
[wipeRule, wipeDay, wipeTime, wipeTz, wipeAnchor, wipeOnceAt, id],
)
}
async function deleteServer(id) {
await core.query(`DELETE FROM ${SERVERS} WHERE id = ?`, [id])
}
@@ -188,6 +213,7 @@ module.exports = {
listServers,
getServer,
upsertServer,
setSchedule,
deleteServer,
listState,
getState,