/* Sovran_SystemsOS Hub β€” Vanilla JS Frontend v8 β€” Status-only dashboard + Tech Support + Feature Manager */ "use strict"; var POLL_INTERVAL_SERVICES = 5000; var POLL_INTERVAL_UPDATES = 1800000; var UPDATE_POLL_INTERVAL = 2000; var REBOOT_CHECK_INTERVAL = 5000; var SUPPORT_TIMER_INTERVAL = 1000; var CATEGORY_ORDER = [ "infrastructure", "bitcoin-base", "bitcoin-apps", "communication", "apps", "nostr", "support", "feature-manager", ]; var FEATURE_SUBCATEGORY_LABELS = { "infrastructure": "πŸ”§ Infrastructure", "bitcoin": "β‚Ώ Bitcoin", "communication": "πŸ’¬ Communication", "nostr": "πŸ“‘ Nostr", }; var FEATURE_SUBCATEGORY_ORDER = ["infrastructure", "bitcoin", "communication", "nostr"]; var FEATURE_UNIT_MAP = { "rdp": "gnome-remote-desktop.service", "haven": "haven-relay.service", "element-calling": "livekit.service", "mempool": "mempool-frontend.service", }; var STATUS_LOADING_STATES = new Set([ "reloading", "activating", "deactivating", "maintenance", ]); // ── State ───────────────────────────────────────────────────────── var _servicesCache = []; var _categoryLabels = {}; var _updateLog = ""; var _updatePollTimer = null; var _updateLogOffset = 0; var _serverWasDown = false; var _updateFinished = false; var _supportTimerInt = null; var _supportEnabledAt = null; var _cachedExternalIp = null; // Feature Manager state var _featuresData = null; var _rebuildLog = ""; var _rebuildLogOffset = 0; var _rebuildPollTimer = null; var _rebuildFinished = false; var _rebuildServerDown = false; var _pendingToggle = null; // ── DOM refs ────────────────────────────────────────────────────── var $tilesArea = document.getElementById("tiles-area"); var $updateBtn = document.getElementById("btn-update"); var $updateBadge = document.getElementById("update-badge"); var $refreshBtn = document.getElementById("btn-refresh"); var $internalIp = document.getElementById("ip-internal"); var $externalIp = document.getElementById("ip-external"); var $modal = document.getElementById("update-modal"); var $modalSpinner = document.getElementById("modal-spinner"); var $modalStatus = document.getElementById("modal-status"); var $modalLog = document.getElementById("modal-log"); var $btnReboot = document.getElementById("btn-reboot"); var $btnSave = document.getElementById("btn-save-report"); var $btnCloseModal = document.getElementById("btn-close-modal"); var $rebootOverlay = document.getElementById("reboot-overlay"); var $credsModal = document.getElementById("creds-modal"); var $credsTitle = document.getElementById("creds-modal-title"); var $credsBody = document.getElementById("creds-body"); var $credsCloseBtn = document.getElementById("creds-close-btn"); var $supportModal = document.getElementById("support-modal"); var $supportBody = document.getElementById("support-body"); var $supportCloseBtn = document.getElementById("support-close-btn"); // Feature Manager β€” rebuild modal var $rebuildModal = document.getElementById("rebuild-modal"); var $rebuildSpinner = document.getElementById("rebuild-spinner"); var $rebuildStatus = document.getElementById("rebuild-status"); var $rebuildLogEl = document.getElementById("rebuild-log"); var $rebuildReboot = document.getElementById("rebuild-reboot-btn"); var $rebuildSave = document.getElementById("rebuild-save-report"); var $rebuildClose = document.getElementById("rebuild-close-btn"); // Feature Manager β€” domain setup modal var $domainSetupModal = document.getElementById("domain-setup-modal"); var $domainSetupTitle = document.getElementById("domain-setup-title"); var $domainSetupBody = document.getElementById("domain-setup-body"); var $domainSetupClose = document.getElementById("domain-setup-close-btn"); // Feature Manager β€” SSL email modal var $sslEmailModal = document.getElementById("ssl-email-modal"); var $sslEmailInput = document.getElementById("ssl-email-input"); var $sslEmailSave = document.getElementById("ssl-email-save-btn"); var $sslEmailCancel = document.getElementById("ssl-email-cancel-btn"); var $sslEmailClose = document.getElementById("ssl-email-close-btn"); // Feature Manager β€” confirm modal var $featureConfirmModal = document.getElementById("feature-confirm-modal"); var $featureConfirmMsg = document.getElementById("feature-confirm-message"); var $featureConfirmOk = document.getElementById("feature-confirm-ok-btn"); var $featureConfirmCancel = document.getElementById("feature-confirm-cancel-btn"); var $featureConfirmClose = document.getElementById("feature-confirm-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) { return escHtml(str).replace(/(https?:\/\/[^\s<]+)/g, '$1'); } function formatDuration(seconds) { var h = Math.floor(seconds / 3600); var m = Math.floor((seconds % 3600) / 60); var 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) { var 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; var grouped = {}; for (var i = 0; i < services.length; i++) { var cat = services[i].category || "other"; if (!grouped[cat]) grouped[cat] = []; grouped[cat].push(services[i]); } $tilesArea.innerHTML = ""; var orderedKeys = CATEGORY_ORDER.filter(function(k) { return grouped[k]; }); Object.keys(grouped).forEach(function(k) { if (orderedKeys.indexOf(k) === -1) orderedKeys.push(k); }); for (var j = 0; j < orderedKeys.length; j++) { var catKey = orderedKeys[j]; var entries = grouped[catKey]; if (!entries || entries.length === 0) continue; var label = categoryLabels[catKey] || catKey; var section = document.createElement("div"); section.className = "category-section"; section.dataset.category = catKey; section.innerHTML = '
' + escHtml(label) + '

