Panel independent of the game server: ServerHost owns port + panel and starts/stops the game server; panel works while stopped/crashed/unconfigured (settings, save ZIP upload, backups, bans offline); crash auto-restart; players told 'offline'; tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 19:02:08 +02:00
co-authored by Claude Opus 5.5
parent f8aad82d18
commit c74c48dfc8
11 changed files with 1012 additions and 417 deletions
+40 -19
View File
@@ -200,11 +200,12 @@ async function refreshTopbar() {
const s = await api("GET", "status");
state.status = s;
$("serverName").textContent = s.name;
$("serverMeta").textContent = s.gameStopped
? `game server stopped · v${s.version}`
: `${s.players}/${s.maxPlayers} players · v${s.version} · up ${duration(s.uptimeSeconds)}`;
$("statusDot").className = "status-dot " + (s.gameStopped ? "" : "ok");
$("statusDot").title = s.gameStopped ? "Game server stopped" : "Online";
const running = s.state === "running";
$("serverMeta").textContent = running
? `${s.players}/${s.maxPlayers} players · v${s.version} · up ${duration(s.uptimeSeconds)}`
: `server ${s.state} · v${s.version}`;
$("statusDot").className = "status-dot " + (running ? "ok" : s.state === "error" ? "bad" : "");
$("statusDot").title = running ? "Online" : "Server " + s.state + (s.lastError ? ": " + s.lastError : "");
document.title = `${s.name} · MSCO Panel`;
} catch {
$("statusDot").className = "status-dot bad";
@@ -215,9 +216,9 @@ async function refreshTopbar() {
function renderDashboard(page) {
const cards = h("div", { class: "cards" });
const control = h("div", { class: "section-body row" });
const control = h("div", { class: "section-body control" });
if (can("Admin")) page.append(h("div", { class: "section" },
h("div", { class: "section-head" }, h("h2", {}, "Server control"), h("span", { class: "muted", id: "gameState" }, "")),
h("div", { class: "section-head" }, h("h2", {}, "Server"), h("span", { class: "state-label", id: "gameState" }, "")),
control));
const recent = h("div", { class: "console small" });
page.append(
@@ -260,20 +261,40 @@ async function serverAction(action, confirmText) {
}
await act(api("POST", "server/" + action));
setTimeout(refreshTopbar, 800);
setTimeout(refreshTopbar, 2500);
}
const STATE_TEXT = {
running: "Running - players can join",
starting: "Starting …",
stopped: "Stopped - players cannot join. The panel keeps running.",
error: "Not running",
};
function renderControl(el, s) {
const state = $("gameState");
if (state) state.textContent = s.gameStopped ? "game server is stopped - players cannot join" : "game server is running";
el.replaceChildren(
s.gameStopped
? h("button", { class: "btn primary", type: "button", onclick: () => serverAction("start-game") }, "▶ Start")
: h("button", { class: "btn", type: "button", onclick: () => serverAction("stop-game", { title: "Stop the game server?",
text: "All players are kicked and nobody can join until you start it again. The panel keeps running.", danger: true, confirm: "Stop" }) }, "■ Stop"),
h("button", { class: "btn", type: "button", onclick: () => serverAction("restart", { title: "Restart the server?",
text: "Settings (server.json), license and save game are loaded again. Players reconnect automatically. You stay logged in.", confirm: "Restart" }) }, "⟳ Restart"),
can("Owner") ? h("button", { class: "btn danger", type: "button", onclick: () => serverAction("shutdown", { title: "Shut down completely?",
text: "The game server AND this panel stop. Start it again on the machine (e.g. docker compose up -d).", danger: true, confirm: "Shut down" }) }, "⏻ Shut down") : null);
const label = $("gameState");
const running = s.state === "running", starting = s.state === "starting";
if (label) {
label.textContent = STATE_TEXT[s.state] || s.state;
label.className = "state-label " + s.state;
}
const buttons = h("div", { class: "row" },
running || starting
? h("button", { class: "btn", type: "button", disabled: starting, onclick: () => serverAction("stop", { title: "Stop the server?",
text: "All players are disconnected and nobody can join until you start it again. The panel keeps running.", danger: true, confirm: "Stop" }) }, "■ Stop")
: h("button", { class: "btn primary", type: "button", onclick: () => serverAction("start") }, "▶ Start"),
h("button", { class: "btn", type: "button", disabled: starting, onclick: () => serverAction("restart", { title: "Restart the server?",
text: "Settings (server.json), license and save game are loaded again. Players reconnect automatically.", confirm: "Restart" }) }, "⟳ Restart"));
// Warum läuft er nicht? Mit direktem Weg zur Lösung
let problem = null;
if (!running && s.lastError) {
const fix = s.hasLicenseKey === false ? h("button", { class: "btn small", type: "button", onclick: () => go("settings") }, "Enter license key")
: s.hasSave === false ? h("button", { class: "btn small", type: "button", onclick: () => go("world") }, "Upload save game")
: h("button", { class: "btn small ghost", type: "button", onclick: () => go("console") }, "Open console");
problem = h("div", { class: "notice " + (s.state === "error" ? "bad" : "") }, h("div", {}, h("b", {}, s.state === "error" ? "The server could not start" : "Note"), h("div", {}, s.lastError)), fix);
}
el.replaceChildren(buttons, problem);
}
// ------------------------------------------------------------------ Einstellungen
@@ -355,7 +376,7 @@ function renderSettings(page) {
secret("licenseKey", "License key", c.licenseKeySet),
secret("steamWebApiKey", "Steam Web API key", c.steamWebApiKeySet))),
h("div", { class: "row" }, h("button", { class: "btn primary", type: "button", onclick: save }, "Save settings"),
h("button", { class: "btn", type: "button", onclick: () => serverAction("restart", { title: "Restart the server now?",
h("button", { class: "btn", type: "button", onclick: () => serverAction("restart", { title: "Restart / start the server now?",
text: "Loads the saved settings. Players reconnect automatically.", confirm: "Restart" }) }, "Restart to apply")));
}
}