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 () {