"use strict"; /* ── The Hub dashboard chrome ────────────────────────────────────── Category navigation, service search, status widgets, and the Systems Operational modal (router ports 80/443 + the live domain diagnostics checklist). 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 $widgets = document.getElementById("widgets"); var $search = document.getElementById("search-input"); var $sysModal = document.getElementById("systems-modal"); var $sysBody = document.getElementById("systems-body"); var _cat = "all"; var _query = ""; var CAT_ICONS = { "all": "g-grid", "infrastructure": "g-server", "bitcoin-base": "g-btc-sym", "bitcoin-apps": "g-btc-sym", "communication": "g-chat", "apps": "g-dots", "nostr": "g-antenna", "other": "g-dots" }; var CAT_FALLBACK_LABELS = { "infrastructure": "Infrastructure", "bitcoin-base": "Bitcoin", "bitcoin-apps": "Bitcoin Apps", "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"; }); } /* ── Category navigation ─────────────────────────────────────── */ function renderNav() { if (!$nav) return; var counts = {}; var order = []; visibleServices().forEach(function (s) { var cat = s.category || "other"; if (!counts[cat]) { counts[cat] = 0; order.push(cat); } counts[cat]++; }); var total = visibleServices().length; var 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 () { _cat = btn.dataset.cat; $nav.querySelectorAll(".nav-item").forEach(function (b) { b.classList.toggle("active", b === btn); }); if ($pageTitle) { $pageTitle.textContent = _cat === "all" ? "Dashboard" : catLabel(_cat); } applyFilter(); }); }); } 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 (category + search, applied together) ─────────── */ function applyFilter() { var area = document.getElementById("tiles-area"); 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 () { _query = $search.value; applyFilter(); }); } /* ── Status widgets ──────────────────────────────────────────── */ function renderWidgets() { if (!$widgets) return; var services = visibleServices(); if (!services.length) { $widgets.style.display = "none"; return; } var running = 0, attention = 0, off = 0; services.forEach(function (s) { if (!s.enabled) { off++; return; } var h = s.health || s.status; if (h === "needs_attention" || h === "failed") attention++; else running++; }); var html = ""; /* Systems operational */ var sub = '' + running + ' running'; if (attention) sub += ' · ' + attention + ' needs attention'; if (off) sub += ' · ' + off + ' off'; var attentionTitle = attention ? "Systems need attention" : "Systems operational"; html += '
' + '
' + icon("g-shield-check") + '
' + '

' + attentionTitle + '

' + sub + '
' + '' + icon("g-chev") + '' + '
'; /* Bitcoin Core sync */ var btc = null; 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) : ""; html += '
' + '
' + '

' + escHtml(btc.name) + ' — syncing timechain

' + '
' + '
Block ' + blocks + ' · ' + pct + '% · ' + escHtml(eta) + '
' + '' + icon("g-chev") + '' + '
'; } $widgets.innerHTML = html; $widgets.style.display = ""; var wSys = document.getElementById("w-systems"); if (wSys) { wSys.addEventListener("click", openSystemsModal); wSys.addEventListener("keydown", function (e) { if (e.key === "Enter") openSystemsModal(); }); } var wBtc = document.getElementById("w-btc"); if (wBtc && btc) { (function (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); }); })(btc); } } /* ── Systems Operational modal ───────────────────────────────── */ function internalIp() { var el = document.getElementById("ip-internal"); var v = el ? el.textContent.trim() : ""; return (v && v !== "…" && v !== "—") ? v : ""; } function step(n, title, sub, value) { return '
' + n + '
' + '
' + title + (sub ? ' · ' + sub + '' : '') + '
' + (value ? '
' + value + '
' : '') + '
'; } 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 openSystemsModal() { if (!$sysModal || !$sysBody) return; 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++; }); var ip = internalIp(); var fwd = ip ? ('\u2192 ' + escHtml(ip)) : "forward to this computer"; var html = ""; /* System status */ html += '
' + '
' + icon("g-shield-check") + 'System Status
' + step(1, "Services running", "", String(running)) + step(2, "Needs attention", "", attention ? escHtml(attentionNames.join(", ")) : "None") + step(3, "Turned off", "", off ? escHtml(offNames.join(", ")) : "None") + '
'; /* Router ports — repo wording (domain-prereqs.js / server.py). Node-only role: ports matter only once BTCPay Server or Lightning Wallet Connections (LNURL) is turned on — until then, no router task. */ if (isNodeRole() && !hasEnabledDomainService(services)) { html += '
' + '
' + icon("g-wifi") + 'Router
' + '
' + icon("g-check") + 'No router setup needed yet
' + '
Ports 80 and 443 only need to be forwarded on your router if you turn on BTCPay Server or Lightning Wallet Connections (LNURL). If you enable one of them, come back here — this card will show exactly what to do.
' + '
'; } else { html += '
' + '
' + icon("g-wifi") + 'Router — Ports to Forward
' + step(1, "Port 80", "TCP — HTTP (redirect to HTTPS)", fwd) + step(2, "Port 443", "TCP — HTTPS", fwd) + '
' + icon("g-alert") + 'One router task
' + '
Set the internal and external port to the same number. You only need to do this once — all your services share these two ports.
' + '
'; } /* Live domain diagnostics (sequential checklist from the backend — same data the domain-service modals show) */ html += ''; /* Who uses these ports (redundant on a Node install with no domain services on — the router note above already covers it) */ if (!isNodeRole() || hasEnabledDomainService(services)) { html += '
' + '
' + icon("g-antenna") + 'Who uses these ports
' + '
' + whoUsesPorts() + '
'; } $sysBody.innerHTML = html; $sysModal.classList.add("open"); /* Poll the first configured domain service for its live checklist */ var checkCard = document.getElementById("sys-check-card"); var stepsEl = document.getElementById("sys-check-steps"); var units = DOMAIN_UNITS.filter(function (u) { return services.some(function (s) { return s.unit === u && s.enabled; }); }); if (!units.length || !checkCard || !stepsEl) return; checkCard.style.display = ""; apiFetch("/api/service-detail/" + encodeURIComponent(units[0])) .then(function (data) { var steps = (data && data.domain_check_steps) || []; if (!steps.length) { checkCard.style.display = "none"; return; } var mark = { ok: "\u2705", error: "\u274c", warning: "\u26a0\ufe0f", skipped: "\u23ed\ufe0f" }; var html2 = ""; steps.forEach(function (st) { var m = mark[st.status] || "\u2014"; var detail = escHtml(st.detail || "").replace(/\n/g, "
"); html2 += '
' + st.step + '
' + '
' + m + " " + escHtml(st.label || "") + '
' + (detail ? '
' + detail + '
' : '') + '
'; }); stepsEl.innerHTML = html2; }) .catch(function () { checkCard.style.display = "none"; }); } function closeSystemsModal() { if ($sysModal) $sysModal.classList.remove("open"); } var sysClose = document.getElementById("systems-close-btn"); if (sysClose) sysClose.addEventListener("click", closeSystemsModal); if ($sysModal) { $sysModal.addEventListener("click", function (e) { if (e.target === $sysModal) closeSystemsModal(); }); $sysModal.addEventListener("keydown", function (e) { if (e.key === "Escape") closeSystemsModal(); }); } /* ── Public hook for tiles.js ────────────────────────────────── */ window.dashboardServicesUpdated = function () { renderNav(); renderWidgets(); applyFilter(); }; })();