/* Sovran_SystemsOS Hub — Vanilla JS Frontend v7 — Status-only dashboard + Tech Support */ "use strict"; const POLL_INTERVAL_SERVICES = 5000; // 5 s const POLL_INTERVAL_UPDATES = 1800000; // 30 min const UPDATE_POLL_INTERVAL = 2000; // 2 s while update is running const REBOOT_CHECK_INTERVAL = 5000; // 5 s between reconnect attempts const SUPPORT_TIMER_INTERVAL = 1000; // 1 s for session timer const CATEGORY_ORDER = [ "infrastructure", "bitcoin-base", "bitcoin-apps", "communication", "apps", "nostr", "support", ]; const STATUS_LOADING_STATES = new Set([ "reloading", "activating", "deactivating", "maintenance", ]); // ── State ───────────────────────────────────────────────────────── let _servicesCache = []; let _categoryLabels = {}; let _updateLog = ""; let _updatePollTimer = null; let _updateLogOffset = 0; let _serverWasDown = false; let _updateFinished = false; let _supportTimerInt = null; let _supportEnabledAt = null; let _cachedExternalIp = null; // ── DOM refs ────────────────────────────────────────────────────── const $tilesArea = document.getElementById("tiles-area"); const $updateBtn = document.getElementById("btn-update"); const $updateBadge = document.getElementById("update-badge"); const $refreshBtn = document.getElementById("btn-refresh"); const $internalIp = document.getElementById("ip-internal"); const $externalIp = document.getElementById("ip-external"); const $modal = document.getElementById("update-modal"); const $modalSpinner = document.getElementById("modal-spinner"); const $modalStatus = document.getElementById("modal-status"); const $modalLog = document.getElementById("modal-log"); const $btnReboot = document.getElementById("btn-reboot"); const $btnSave = document.getElementById("btn-save-report"); const $btnCloseModal = document.getElementById("btn-close-modal"); const $rebootOverlay = document.getElementById("reboot-overlay"); const $credsModal = document.getElementById("creds-modal"); const $credsTitle = document.getElementById("creds-modal-title"); const $credsBody = document.getElementById("creds-body"); const $credsCloseBtn = document.getElementById("creds-close-btn"); const $supportModal = document.getElementById("support-modal"); const $supportBody = document.getElementById("support-body"); const $supportCloseBtn = document.getElementById("support-close-btn"); // ── Helpers ─────────────────────────────────────────────────────── function tileId(svc) { return svc.unit + "::" + svc.name; } function statusClass(status) { if (!status) return "unknown"; if (status === "active") return "active"; if (status === "inactive") return "inactive"; if (status === "failed") return "failed"; if (status === "disabled") return "disabled"; if (STATUS_LOADING_STATES.has(status)) return "loading"; return "unknown"; } function statusText(status, enabled) { if (!enabled) return "disabled"; if (!status || status === "unknown") return "unknown"; return status; } function escHtml(str) { return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function linkify(str) { const escaped = escHtml(str); return escaped.replace( /(https?:\/\/[^\s<]+)/g, '$1' ); } function formatDuration(seconds) { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); if (h > 0) return `${h}h ${m}m ${s}s`; if (m > 0) return `${m}m ${s}s`; return `${s}s`; } // ── Fetch wrappers ──────────────────────────────────────────────── async function apiFetch(path, options = {}) { const res = await fetch(path, options); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); return res.json(); } // ── Render: initial build ───────────────────────────────────────── function buildTiles(services, categoryLabels) { _servicesCache = services; const grouped = {}; for (const svc of services) { const cat = svc.category || "other"; if (!grouped[cat]) grouped[cat] = []; grouped[cat].push(svc); } $tilesArea.innerHTML = ""; const orderedKeys = [ ...CATEGORY_ORDER.filter(k => grouped[k]), ...Object.keys(grouped).filter(k => !CATEGORY_ORDER.includes(k)), ]; for (const catKey of orderedKeys) { const entries = grouped[catKey]; if (!entries || entries.length === 0) continue; const label = categoryLabels[catKey] || catKey; const section = document.createElement("div"); section.className = "category-section"; section.dataset.category = catKey; section.innerHTML = `
${escHtml(label)}

