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:
@@ -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
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user