'; var grid = section.querySelector(".tiles-grid"); for (var k = 0; k < entries.length; k++) { grid.appendChild(buildTile(entries[k])); } $tilesArea.appendChild(section); } if ($tilesArea.children.length === 0) { $tilesArea.innerHTML = '

No services configured.

'; } } function buildTile(svc) { var isSupport = svc.type === "support"; var sc = statusClass(svc.status); var st = statusText(svc.status, svc.enabled); var dis = !svc.enabled; var hasCreds = svc.has_credentials && svc.enabled; var 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) { tile.innerHTML = '' + escHtml(svc.name) + '
' + escHtml(svc.name) + '
Click to manage
'; tile.style.cursor = "pointer"; tile.addEventListener("click", function() { openSupportModal(); }); return tile; } var infoBtn = hasCreds ? '' : ""; tile.innerHTML = infoBtn + '' + escHtml(svc.name) + '
' + escHtml(svc.name) + '
' + escHtml(st) + '
'; var infoBtnEl = tile.querySelector(".tile-info-btn"); if (infoBtnEl) { infoBtnEl.addEventListener("click", function(e) { e.stopPropagation(); openCredsModal(svc.unit, svc.name); }); } return tile; } // ── Render: live update ─────────────────────────────────────────── function updateTiles(services) { _servicesCache = services; for (var i = 0; i < services.length; i++) { var svc = services[i]; if (svc.type === "support") continue; var id = CSS.escape(tileId(svc)); var tile = $tilesArea.querySelector('.service-tile[data-tile-id="' + id + '"]'); if (!tile) continue; var sc = statusClass(svc.status); var st = statusText(svc.status, svc.enabled); var dot = tile.querySelector(".status-dot"); var text = tile.querySelector(".status-text"); if (dot) dot.className = "status-dot " + sc; if (text) text.textContent = st; } } // ── Service polling ─────────────────────────────────────────────── var _firstLoad = true; async function refreshServices() { try { var 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 { var 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 { var data = await apiFetch("/api/updates/check"); var 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 { var data = await apiFetch("/api/credentials/" + encodeURIComponent(unit)); if (!data.credentials || data.credentials.length === 0) { $credsBody.innerHTML = '

No connection info available yet.

'; return; } var html = ""; for (var i = 0; i < data.credentials.length; i++) { var cred = data.credentials[i]; var id = "cred-" + Math.random().toString(36).substring(2, 8); var displayValue = linkify(cred.value); var 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(function(btn) { btn.addEventListener("click", function() { var target = document.getElementById(btn.dataset.target); if (target) { navigator.clipboard.writeText(target.textContent).then(function() { btn.textContent = "Copied!"; btn.classList.add("copied"); setTimeout(function() { btn.textContent = "Copy"; btn.classList.remove("copied"); }, 1500); }).catch(function() {}); } }); }); } 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 { var 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(); var 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() { var 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(); var icon = verified ? "βœ…" : "⚠️"; var 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."; var vclass = verified ? "verified-gone" : "verify-warning"; var vlabel = verified ? "βœ“ Removed β€” No access" : "⚠ Verify by rebooting"; $supportBody.innerHTML = '
' + icon + '

Support Session Ended

' + escHtml(msg) + '

SSH Key Status:' + vlabel + '
'; document.getElementById("btn-support-done").addEventListener("click", closeSupportModal); } async function enableSupport() { var btn = document.getElementById("btn-support-enable"); if (btn) { btn.disabled = true; btn.textContent = "Enabling…"; } try { await apiFetch("/api/support/enable", { method: "POST" }); var status = await apiFetch("/api/support/status");