feat(rust): Admin → Rust servers page and the next wipe on the web (phase 16)
All checks were successful
PR Checks / client-build (pull_request) Successful in 26s
PR Checks / frozen-manifest (pull_request) Successful in 49s
PR Checks / server-tests (pull_request) Successful in 7m49s

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
This commit is contained in:
2026-09-25 13:26:10 -05:00
parent 0670341198
commit cddba957d3
8 changed files with 450 additions and 11 deletions

View File

@@ -125,6 +125,29 @@ export function shortId(steamId) {
return id.length > 10 ? `…${id.slice(-6)}` : id
}
/**
* The next wipe (phase 16, D130), as `Thu, Oct 1, 7:00 PM (in 6 days)` in the
* VIEWER's locale and clock — or `null` when the server has no schedule.
*
* The server computes the instant from the operator's rule in the operator's
* zone; the page only says it the reader's way. A `once` source is an operator
* moving a wipe, and says so, because "the wipe is not when it usually is" is
* the thing a regular needs to notice.
*/
export function nextWipe(value, now = Date.now()) {
if (!value || !value.at) return null
const at = toMillis(value.at)
if (at === null) return null
const when = new Date(at).toLocaleString(undefined, {
weekday: 'short',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
})
return `${when} (${ago(at, now)})${value.source === 'once' ? ' — rescheduled' : ''}`
}
function toMillis(value) {
if (value === null || value === undefined || value === '') return null
if (typeof value === 'number') return Number.isFinite(value) ? value : null
@@ -133,4 +156,4 @@ function toMillis(value) {
return Number.isNaN(parsed) ? null : parsed
}
export default { ago, clock, day, duration, count, prefab, shortId }
export default { ago, clock, day, duration, count, prefab, shortId, nextWipe }