License grace period: keep running when the master is down or rejects the key (LicenseGraceHours, default 72 h), start with the last confirmed license, auto-retry; panel restart button (port/panel settings without program restart, login kept); tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 20:02:05 +02:00
co-authored by Claude Opus 5.5
parent d4e5897b7f
commit 14db3ef4f4
10 changed files with 325 additions and 24 deletions
+35 -7
View File
@@ -306,7 +306,32 @@ function renderControl(el, s) {
: 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 ?? ""); // replaceChildren macht aus null den Text "null"
// Lizenz gerade nicht bestätigt (Master aus o.ä.) - der Server läuft weiter, aber nur noch eine Weile
const license = running && s.licenseWarning
? h("div", { class: "notice" }, h("div", {}, h("b", {}, "License not confirmed"), h("div", {}, s.licenseWarning))) : null;
el.replaceChildren(...[buttons, problem, license].filter(Boolean)); // null würde sonst als Text "null" erscheinen
}
/** Panel neu starten (Port, Unterpfad, Proxy übernehmen) und danach die (ggf. neue) Adresse laden. */
async function restartPanel() {
const ok = await modal({ title: "Restart the panel?",
text: "Applies the saved port and panel settings. If the game port changed, the game server restarts too. You stay logged in.",
confirm: "Restart panel" });
if (!ok) return;
const r = await act(api("POST", "server/restart-panel"));
if (!r) return;
const target = r.url && r.url !== location.origin + location.pathname ? r.url : location.href;
// Warten, bis das Panel wieder antwortet, dann neu laden
const started = Date.now();
const tryReload = async () => {
try {
const res = await fetch(new URL("api/session", target).href, { credentials: "include", mode: target.startsWith(location.origin) ? "same-origin" : "no-cors" });
if (res.ok || res.type === "opaque") { location.href = target; return; }
} catch { /* noch nicht da */ }
if (Date.now() - started < 30000) setTimeout(tryReload, 1000);
else location.href = target;
};
setTimeout(tryReload, 2500);
}
// ------------------------------------------------------------------ Einstellungen
@@ -376,7 +401,9 @@ function renderSettings(page) {
h("div", { class: "section-head" }, h("h2", {}, "Saving")),
h("div", { class: "section-body form-grid wide" },
number("saveSyncMinutes", "Take the save game from a player every … minutes", c.saveSyncMinutes, { min: 0, max: 240, hint: "0 = only when someone joins" }),
number("autosaveSeconds", "Write the world state every … seconds", c.autosaveSeconds, { min: 10, max: 3600 }))),
number("autosaveSeconds", "Write the world state every … seconds", c.autosaveSeconds, { min: 10, max: 3600 }),
number("licenseGraceHours", "Keep running without license confirmation for … hours", c.licenseGraceHours, { min: 1, max: 720,
hint: "If the master server is down or rejects the key, the server keeps running this long" }))),
h("div", { class: "section" },
h("div", { class: "section-head" }, h("h2", {}, "Network & panel"), h("span", { class: "muted" }, "needs a restart")),
h("div", { class: "section-body form-grid wide" },
@@ -396,13 +423,14 @@ function renderSettings(page) {
h("a", { href: "https://steamcommunity.com/dev/apikey", target: "_blank", rel: "noopener noreferrer" }, "Get your free key on Steam ↗"))))),
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 / start the server now?",
text: "Loads the saved settings. Players reconnect automatically.", confirm: "Restart" }) }, "Restart to apply")));
text: "Loads the saved settings. Players reconnect automatically.", confirm: "Restart" }) }, "Restart to apply"),
h("button", { class: "btn", type: "button", onclick: restartPanel }, "⟳ Restart panel")));
}
}
/**
* Warnung, wenn Spiel- oder Panel-Port geändert werden: Ports übernimmt erst ein Neustart des ganzen Programms
* (der Restart-Knopf startet nur den Gameserver neu), danach ist das Panel nur noch am neuen Port - in Docker nur,
* Warnung, wenn Spiel- oder Panel-Port geändert werden: Ports übernimmt "Panel neu starten" (oder ein Programmneustart),
* danach ist das Panel nur noch am neuen Port - in Docker nur,
* wenn der Port in docker-compose.yml freigegeben ist. Text direkt in beiden Sprachen (enthält Zahlen).
*/
function portChangeWarning(c, body) {
@@ -419,8 +447,8 @@ function portChangeWarning(c, body) {
if (port !== c.port)
lines.push(de ? `Spieler verbinden sich dann über Port ${port}.` : `Players then connect on port ${port}.`);
lines.push(de
? "Das gilt erst nach einem Neustart des ganzen Programms (docker compose restart) - der Neustart-Knopf startet nur den Gameserver neu."
: "This only applies after the whole program restarts (docker compose restart) - the Restart button only restarts the game server.");
? "Das gilt, sobald du unten „Panel neu starten“ drückst (oder das Programm neu startest)."
: "It applies as soon as you press “Restart panel” at the bottom (or restart the program).");
const ports = [...new Set([port, newPanel])].map((p) => `"${p}:${p}/tcp"`).join(", ");
lines.push(c.inDocker
? (de ? `Docker: trag vorher unter ports in docker-compose.yml ${ports} ein, sonst sperrst du dich aus.`
+14
View File
@@ -281,6 +281,16 @@
"The save game is too large": "Der Spielstand ist zu groß",
"Enter a command": "Gib einen Befehl ein",
"Choose a ZIP, RAR or 7z file": "Wähle eine ZIP-, RAR- oder 7z-Datei",
"License not confirmed": "Lizenz nicht bestätigt",
"Keep running without license confirmation for … hours": "Ohne Lizenzbestätigung weiterlaufen für … Stunden",
"If the master server is down or rejects the key, the server keeps running this long": "Ist der Master-Server aus oder lehnt den Schlüssel ab, läuft der Server so lange weiter",
"⟳ Restart panel": "⟳ Panel neu starten",
"Restart the panel?": "Panel neu starten?",
"Applies the saved port and panel settings. If the game port changed, the game server restarts too. You stay logged in.":
"Übernimmt die gespeicherten Port- und Panel-Einstellungen. Hat sich der Spiel-Port geändert, startet auch der Gameserver neu. Du bleibst angemeldet.",
"Restart panel": "Panel neu starten",
"The panel restarts - reloading in a few seconds": "Das Panel startet neu - die Seite lädt in ein paar Sekunden neu",
"The panel is turned off in the settings - turn it on again or edit server.json by hand": "Das Panel ist in den Einstellungen ausgeschaltet - schalte es wieder ein oder bearbeite server.json von Hand",
};
// Zusammengesetzte Texte (Name, Zahl ... darin)
@@ -330,6 +340,10 @@
[/^License key invalid: (.+)$/, "Lizenzschlüssel ungültig: $1"],
[/^Master server not reachable (.+)$/, "Master-Server nicht erreichbar $1"],
[/^Crashed: (.+)$/, "Abgestürzt: $1"],
[/^The master server (rejects the license key|is not reachable) \(last confirmed (.+) ago\) - the server keeps running for (.+)$/,
(m, why, ago, left) => (why.startsWith("rejects") ? "Der Master-Server lehnt den Lizenzschlüssel ab" : "Der Master-Server ist nicht erreichbar") +
" (zuletzt bestätigt vor " + ago + ") - der Server läuft noch " + left + " weiter"],
[/^The license key could not be confirmed by the master server for (.+)$/, "Der Lizenzschlüssel konnte $1 lang nicht vom Master-Server bestätigt werden"],
[/^This file could not be read - upload a ZIP, RAR or 7z archive (.+)$/, "Die Datei konnte nicht gelesen werden - lade ein ZIP-, RAR- oder 7z-Archiv hoch $1"],
[/^No defaultES2File\.txt in the archive(.*)$/, "Keine defaultES2File.txt im Archiv - pack die Dateien aus %USERPROFILE%\\AppData\\LocalLow\\Amistech\\My Summer Car"],
];