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
+9
View File
@@ -152,6 +152,15 @@ tbody tr:hover { background: rgba(255, 255, 255, .02); }
.toggle input { width: 18px; height: 18px; margin: 1px 0 0; accent-color: var(--accent); flex: none; }
input[type=file] { padding: 7px; }
input[type=file]::file-selector-button { background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 5px 10px; margin-right: 10px; cursor: pointer; }
.control { display: grid; gap: 14px; }
.state-label { font-size: 13px; font-weight: 600; color: var(--muted); display: inline-flex; align-items: center; gap: 7px; }
.state-label::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--pending); }
.state-label.running::before { background: var(--online); }
.state-label.error::before { background: var(--danger); }
.notice { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; padding: 12px 14px;
border-radius: 10px; background: rgba(242, 194, 48, .08); border: 1px solid rgba(242, 194, 48, .25); }
.notice.bad { background: rgba(229, 72, 77, .08); border-color: rgba(229, 72, 77, .3); }
.notice b { display: block; margin-bottom: 2px; }
.console-input { display: flex; gap: 10px; padding: 12px; border-top: 1px solid var(--border); background: var(--panel); }
.console-input input { font-family: var(--mono); }
+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")));
}
}