`; const grid = section.querySelector(".tiles-grid"); for (const svc of entries) { grid.appendChild(buildTile(svc)); } $tilesArea.appendChild(section); } if ($tilesArea.children.length === 0) { $tilesArea.innerHTML = `

No services configured.

`; } } function buildTile(svc) { const isSupport = svc.type === "support"; const sc = statusClass(svc.status); const st = statusText(svc.status, svc.enabled); const dis = !svc.enabled; const hasCreds = svc.has_credentials && svc.enabled; const tile = document.createElement("div"); tile.className = "service-tile" + (dis ? " disabled" : "") + (isSupport ? " support-tile" : ""); tile.dataset.unit = svc.unit; tile.dataset.tileId = tileId(svc); if (dis) tile.title = `${svc.name} is not enabled in custom.nix`; if (isSupport) { // Support tile — clickable, no info button, no status dot tile.innerHTML = ` ${escHtml(svc.name)}
${escHtml(svc.name)}
Click to manage
`; tile.style.cursor = "pointer"; tile.addEventListener("click", () => openSupportModal()); return tile; } // Normal tile const infoBtn = hasCreds ? `` : ""; tile.innerHTML = ` ${infoBtn} ${escHtml(svc.name)}
${escHtml(svc.name)}
${escHtml(st)}
`; const infoBtnEl = tile.querySelector(".tile-info-btn"); if (infoBtnEl) { infoBtnEl.addEventListener("click", (e) => { e.stopPropagation(); openCredsModal(svc.unit, svc.name); }); } return tile; } // ── Render: live update (no DOM rebuild) ────────────────────────── function updateTiles(services) { _servicesCache = services; for (const svc of services) { const id = CSS.escape(tileId(svc)); const tile = $tilesArea.querySelector(`.service-tile[data-tile-id="${id}"]`); if (!tile) continue; if (svc.type === "support") continue; // Support tile doesn't have a systemd status const sc = statusClass(svc.status); const st = statusText(svc.status, svc.enabled); const dot = tile.querySelector(".status-dot"); const text = tile.querySelector(".status-text"); if (dot) { dot.className = `status-dot ${sc}`; } if (text) { text.textContent = st; } } } // ── Service polling ─────────────────────────────────────────────── let _firstLoad = true; async function refreshServices() { try { const services = await apiFetch("/api/services"); if (_firstLoad) { buildTiles(services, _categoryLabels); _firstLoad = false; } else { updateTiles(services); } } catch (err) { console.warn("Failed to fetch services:", err); } } // ── Network IPs ─────────────────────────────────────────────────── async function loadNetwork() { try { const data = await apiFetch("/api/network"); if ($internalIp) $internalIp.textContent = data.internal_ip || "—"; if ($externalIp) $externalIp.textContent = data.external_ip || "—"; _cachedExternalIp = data.external_ip || "unavailable"; } catch (_) { if ($internalIp) $internalIp.textContent = "—"; if ($externalIp) $externalIp.textContent = "—"; } } // ── Update check ────────────────────────────────────────────────── async function checkUpdates() { try { const data = await apiFetch("/api/updates/check"); const hasUpdates = !!data.available; if ($updateBadge) { $updateBadge.classList.toggle("visible", hasUpdates); } if ($updateBtn) { $updateBtn.classList.toggle("has-updates", hasUpdates); } } catch (_) {} } // ── Credentials info modal ──────────────────────────────────────── async function openCredsModal(unit, name) { if (!$credsModal) return; if ($credsTitle) $credsTitle.textContent = name + " — Connection Info"; if ($credsBody) $credsBody.innerHTML = '

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); const displayValue = linkify(cred.value); let qrBlock = ""; if (cred.qrcode) { qrBlock = `
QR Code for ${escHtml(cred.label)}
Scan with Zeus app on your phone
`; } html += `
${escHtml(cred.label)}
${qrBlock}
${displayValue}
`; } $credsBody.innerHTML = html; $credsBody.querySelectorAll(".creds-copy-btn").forEach(btn => { btn.addEventListener("click", () => { const target = document.getElementById(btn.dataset.target); if (target) { navigator.clipboard.writeText(target.textContent).then(() => { btn.textContent = "Copied!"; btn.classList.add("copied"); setTimeout(() => { btn.textContent = "Copy"; btn.classList.remove("copied"); }, 1500); }).catch(() => {}); } }); }); } catch (err) { $credsBody.innerHTML = '

Could not load credentials.

'; } } function closeCredsModal() { if ($credsModal) $credsModal.classList.remove("open"); } // ── Tech Support modal ──────────────────────────────────────────── async function openSupportModal() { if (!$supportModal) return; $supportModal.classList.add("open"); $supportBody.innerHTML = '

Checking support status…

'; try { const status = await apiFetch("/api/support/status"); if (status.active) { _supportEnabledAt = status.enabled_at; renderSupportActive(); } else { renderSupportInactive(); } } catch (err) { $supportBody.innerHTML = '

Could not check support status.

'; } } function renderSupportInactive() { stopSupportTimer(); const ip = _cachedExternalIp || "loading…"; $supportBody.innerHTML = `
🛟

