diff --git a/app/sovran_systemsos_web/static/app.js b/app/sovran_systemsos_web/static/app.js index 3120df6..01c38b5 100644 --- a/app/sovran_systemsos_web/static/app.js +++ b/app/sovran_systemsos_web/static/app.js @@ -72,6 +72,15 @@ function statusText(status, enabled) { return status; } +function escHtml(str) { + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + // ── Fetch wrappers ──────────────────────────────────────────────── async function apiFetch(path, options = {}) { @@ -107,4 +116,428 @@ function buildTiles(services, categoryLabels) { const section = document.createElement("div"); section.className = "category-section"; - section.dataset.category \ No newline at end of file + section.dataset.category = catKey; + + section.innerHTML = ` +
No services configured.
Loading…
'; + + $credsModal.classList.add("open"); + + try { + const data = await apiFetch(`/api/credentials/${encodeURIComponent(unit)}`); + + if (!data.credentials || data.credentials.length === 0) { + $credsBody.innerHTML = 'No connection info available yet.
'; + return; + } + + let html = ""; + for (const cred of data.credentials) { + const id = "cred-" + Math.random().toString(36).substring(2, 8); + html += ` +Could not load credentials.
'; + } +} + +function closeCredsModal() { + if ($credsModal) $credsModal.classList.remove("open"); +} + +// ── Update modal ────────────────────────────────────────────────── + +function openUpdateModal() { + if (!$modal) return; + _updateLog = ""; + _updateLogOffset = 0; + _serverWasDown = false; + _updateFinished = false; + if ($modalLog) $modalLog.textContent = ""; + if ($modalStatus) $modalStatus.textContent = "Starting update…"; + if ($modalSpinner) $modalSpinner.classList.add("spinning"); + if ($btnReboot) { $btnReboot.style.display = "none"; } + if ($btnSave) { $btnSave.style.display = "none"; } + if ($btnCloseModal) { $btnCloseModal.disabled = true; } + + $modal.classList.add("open"); + startUpdate(); +} + +function closeUpdateModal() { + if (!$modal) return; + $modal.classList.remove("open"); + stopUpdatePoll(); +} + +function appendLog(text) { + if (!text) return; + _updateLog += text; + if ($modalLog) { + $modalLog.textContent += text; + $modalLog.scrollTop = $modalLog.scrollHeight; + } +} + +function startUpdate() { + fetch("/api/updates/run", { method: "POST" }) + .then(response => { + if (!response.ok) { + return response.text().then(t => { throw new Error(t); }); + } + return response.json(); + }) + .then(data => { + if (data.status === "already_running") { + appendLog("[Update already in progress, attaching…]\n\n"); + } + if ($modalStatus) $modalStatus.textContent = "Updating…"; + startUpdatePoll(); + }) + .catch(err => { + appendLog(`[Error: failed to start update — ${err}]\n`); + onUpdateDone(false); + }); +} + +function startUpdatePoll() { + pollUpdateStatus(); + _updatePollTimer = setInterval(pollUpdateStatus, UPDATE_POLL_INTERVAL); +} + +function stopUpdatePoll() { + if (_updatePollTimer) { + clearInterval(_updatePollTimer); + _updatePollTimer = null; + } +} + +async function pollUpdateStatus() { + if (_updateFinished) return; + + try { + const data = await apiFetch(`/api/updates/status?offset=${_updateLogOffset}`); + + if (_serverWasDown) { + _serverWasDown = false; + appendLog("[Server reconnected]\n"); + if ($modalStatus) $modalStatus.textContent = "Updating…"; + } + + if (data.log) { + appendLog(data.log); + } + _updateLogOffset = data.offset; + + if (data.running) { + return; + } + + _updateFinished = true; + stopUpdatePoll(); + + if (data.result === "success") { + onUpdateDone(true); + } else { + onUpdateDone(false); + } + } catch (err) { + if (!_serverWasDown) { + _serverWasDown = true; + appendLog("\n[Server restarting — waiting for it to come back…]\n"); + if ($modalStatus) $modalStatus.textContent = "Server restarting…"; + } + } +} + +function onUpdateDone(success) { + if ($modalSpinner) $modalSpinner.classList.remove("spinning"); + if ($btnCloseModal) $btnCloseModal.disabled = false; + + if (success) { + if ($modalStatus) $modalStatus.textContent = "✓ Update complete"; + if ($btnReboot) $btnReboot.style.display = "inline-flex"; + } else { + if ($modalStatus) $modalStatus.textContent = "✗ Update failed"; + if ($btnSave) $btnSave.style.display = "inline-flex"; + if ($btnReboot) $btnReboot.style.display = "inline-flex"; + } +} + +function saveErrorReport() { + const blob = new Blob([_updateLog], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `sovran-update-error-${new Date().toISOString().split('.')[0].replace(/:/g, '-')}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +// ── Reboot with confirmation overlay ────────────────────────────── + +function doReboot() { + if ($modal) $modal.classList.remove("open"); + stopUpdatePoll(); + if ($rebootOverlay) $rebootOverlay.classList.add("visible"); + fetch("/api/reboot", { method: "POST" }).catch(() => {}); + setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL); +} + +function waitForServerReboot() { + fetch("/api/config", { cache: "no-store" }) + .then(res => { + if (res.ok) { + window.location.reload(); + } else { + setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL); + } + }) + .catch(() => { + setTimeout(waitForServerReboot, REBOOT_CHECK_INTERVAL); + }); +} + +// ── Event listeners ─────────────────────────────────────────────── + +if ($updateBtn) $updateBtn.addEventListener("click", openUpdateModal); +if ($refreshBtn) $refreshBtn.addEventListener("click", () => refreshServices()); +if ($btnCloseModal) $btnCloseModal.addEventListener("click", closeUpdateModal); +if ($btnReboot) $btnReboot.addEventListener("click", doReboot); +if ($btnSave) $btnSave.addEventListener("click", saveErrorReport); +if ($credsCloseBtn) $credsCloseBtn.addEventListener("click", closeCredsModal); + +if ($modal) { + $modal.addEventListener("click", (e) => { + if (e.target === $modal) closeUpdateModal(); + }); +} + +if ($credsModal) { + $credsModal.addEventListener("click", (e) => { + if (e.target === $credsModal) closeCredsModal(); + }); +} + +// ── Init ────────────────────────────────────────────────────────── + +async function init() { + try { + const cfg = await apiFetch("/api/config"); + if (cfg.category_order) { + for (const [key, label] of cfg.category_order) { + _categoryLabels[key] = label; + } + } + const badge = document.getElementById("role-badge"); + if (badge && cfg.role_label) badge.textContent = cfg.role_label; + } catch (_) {} + + await refreshServices(); + loadNetwork(); + checkUpdates(); + + setInterval(refreshServices, POLL_INTERVAL_SERVICES); + setInterval(checkUpdates, POLL_INTERVAL_UPDATES); +} + +document.addEventListener("DOMContentLoaded", init); \ No newline at end of file