/* 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 = '
No services configured.
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 = '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 = 'This will temporarily give Sovran Systems secure SSH access to your machine so we can diagnose and fix issues for you.
Give this IP to your Sovran Systems technician when asked.
What happens when you click Enable:
You can end the session at any time. The access key will be completely removed.
Sovran Systems can currently connect to your machine via SSH.
When your support session is complete, click the button below to immediately remove the access key.
' + escHtml(msg) + '