Need help from Sovran Systems?

This will temporarily give Sovran Systems secure SSH access to your machine so we can diagnose and fix issues for you.

Your External IP ${escHtml(ip)}

Give this IP to your Sovran Systems technician when asked.

What happens when you click Enable:

  1. A Sovran Systems SSH key is added to this machine
  2. You give us your External IP shown above
  3. We connect and help you remotely
  4. When done, you click End Support Session to remove the key

You can end the session at any time. The access key will be completely removed.

`; document.getElementById("btn-support-enable").addEventListener("click", enableSupport); } function renderSupportActive() { const ip = _cachedExternalIp || "loading…"; $supportBody.innerHTML = `
🔓

Support Access is Active

Sovran Systems can currently connect to your machine via SSH.

Your External IP ${escHtml(ip)}
Session Duration

When your support session is complete, click the button below to immediately remove the access key.

`; document.getElementById("btn-support-disable").addEventListener("click", disableSupport); startSupportTimer(); } function renderSupportRemoved(verified) { stopSupportTimer(); const icon = verified ? "✅" : "⚠️"; const msg = verified ? "The Sovran Systems SSH key has been completely removed from your machine. We no longer have any access." : "The key removal was requested but could not be fully verified. Please reboot your machine to be sure."; $supportBody.innerHTML = `
${icon}

Support Session Ended

${escHtml(msg)}

SSH Key Status: ${verified ? "✓ Removed — No access" : "⚠ Verify by rebooting"}
`; document.getElementById("btn-support-done").addEventListener("click", closeSupportModal); } async function enableSupport() { const btn = document.getElementById("btn-support-enable"); if (btn) { btn.disabled = true; btn.textContent = "Enabling…"; } try { await apiFetch("/api/support/enable", { method: "POST" }); const status = await apiFetch("/api/support/status"); _supportEnabledAt = status.enabled_at; renderSupportActive(); } catch (err) { if (btn) { btn.disabled = false; btn.textContent = "Enable Support Access"; } alert("Failed to enable support access. Please try again."); } } async function disableSupport() { const btn = document.getElementById("btn-support-disable"); if (btn) { btn.disabled = true; btn.textContent = "Removing key…"; } try { const result = await apiFetch("/api/support/disable", { method: "POST" }); renderSupportRemoved(result.verified); } catch (err) { if (btn) { btn.disabled = false; btn.textContent = "End Support Session"; } alert("Failed to disable support access. Please try again."); } } function startSupportTimer() { stopSupportTimer(); updateSupportTimer(); _supportTimerInt = setInterval(updateSupportTimer, SUPPORT_TIMER_INTERVAL); } function stopSupportTimer() { if (_supportTimerInt) { clearInterval(_supportTimerInt); _supportTimerInt = null; } } function updateSupportTimer() { const el = document.getElementById("support-timer"); if (!el || !_supportEnabledAt) return; const elapsed = (Date.now() / 1000) - _supportEnabledAt; el.textContent = formatDuration(Math.max(0, elapsed)); } function closeSupportModal() { if ($supportModal) $supportModal.classList.remove("open"); stopSupportTimer(); } // ── 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(_