From 1d596d0efab631de02b92a6c0f25cfe3f6c52570 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 15:43:32 -0500 Subject: [PATCH] feat(dashboard): restarts from 30 seconds; no "undefined" in the restart banner Restarts can be scheduled in seconds (30 s to 24 h; minutes still accepted), with 30 second and 1 minute choices for quick restarts. The restart banner only names who scheduled it when that is known: staff who manage restarts read it from /api/server/restart, everyone else gets when and why from the status, which leaves the name out. Co-Authored-By: Claude Opus 5.5 --- dDashboardServer/routes/EventsCalendar.cpp | 2 +- dDashboardServer/routes/LiveWorld.cpp | 19 +++++++++++-------- dDashboardServer/routes/LiveWorld.h | 2 +- dDashboardServer/static/js/dashboard.js | 6 ++++-- dDashboardServer/templates/index.jinja2 | 8 ++++---- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/dDashboardServer/routes/EventsCalendar.cpp b/dDashboardServer/routes/EventsCalendar.cpp index ad55421dc..c9b2356c1 100644 --- a/dDashboardServer/routes/EventsCalendar.cpp +++ b/dDashboardServer/routes/EventsCalendar.cpp @@ -463,7 +463,7 @@ namespace { if (when == "start") part.status = "Restarts when the event ends"; return true; } - const auto error = LiveWorld::ScheduleRestart(part.config.value("minutes", 15), Text(part.config, "reason"), run.actor, false); + const auto error = LiveWorld::ScheduleRestart(part.config.value("minutes", int64_t{ 15 }) * 60, Text(part.config, "reason"), run.actor, false); part.status = error ? "Not scheduled: " + *error : "Restart scheduled when it " + std::string(when == "start" ? "started" : "ended"); return true; } diff --git a/dDashboardServer/routes/LiveWorld.cpp b/dDashboardServer/routes/LiveWorld.cpp index 5243dff8a..6c54d09fd 100644 --- a/dDashboardServer/routes/LiveWorld.cpp +++ b/dDashboardServer/routes/LiveWorld.cpp @@ -31,6 +31,9 @@ namespace { constexpr const char* RESTART_STATE = "scheduled_restart"; // Warnings before a scheduled restart, in seconds; only those shorter than the notice given are sent constexpr int64_t WARNINGS[] = { 3600, 1800, 900, 600, 300, 120, 60, 30, 10 }; + // How soon a scheduled restart may be (short ones are for quick restarts while nobody is on), and how late + constexpr int64_t MIN_RESTART_SECONDS = 30; + constexpr int64_t MAX_RESTART_SECONDS = 24 * 3600; struct WorldPlayers { PlayerPositions positions; @@ -149,17 +152,17 @@ namespace { } namespace LiveWorld { - std::optional ScheduleRestart(int64_t minutes, const std::string& reason, const HTTPContext& actor, bool replace) { - if (minutes < 1 || minutes > 1440) return "Pick between 1 minute and 24 hours"; + std::optional ScheduleRestart(int64_t seconds, const std::string& reason, const HTTPContext& actor, bool replace) { + if (seconds < MIN_RESTART_SECONDS || seconds > MAX_RESTART_SECONDS) return "Pick between 30 seconds and 24 hours"; if (reason.size() > 300) return "The reason can be at most 300 characters"; if (!g_RestartLoaded) LoadRestart(); if (g_Restart && !replace) return "A restart is already scheduled (by " + g_Restart->by + ")"; - g_Restart = Restart{ static_cast(std::time(nullptr)) + minutes * 60, reason, actor.authenticatedUser, 0 }; + g_Restart = Restart{ static_cast(std::time(nullptr)) + seconds, reason, actor.authenticatedUser, 0 }; SaveRestart(); g_NextPush = {}; // send the first warning right away - Audit(actor, "schedule_restart", "In " + Duration(minutes * 60) + (reason.empty() ? "" : ": " + reason)); - Alerts::Emit("server", "Restart scheduled", "The server restarts in " + Duration(minutes * 60) + ".", + Audit(actor, "schedule_restart", "In " + Duration(seconds) + (reason.empty() ? "" : ": " + reason)); + Alerts::Emit("server", "Restart scheduled", "The server restarts in " + Duration(seconds) + ".", { { "By", actor.authenticatedUser }, { "Reason", reason } }, "/"); return std::nullopt; } @@ -292,13 +295,13 @@ namespace LiveWorld { JsonReply(reply, eHTTPStatusCode::OK, RestartStatus(true)); }); - Route(eHTTPMethod::POST, "/api/server/restart", Perm("server_restart"), "Schedule a restart with in-game warnings. Body: {minutes (1-1440), reason}. Needs a process supervisor to start the server again", + Route(eHTTPMethod::POST, "/api/server/restart", Perm("server_restart"), "Schedule a restart with in-game warnings. Body: {seconds (30-86400) or minutes (1-1440), reason}. Needs a process supervisor to start the server again", [](HTTPReply& reply, const HTTPContext& context) { const auto body = ParseBody(context); if (!body) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, "Invalid JSON"); - const auto minutes = body->value("minutes", 0); + const int64_t seconds = body->contains("seconds") ? body->value("seconds", int64_t{ 0 }) : body->value("minutes", int64_t{ 0 }) * 60; const std::string reason = body->value("reason", ""); - if (const auto error = LiveWorld::ScheduleRestart(minutes, reason, context, true)) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, *error); + if (const auto error = LiveWorld::ScheduleRestart(seconds, reason, context, true)) return JsonError(reply, eHTTPStatusCode::BAD_REQUEST, *error); JsonSuccess(reply, { {"message", "Restart scheduled"}, {"restart", RestartStatus(true)} }); }); diff --git a/dDashboardServer/routes/LiveWorld.h b/dDashboardServer/routes/LiveWorld.h index 13af3a9c3..0ad39405c 100644 --- a/dDashboardServer/routes/LiveWorld.h +++ b/dDashboardServer/routes/LiveWorld.h @@ -33,7 +33,7 @@ namespace LiveWorld { * Schedule a restart in `minutes` (1 to 1440) with in-game warnings, audited as `actor`. One already scheduled is * replaced when `replace`, or kept with an error. The error, if it wasn't scheduled */ - std::optional ScheduleRestart(int64_t minutes, const std::string& reason, const HTTPContext& actor, bool replace); + std::optional ScheduleRestart(int64_t seconds, const std::string& reason, const HTTPContext& actor, bool replace); // Scheduled restart for the status sent to every dashboard ({} when none) // The scheduled restart ({} when none); the staff member's name only with withStaffName diff --git a/dDashboardServer/static/js/dashboard.js b/dDashboardServer/static/js/dashboard.js index 2491da77f..e7c042ffb 100644 --- a/dDashboardServer/static/js/dashboard.js +++ b/dDashboardServer/static/js/dashboard.js @@ -210,7 +210,7 @@ function tick() { var left = Math.max(0, Math.round(restart.at - Date.now() / 1000)); var text = left >= 60 ? Math.floor(left / 60) + ' min ' + (left % 60) + ' s' : left + ' s'; - banner.textContent = 'Server restart in ' + text + (restart.reason ? ': ' + restart.reason : '') + ' (scheduled by ' + restart.by + ')'; + banner.textContent = 'Server restart in ' + text + (restart.reason ? ': ' + restart.reason : '') + (restart.by ? ' (scheduled by ' + restart.by + ')' : ''); } tick(); restartTimer = setInterval(tick, 1000); @@ -304,6 +304,8 @@ if (initial) initial.textContent = (document.body.dataset.username || '?').charAt(0).toUpperCase(); connect(); if (DASH.can('moderate_names')) api.get('/api/moderation/counts').then(updateBadges).catch(function () {}); - api.get('/api/status').then(function (s) { showRestart(s.restart); }).catch(function () {}); + // Staff who manage restarts also see who scheduled it; everyone else gets when and why from the status + if (DASH.can('server_restart')) api.get('/api/server/restart').then(function (r) { showRestart(r && r.at ? r : null); }).catch(function () {}); + else api.get('/api/status').then(function (s) { showRestart(s.restart); }).catch(function () {}); }); })(); diff --git a/dDashboardServer/templates/index.jinja2 b/dDashboardServer/templates/index.jinja2 index 6f5d3ef2d..a6f581428 100644 --- a/dDashboardServer/templates/index.jinja2 +++ b/dDashboardServer/templates/index.jinja2 @@ -77,9 +77,9 @@ The server shuts down cleanly; your process supervisor (systemd, Docker restart policy...) starts it again.

In - + +
@@ -112,7 +112,7 @@ $(function () { }; schedule.addEventListener('click', function () { if (!confirm('Schedule a restart? Everyone online will be warned and then disconnected.')) return; - api.action('/api/server/restart', { minutes: parseInt(document.getElementById('restartMinutes').value, 10), reason: document.getElementById('restartReason').value.trim() }, 'Restart scheduled') + api.action('/api/server/restart', { seconds: parseInt(document.getElementById('restartSeconds').value, 10), reason: document.getElementById('restartReason').value.trim() }, 'Restart scheduled') .then(function (r) { showRestart(r.restart); }).catch(function () {}); }); document.getElementById('restartCancel').addEventListener('click', function () {