"use strict"; /* ── The Hub dashboard chrome ────────────────────────────────────── Welcome dashboard (default view), category navigation, service search, status cards, and the Systems Operational modal. Everything lives in an IIFE so no globals leak into the other Hub scripts; tiles.js calls window.dashboardServicesUpdated() whenever the service list refreshes. */ (function () { var $nav = document.getElementById("sidebar-nav"); var $pageTitle = document.getElementById("page-title"); var $search = document.getElementById("search-input"); var $sysModal = document.getElementById("systems-modal"); var $sysBody = document.getElementById("systems-body"); var $welcome = document.getElementById("welcome-view"); var $tilesArea = document.getElementById("tiles-area"); var $wcSystems = document.getElementById("wc-systems"); var $wcMore = document.getElementById("wc-more"); var $greeting = document.getElementById("welcome-greeting"); var $welcomeRole = document.getElementById("welcome-role"); var $browseBtn = document.getElementById("welcome-browse-btn"); var _view = "dashboard"; // "dashboard" | "services" var _cat = "all"; var _query = ""; var CAT_ICONS = { "all": "g-grid", "infrastructure": "g-server", "bitcoin": "g-btc-sym", "communication": "g-chat", "apps": "g-dots", "nostr": "g-antenna", "other": "g-dots" }; var CAT_FALLBACK_LABELS = { "infrastructure": "Infrastructure", "bitcoin": "Bitcoin", "communication": "Communication", "apps": "Personal Apps", "nostr": "Nostr", "other": "Other" }; /* Units that serve a domain — used to find a live diagnostics checklist for the Systems Operational modal (order matters only for which service is polled first). */ var DOMAIN_UNITS = [ "matrix-synapse.service", "btcpayserver.service", "vaultwarden.service", "phpfpm-nextcloud.service", "phpfpm-wordpress.service", "haven-relay.service", "livekit.service", "albyhub.service" ]; function icon(id) { return ''; } function visibleServices() { var services = (typeof _servicesCache !== "undefined" && _servicesCache) ? _servicesCache : []; return services.filter(function (s) { return s.category !== "support" && s.type !== "support"; }); } /* ── Views ────────────────────────────────────────────────────── */ function setTitle(t) { if ($pageTitle) $pageTitle.textContent = t; } function syncNav() { if (!$nav) return; $nav.querySelectorAll(".nav-item").forEach(function (b) { var isActive; if (b.dataset.cat === "__dash") isActive = (_view === "dashboard"); else isActive = (_view === "services" && b.dataset.cat === _cat); b.classList.toggle("active", isActive); }); } function showDashboard() { _view = "dashboard"; _cat = "all"; if ($welcome) $welcome.style.display = ""; if ($tilesArea) $tilesArea.style.display = "none"; setTitle("Dashboard"); syncNav(); } function showServices(cat) { _view = "services"; if (cat) _cat = cat; if ($welcome) $welcome.style.display = "none"; if ($tilesArea) $tilesArea.style.display = ""; setTitle(_cat === "all" ? "All services" : catLabel(_cat)); syncNav(); applyFilter(); } /* ── Category navigation ──────────────────────────────────────── */ function renderNav() { if (!$nav) return; var counts = {}; var order = []; visibleServices().forEach(function (s) { var cat = s.category || "other"; if (CATEGORY_ALIASES[cat]) cat = CATEGORY_ALIASES[cat]; if (!counts[cat]) { counts[cat] = 0; order.push(cat); } counts[cat]++; }); var total = visibleServices().length; var html = '
'; html += ''; html += ''; html += navItem("all", "All services", total); order.forEach(function (cat) { html += navItem(cat, catLabel(cat), counts[cat]); }); $nav.innerHTML = html; $nav.querySelectorAll(".nav-item").forEach(function (btn) { btn.addEventListener("click", function () { if (btn.dataset.cat === "__dash") showDashboard(); else showServices(btn.dataset.cat); }); }); } function navItem(cat, label, count) { return ''; } function catLabel(cat) { if (typeof _categoryLabels !== "undefined" && _categoryLabels[cat]) return _categoryLabels[cat]; return CAT_FALLBACK_LABELS[cat] || cat; } /* ── Filtering (applies in the services view) ─────────────────── */ function applyFilter() { if (_view !== "services") return; var area = $tilesArea; if (!area) return; var q = _query.trim().toLowerCase(); area.querySelectorAll(".category-section").forEach(function (section) { var catMatch = (_cat === "all") || (section.dataset.category === _cat); var anyVisible = false; section.querySelectorAll(".service-tile").forEach(function (tile) { var nameEl = tile.querySelector(".tile-name"); var name = nameEl ? nameEl.textContent.toLowerCase() : ""; var match = catMatch && (!q || name.indexOf(q) !== -1); tile.style.display = match ? "" : "none"; if (match) anyVisible = true; }); section.style.display = (catMatch && (anyVisible || !q)) ? "" : "none"; }); } if ($search) { $search.addEventListener("input", function () { // Searching implies browsing services — leave the welcome view. if (_view === "dashboard" && $search.value) showServices("all"); _query = $search.value; applyFilter(); }); } if ($browseBtn) { $browseBtn.addEventListener("click", function () { showServices("all"); }); } /* ── Welcome header ───────────────────────────────────────────── */ function updateWelcomeMeta() { if ($greeting) { var h = new Date().getHours(); var g; if (h >= 5 && h < 12) g = "Good morning"; else if (h >= 12 && h < 17) g = "Good afternoon"; else g = "Good evening"; $greeting.textContent = g; } if ($welcomeRole) { $welcomeRole.textContent = (typeof window._roleLabel !== "undefined" && window._roleLabel) ? window._roleLabel : ""; } } /* ── Status cards ─────────────────────────────────────────────── */ function serviceCounts() { var services = visibleServices(); var running = 0, attention = 0, off = 0; var attentionNames = [], offNames = []; services.forEach(function (s) { if (!s.enabled) { off++; offNames.push(s.name); return; } var h = s.health || s.status; if (h === "needs_attention" || h === "failed") { attention++; attentionNames.push(s.name); } else running++; }); return { services: services, running: running, attention: attention, off: off, attentionNames: attentionNames, offNames: offNames }; } function renderWidgets() { if (!$wcSystems) return; var c = serviceCounts(); if (!c.services.length) { $wcSystems.innerHTML = ""; if ($wcMore) $wcMore.innerHTML = ""; return; } /* Systems operational */ var sub = '' + c.running + ' running'; if (c.attention) sub += ' · ' + c.attention + ' needs attention'; if (c.off) sub += ' · ' + c.off + ' off'; var attentionTitle = c.attention ? "Systems need attention" : "Systems operational"; $wcSystems.innerHTML = ''; var wSys = document.getElementById("w-systems"); if (wSys) { wSys.addEventListener("click", openSystemsModal); wSys.addEventListener("keydown", function (e) { if (e.key === "Enter") openSystemsModal(); }); } if (!$wcMore) return; var more = ""; /* Bitcoin Core sync */ var btc = null; c.services.forEach(function (s) { if (s.sync_ibd && s.enabled) btc = s; }); if (btc) { var pct = Math.round((btc.sync_progress || 0) * 100); var blocks = btc.sync_blocks ? btc.sync_blocks.toLocaleString() : "—"; var eta = (typeof _calcBtcEta === "function") ? _calcBtcEta(btc.unit + "::" + btc.name, btc.sync_progress || 0) : ""; more += ''; } else { var btcDone = null; c.services.forEach(function (s) { if (s.unit === "bitcoind.service" && s.enabled) btcDone = s; }); if (btcDone) { var blk = btcDone.sync_blocks ? btcDone.sync_blocks.toLocaleString() : ""; more += ''; } } /* Updates — mirror the sidebar's read of the update state, so the card never claims "up to date" while an update failed or needs a restart */ var upd = (typeof window._lastUpdateCheck === "object" && window._lastUpdateCheck) ? window._lastUpdateCheck : null; var updStatus = (upd && upd.status) || "idle"; var updTitle, updSub, updChip; if (updStatus === "failed") { updTitle = "Update failed"; updSub = "Click to retry the update"; updChip = "chip-amber"; } else if (updStatus === "reboot_required") { updTitle = "Restart required"; updSub = "Click to restart and finish the update"; updChip = "chip-amber"; } else if (updStatus === "running") { updTitle = "Update in progress"; updSub = "Installing the new system generation"; updChip = "chip-amber"; } else if (upd && upd.available) { updTitle = "Updates available"; updSub = "Click to review and update"; updChip = "chip-amber"; } else { updTitle = "System is up to date"; updSub = "Sovran_SystemsOS keeps itself current"; updChip = "chip-green"; } more += ''; $wcMore.innerHTML = more; var wBtc = document.getElementById("w-btc"); if (wBtc) { var svc = btc || btcDone; if (svc) { wBtc.addEventListener("click", function () { openServiceDetailModal(svc.unit, svc.name, svc.icon); }); wBtc.addEventListener("keydown", function (e) { if (e.key === "Enter") openServiceDetailModal(svc.unit, svc.name, svc.icon); }); } } var wUpd = document.getElementById("w-updates"); if (wUpd) { wUpd.addEventListener("click", function () { openUpdateModal(); }); wUpd.addEventListener("keydown", function (e) { if (e.key === "Enter") openUpdateModal(); }); } } /* ── Systems Operational modal ────────────────────────────────── */ function isNodeRole() { return (typeof _currentRole !== "undefined" && _currentRole === "node"); } function hasEnabledDomainService(services) { return DOMAIN_UNITS.some(function (u) { return services.some(function (s) { return s.unit === u && s.enabled; }); }); } function whoUsesPorts() { if (isNodeRole()) { return 'On this Bitcoin Node install, BTCPay Server and Lightning Wallet Connections (LNURL) are the domain services that use these ports.'; } return 'All your domain services share ports 80 and 443 — Matrix, BTCPay Server, VaultWarden, Nextcloud, WordPress, Haven Relay, Lightning Wallet Connections, and Element Calling.'; } function step(n, title, sub, value) { return '