const TOKEN_KEY = "gameservers-control-token"; const state = { token: localStorage.getItem(TOKEN_KEY) || "", session: null, servers: [], groups: [], allServers: [], }; const els = { status: document.getElementById("status"), sessionLabel: document.getElementById("sessionLabel"), loginView: document.getElementById("loginView"), appView: document.getElementById("appView"), loginForm: document.getElementById("loginForm"), passwordInput: document.getElementById("passwordInput"), logoutBtn: document.getElementById("logoutBtn"), refreshBtn: document.getElementById("refreshBtn"), adminTab: document.getElementById("adminTab"), logsTab: document.getElementById("logsTab"), serversBody: document.getElementById("serversBody"), logsBody: document.getElementById("logsBody"), groupsList: document.getElementById("groupsList"), createGroupForm: document.getElementById("createGroupForm"), newGroupName: document.getElementById("newGroupName"), newGroupPassword: document.getElementById("newGroupPassword"), generatedPassword: document.getElementById("generatedPassword"), cleanupServersBtn: document.getElementById("cleanupServersBtn"), }; function setStatus(message, type = "info") { els.status.textContent = message; els.status.className = `status status--${type}`; els.status.hidden = false; } function clearStatus() { els.status.hidden = true; els.status.textContent = ""; } async function apiFetch(path, options = {}) { const headers = new Headers(options.headers || {}); if (state.token) headers.set("Authorization", `Bearer ${state.token}`); const isFormData = options.body instanceof FormData; if (options.body && !isFormData && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } const response = await fetch(path, { method: options.method || "GET", headers, body: options.body && !isFormData && typeof options.body !== "string" ? JSON.stringify(options.body) : options.body, }); const contentType = response.headers.get("content-type") || ""; const payload = contentType.includes("application/json") ? await response.json() : await response.text(); if (!response.ok) { throw new Error(payload?.error || response.statusText); } return payload; } function setAuthenticated({ token, session }) { if (token) { state.token = token; localStorage.setItem(TOKEN_KEY, token); } state.session = session; els.loginView.hidden = true; els.appView.hidden = false; els.logoutBtn.hidden = false; els.refreshBtn.hidden = false; els.adminTab.hidden = !session.admin; els.logsTab.hidden = !session.admin; els.sessionLabel.textContent = session.admin ? "Administrateur" : "Connecte"; } function setLoggedOut() { state.token = ""; state.session = null; localStorage.removeItem(TOKEN_KEY); els.loginView.hidden = false; els.appView.hidden = true; els.logoutBtn.hidden = true; els.refreshBtn.hidden = true; els.sessionLabel.textContent = "Non connecte"; switchTab("servers"); } async function loadSession() { if (!state.token) return setLoggedOut(); try { const data = await apiFetch("/api/session"); setAuthenticated({ session: data.session }); await refreshAll(); } catch { setLoggedOut(); } } async function refreshAll() { await loadServers(); if (state.session?.admin) { await Promise.all([loadAdmin(), loadLogs()]); } } async function loadServers() { const data = await apiFetch("/api/servers"); state.servers = data.servers || []; renderServers(); } function renderServers() { els.serversBody.innerHTML = ""; if (!state.servers.length) { els.serversBody.innerHTML = `
Aucun serveur visible.
`; return; } for (const server of state.servers) { const card = document.createElement("article"); card.className = "server-card"; const heroStyle = server.heroImage ? ` style="--server-hero-image: url('${escapeCssUrl(server.heroImage)}')"` : ""; const secrets = server.secrets && Object.keys(server.secrets).length ? Object.entries(server.secrets) .map( ([key, value]) => `
${escapeHtml(formatSecretLabel(key))} ${escapeHtml(value)}
` ) .join("") : server.hasSecrets ? `Masques` : `
Mot de passe aucun
`; card.innerHTML = `
${server.active ? "En ligne" : "Hors ligne"}

Serveur jeu

${escapeHtml(server.game)}

Adresse IP ${escapeHtml(server.address || "-")}
Port ${server.port ?? "-"}
${secrets}
${renderAdminEditButton(server)} ${renderAdminEditPanel(server)}
`; els.serversBody.appendChild(card); } } function renderAdminEditButton(server) { if (!state.session?.admin) return ""; return ` `; } function renderAdminEditPanel(server) { if (!state.session?.admin) return ""; return ` `; } function renderPasswordControls(server) { if (!state.session?.admin) return ""; const password = getServerPasswordValue(server); return `
`; } function renderImageControls(server) { if (!state.session?.admin) return ""; return `
`; } function getServerPasswordValue(server) { if (!server.secrets) return ""; return server.secrets.serverPassword || server.secrets.password || ""; } async function startServer(serverId, button) { button.disabled = true; const previous = button.textContent; button.textContent = "Lancement..."; try { await apiFetch(`/api/servers/${encodeURIComponent(serverId)}/start`, { method: "POST" }); setStatus("Lancement demande", "success"); await refreshAll(); } catch (error) { setStatus(error.message, "error"); button.disabled = false; } finally { button.textContent = previous; } } async function uploadServerImage(serverId) { const input = document.querySelector(`[data-image-input="${cssEscape(serverId)}"]`); const file = input?.files?.[0]; if (!file) { setStatus("Choisis une image avant l'envoi", "error"); return; } const formData = new FormData(); formData.set("image", file); await apiFetch(`/api/admin/servers/${encodeURIComponent(serverId)}/image`, { method: "POST", body: formData, }); setStatus("Image serveur mise a jour", "success"); await refreshAll(); } async function deleteServerImage(serverId) { await apiFetch(`/api/admin/servers/${encodeURIComponent(serverId)}/image`, { method: "DELETE", }); setStatus("Image serveur retiree", "success"); await refreshAll(); } function toggleServerEditPanel(serverId) { const panel = document.querySelector(`[data-edit-panel="${cssEscape(serverId)}"]`); if (!panel) return; panel.hidden = !panel.hidden; } async function saveServerPassword(serverId) { const input = document.querySelector(`[data-password-input="${cssEscape(serverId)}"]`); await apiFetch(`/api/admin/servers/${encodeURIComponent(serverId)}/password`, { method: "PATCH", body: { password: input?.value || "" }, }); setStatus("Mot de passe serveur mis a jour", "success"); await refreshAll(); } async function loadAdmin() { const data = await apiFetch("/api/admin/groups"); state.groups = data.groups || []; state.allServers = data.servers || []; renderGroups(); } function renderGroups() { els.groupsList.innerHTML = ""; for (const group of state.groups) { const article = document.createElement("article"); article.className = "group-card"; const permissions = group.permissions || { servers: [] }; article.innerHTML = `
${group.admin ? "Admin" : "Groupe"}
${state.allServers .map( (server) => ` ` ) .join("")}
`; els.groupsList.appendChild(article); } } async function saveGroup(groupId) { const name = document.querySelector(`[data-name="${cssEscape(groupId)}"]`)?.value.trim(); const servers = Array.from(document.querySelectorAll(`[data-server-perm="${cssEscape(groupId)}"]:checked`)).map( (input) => input.value ); await apiFetch(`/api/admin/groups/${encodeURIComponent(groupId)}`, { method: "PATCH", body: { name, permissions: { servers }, }, }); setStatus("Groupe sauvegarde", "success"); await loadAdmin(); } async function regeneratePassword(groupId) { const data = await apiFetch(`/api/admin/groups/${encodeURIComponent(groupId)}/regenerate-password`, { method: "POST", }); showGeneratedPassword(data.group.name, data.password); await loadAdmin(); } async function setGroupPassword(groupId) { const input = document.querySelector(`[data-group-password="${cssEscape(groupId)}"]`); const password = input?.value.trim() || ""; if (!password) { setStatus("Mot de passe requis", "error"); return; } const data = await apiFetch(`/api/admin/groups/${encodeURIComponent(groupId)}/password`, { method: "PATCH", body: { password }, }); input.value = ""; showGeneratedPassword(data.group.name, data.password); await loadAdmin(); } async function deleteGroup(groupId) { await apiFetch(`/api/admin/groups/${encodeURIComponent(groupId)}`, { method: "DELETE" }); setStatus("Groupe supprime", "success"); await loadAdmin(); } async function createGroup(event) { event.preventDefault(); const data = await apiFetch("/api/admin/groups", { method: "POST", body: { name: els.newGroupName.value.trim(), password: els.newGroupPassword.value.trim(), permissions: { servers: [] }, }, }); els.createGroupForm.reset(); showGeneratedPassword(data.group.name, data.password); await loadAdmin(); } async function cleanupServers() { const data = await apiFetch("/api/admin/servers/cleanup", { method: "POST" }); const removedCount = data.removedServers?.length || 0; const imageCount = data.removedImages?.length || 0; setStatus( `Nettoyage termine: ${removedCount} configuration(s), ${imageCount} image(s) retiree(s)`, "success" ); await refreshAll(); } function showGeneratedPassword(groupName, password) { els.generatedPassword.hidden = false; els.generatedPassword.innerHTML = `Mot de passe pour ${escapeHtml(groupName)}: ${escapeHtml(password)}`; } async function loadLogs() { const data = await apiFetch("/api/logs"); renderLogs(data.logs || []); } function renderLogs(logs) { els.logsBody.innerHTML = ""; if (!logs.length) { els.logsBody.innerHTML = `Aucun lancement sur 7 jours.`; return; } for (const log of logs) { const tr = document.createElement("tr"); tr.innerHTML = ` ${escapeHtml(new Date(log.createdAt).toLocaleString())} ${escapeHtml(log.groupName || log.groupId || "-")} ${escapeHtml(log.serverName || log.serverId || "-")} `; els.logsBody.appendChild(tr); } } function switchTab(tabName) { document.querySelectorAll(".tab").forEach((tab) => { tab.classList.toggle("tab--active", tab.dataset.tab === tabName); }); document.querySelectorAll("[data-panel]").forEach((panel) => { panel.hidden = panel.dataset.panel !== tabName; }); } function escapeHtml(value = "") { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function escapeCssUrl(value = "") { return String(value).replace(/['"\\\n\r]/g, ""); } function formatSecretLabel(key = "") { if (key === "password" || key === "serverPassword") { return "Mot de passe"; } return key; } function cssEscape(value) { return CSS.escape(value); } els.loginForm.addEventListener("submit", async (event) => { event.preventDefault(); clearStatus(); try { const data = await apiFetch("/api/login", { method: "POST", body: { password: els.passwordInput.value }, }); els.passwordInput.value = ""; setAuthenticated(data); await refreshAll(); } catch (error) { setStatus(error.message, "error"); } }); els.logoutBtn.addEventListener("click", async () => { try { await apiFetch("/api/logout", { method: "POST" }); } catch { // Local logout remains valid if the server session already expired. } setLoggedOut(); }); els.refreshBtn.addEventListener("click", () => refreshAll().catch((error) => setStatus(error.message, "error"))); els.createGroupForm.addEventListener("submit", (event) => createGroup(event).catch((error) => setStatus(error.message, "error")) ); els.cleanupServersBtn?.addEventListener("click", () => cleanupServers().catch((error) => setStatus(error.message, "error")) ); document.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; const tab = target.closest("[data-tab]"); if (tab) switchTab(tab.dataset.tab); const startId = target.dataset.start; if (startId) startServer(startId, target).catch((error) => setStatus(error.message, "error")); const saveId = target.dataset.saveGroup; if (saveId) saveGroup(saveId).catch((error) => setStatus(error.message, "error")); const regenId = target.dataset.regen; if (regenId) regeneratePassword(regenId).catch((error) => setStatus(error.message, "error")); const setPasswordId = target.dataset.setGroupPassword; if (setPasswordId) setGroupPassword(setPasswordId).catch((error) => setStatus(error.message, "error")); const deleteId = target.dataset.delete; if (deleteId) deleteGroup(deleteId).catch((error) => setStatus(error.message, "error")); const uploadImageId = target.dataset.uploadImage; if (uploadImageId) uploadServerImage(uploadImageId).catch((error) => setStatus(error.message, "error")); const deleteImageId = target.dataset.deleteImage; if (deleteImageId) deleteServerImage(deleteImageId).catch((error) => setStatus(error.message, "error")); const editServerId = target.dataset.editServer; if (editServerId) toggleServerEditPanel(editServerId); const savePasswordId = target.dataset.savePassword; if (savePasswordId) saveServerPassword(savePasswordId).catch((error) => setStatus(error.message, "error")); }); loadSession();