From cf176ea2db144d4c467234d0a17343ea8e2b5075 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 15:46:48 +0000 Subject: [PATCH 1/2] Initial plan From 7361047b48eeb02f663bc336f95c3d57ddcceca7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 15:52:44 +0000 Subject: [PATCH 2/2] Add composite health status, smart port language, remove banner, center layout, bigger logo Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/fbd178f9-a25d-4065-b3c1-79eecd3caade Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com> --- app/sovran_systemsos_web/server.py | 55 +++++ app/sovran_systemsos_web/static/app.js | 226 +++++++----------- app/sovran_systemsos_web/static/style.css | 133 +---------- app/sovran_systemsos_web/templates/index.html | 3 - 4 files changed, 147 insertions(+), 270 deletions(-) diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index dec9bf5..099781c 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -1198,6 +1198,12 @@ async def api_services(): # Read runtime feature overrides from custom.nix Hub Managed section overrides, _ = await loop.run_in_executor(None, _read_hub_overrides) + # Cache port/firewall data once for the entire /api/services request + listening_ports, firewall_ports = await asyncio.gather( + loop.run_in_executor(None, _get_listening_ports), + loop.run_in_executor(None, _get_firewall_allowed_ports), + ) + async def get_status(entry): unit = entry.get("unit", "") scope = entry.get("type", "system") @@ -1235,6 +1241,34 @@ async def api_services(): except OSError: domain = None + # Compute composite health + if not enabled: + health = "disabled" + elif status == "active": + has_port_issues = False + if port_requirements: + for p in port_requirements: + ps = _check_port_status( + str(p.get("port", "")), + str(p.get("protocol", "TCP")), + listening_ports, + firewall_ports, + ) + if ps == "closed": + has_port_issues = True + break + has_domain_issues = False + if needs_domain: + if not domain: + has_domain_issues = True + health = "needs_attention" if (has_port_issues or has_domain_issues) else "healthy" + elif status == "inactive": + health = "inactive" + elif status == "failed": + health = "failed" + else: + health = status # loading states, etc. + return { "name": entry.get("name", ""), "unit": unit, @@ -1243,6 +1277,7 @@ async def api_services(): "enabled": enabled, "category": entry.get("category", "other"), "status": status, + "health": health, "has_credentials": has_credentials, "port_requirements": port_requirements, "needs_domain": needs_domain, @@ -1424,11 +1459,31 @@ async def api_service_detail(unit: str): "description": p.get("description", ""), }) + # Compute composite health + if not enabled: + health = "disabled" + elif status == "active": + has_port_issues = any(p["status"] == "closed" for p in port_statuses) + has_domain_issues = False + if needs_domain: + if not domain: + has_domain_issues = True + elif domain_status and domain_status.get("status") not in ("connected", None): + has_domain_issues = True + health = "needs_attention" if (has_port_issues or has_domain_issues) else "healthy" + elif status == "inactive": + health = "inactive" + elif status == "failed": + health = "failed" + else: + health = status # loading states, etc. + return { "name": entry.get("name", ""), "unit": unit, "icon": icon, "status": status, + "health": health, "enabled": enabled, "description": SERVICE_DESCRIPTIONS.get(unit, ""), "has_credentials": has_credentials and bool(resolved_creds), diff --git a/app/sovran_systemsos_web/static/app.js b/app/sovran_systemsos_web/static/app.js index 5533b7d..bef1aa3 100644 --- a/app/sovran_systemsos_web/static/app.js +++ b/app/sovran_systemsos_web/static/app.js @@ -4,12 +4,9 @@ const POLL_INTERVAL_SERVICES = 5000; const POLL_INTERVAL_UPDATES = 1800000; -const POLL_INTERVAL_PORT_HEALTH = 15000; const UPDATE_POLL_INTERVAL = 2000; const REBOOT_CHECK_INTERVAL = 5000; const SUPPORT_TIMER_INTERVAL = 1000; -const BANNER_AUTO_FADE_DELAY = 5000; -const BANNER_FADE_TRANSITION_MS = 550; const CATEGORY_ORDER = [ "infrastructure", @@ -124,26 +121,34 @@ const $portReqBody = document.getElementById("port-req-body"); const $portReqClose = document.getElementById("port-req-close-btn"); // System status banner -const $statusBanner = document.getElementById("system-status-banner"); +// (removed — health is now shown per-tile via the composite health field) // ── 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"; +function statusClass(health) { + if (!health) return "unknown"; + if (health === "healthy") return "active"; + if (health === "needs_attention") return "needs-attention"; + if (health === "active") return "active"; // backwards compat + if (health === "inactive") return "inactive"; + if (health === "failed") return "failed"; + if (health === "disabled") return "disabled"; + if (STATUS_LOADING_STATES.has(health)) return "loading"; return "unknown"; } -function statusText(status, enabled) { - if (!enabled) return "disabled"; - if (!status || status === "unknown") return "unknown"; - return status; +function statusText(health, enabled) { + if (!enabled) return "Disabled"; + if (health === "healthy") return "Active"; + if (health === "needs_attention") return "Needs Attention"; + if (health === "active") return "Active"; + if (health === "inactive") return "Inactive"; + if (health === "failed") return "Failed"; + if (!health || health === "unknown") return "Unknown"; + if (STATUS_LOADING_STATES.has(health)) return health; + return health; } function escHtml(str) { @@ -242,8 +247,8 @@ function renderSidebarSupport(supportServices) { function buildTile(svc) { var isSupport = svc.type === "support"; - var sc = statusClass(svc.status); - var st = statusText(svc.status, svc.enabled); + var sc = statusClass(svc.health || svc.status); + var st = statusText(svc.health || svc.status, svc.enabled); var dis = !svc.enabled; var tile = document.createElement("div"); @@ -279,8 +284,8 @@ function updateTiles(services) { 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 sc = statusClass(svc.health || svc.status); + var st = statusText(svc.health || svc.status, svc.enabled); var dot = tile.querySelector(".status-dot"); var text = tile.querySelector(".status-text"); if (dot) dot.className = "status-dot " + sc; @@ -396,8 +401,8 @@ async function openServiceDetailModal(unit, name) { } // Section B: Status - var sc = statusClass(data.status); - var st = statusText(data.status, data.enabled); + var sc = statusClass(data.health || data.status); + var st = statusText(data.health || data.status, data.enabled); html += '
' + '
Status
' + '
' + @@ -425,26 +430,69 @@ async function openServiceDetailModal(unit, name) { statusIcon = "— Unknown"; statusClass2 = "port-status-unknown"; } + var desc = p.description; + var portNum = parseInt(p.port, 10); + if (portNum === 80 || portNum === 443) { + desc += " (shared — all services)"; + } portTableRows += '' + '' + escHtml(p.port) + '' + '' + escHtml(p.protocol) + '' + - '' + escHtml(p.description) + '' + + '' + escHtml(desc) + '' + '' + statusIcon + '' + ''; }); var troubleshootHtml = ""; if (anyPortClosed) { - troubleshootHtml = '
' + - '⚠️ Some ports are not open yet. Here\'s how to fix it:' + - '
    ' + - '
  1. Log into your router\'s admin panel (usually http://192.168.1.1)
  2. ' + - '
  3. Find the Port Forwarding section
  4. ' + - '
  5. Forward each closed port below to this machine\'s internal IP: ' + escHtml(data.internal_ip || "—") + '
  6. ' + - '
  7. Save your router settings
  8. ' + - '
' + - '

💡 Search "how to set up port forwarding on [your router model]" for step-by-step instructions.

' + - '
'; + var sharedPorts = []; + var specificPorts = []; + data.port_statuses.forEach(function(p) { + if (p.status === "closed") { + var portNum = parseInt(p.port, 10); + if (portNum === 80 || portNum === 443) { + sharedPorts.push(p); + } else { + specificPorts.push(p); + } + } + }); + + var troubleParts = []; + + if (sharedPorts.length > 0) { + troubleParts.push( + '⚠️ Ports 80 and 443 need to be forwarded on your router.' + + '

These are shared system ports — you only need to set them up once and they cover all your domain-based services ' + + '(BTCPayServer, Nextcloud, Matrix, WordPress, etc.).

' + + '

If you already forwarded these ports during onboarding, you don\'t need to do it again. Otherwise:

' + + '
    ' + + '
  1. Log into your router\'s admin panel (usually http://192.168.1.1)
  2. ' + + '
  3. Find the Port Forwarding section
  4. ' + + '
  5. Forward port 80 (TCP) and port 443 (TCP) to your machine\'s internal IP: ' + escHtml(data.internal_ip || "—") + '
  6. ' + + '
  7. Save your router settings
  8. ' + + '
' + + '

💡 Once these two ports are forwarded, you won\'t see this warning on any service again.

' + ); + } + + if (specificPorts.length > 0) { + var portList = specificPorts.map(function(p) { + return '' + escHtml(p.port) + ' (' + escHtml(p.protocol) + ') — ' + escHtml(p.description); + }).join('
'); + + troubleParts.push( + '⚠️ This service requires additional ports to be forwarded:' + + '

' + portList + '

' + + '
    ' + + '
  1. Log into your router\'s admin panel
  2. ' + + '
  3. Forward each port listed above to your machine\'s internal IP: ' + escHtml(data.internal_ip || "—") + '
  4. ' + + '
  5. Save your router settings
  6. ' + + '
' + ); + } + + troubleshootHtml = '
' + troubleParts.join('
') + '
'; } html += '
' + @@ -1750,116 +1798,6 @@ if ($modal) $modal.addEventListener("click", function(e) { if (e.target === $mod if ($credsModal) $credsModal.addEventListener("click", function(e) { if (e.target === $credsModal) closeCredsModal(); }); if ($supportModal) $supportModal.addEventListener("click", function(e) { if (e.target === $supportModal) closeSupportModal(); }); -// ── Port health banner ──────────────────────────────────────────── - -var _bannerFadeTimer = null; -var _bannerDetailsOpen = false; - -async function loadPortHealth() { - if (!$statusBanner) return; - try { - var data = await apiFetch("/api/ports/health"); - _renderPortHealthBanner(data); - } catch (_) { - // Silently ignore — banner stays hidden on error - } -} - -function _renderPortHealthBanner(data) { - if (!$statusBanner) return; - - // Clear any pending fade-out timer - if (_bannerFadeTimer) { - clearTimeout(_bannerFadeTimer); - _bannerFadeTimer = null; - } - - var status = data.status || "ok"; - var totalPorts = data.total_ports || 0; - var closedPorts = data.closed_ports || 0; - var affectedSvcs = data.affected_services || []; - - // No port requirements — hide banner - if (totalPorts === 0) { - $statusBanner.style.display = "none"; - $statusBanner.className = "status-banner"; - return; - } - - // Build expandable details for warn/critical states - function buildDetailsHtml(svcs) { - if (!svcs.length) return ""; - var rows = svcs.map(function(svc) { - var portList = (svc.closed_ports || []).map(function(p) { - return '🔴 ' + escHtml(p.port) + '/' + escHtml(p.protocol) + '' - + (p.description ? ' — ' + escHtml(p.description) + '' : ''); - }).join(", "); - return '' + escHtml(svc.name) + '' + portList + ''; - }).join(""); - return '' - + '' - + '' + rows + '' - + '
ServiceClosed Ports
'; - } - - var html = ""; - $statusBanner.className = "status-banner"; - - if (status === "ok") { - // Switching from warn/critical to ok: reset details-open state - _bannerDetailsOpen = false; - $statusBanner.classList.add("status-banner--ok"); - html = "✅ All Systems Operational — All ports open for all enabled services"; - $statusBanner.style.display = "block"; - $statusBanner.style.opacity = "1"; - $statusBanner.innerHTML = html; - // Auto-fade after BANNER_AUTO_FADE_DELAY - _bannerFadeTimer = setTimeout(function() { - $statusBanner.classList.add("status-banner--fade-out"); - _bannerFadeTimer = setTimeout(function() { - $statusBanner.style.display = "none"; - }, BANNER_FADE_TRANSITION_MS); - }, BANNER_AUTO_FADE_DELAY); - return; - } - - if (status === "partial") { - $statusBanner.classList.add("status-banner--warn"); - html = "⚠️ Some Services May Be Affected — " + closedPorts + " of " + totalPorts + " ports closed"; - } else { - // critical - $statusBanner.classList.add("status-banner--critical"); - html = "⚠ Some ports are closed — certain services may be affected"; - } - - var detailsId = "status-banner-detail-body"; - var toggleId = "status-banner-toggle"; - var detailsHtml = buildDetailsHtml(affectedSvcs); - - html += ' ' - + '
' - + detailsHtml - + '
'; - - $statusBanner.style.display = "block"; - $statusBanner.style.opacity = "1"; - $statusBanner.innerHTML = html; - - var toggleBtn = document.getElementById(toggleId); - var detailsBody = document.getElementById(detailsId); - - if (toggleBtn && detailsBody) { - toggleBtn.addEventListener("click", function() { - _bannerDetailsOpen = !_bannerDetailsOpen; - detailsBody.style.display = _bannerDetailsOpen ? "block" : "none"; - toggleBtn.textContent = _bannerDetailsOpen ? "Hide Details ▲" : "View Details ▼"; - }); - } -} - // ── Init ────────────────────────────────────────────────────────── async function init() { @@ -1887,11 +1825,9 @@ async function init() { await refreshServices(); loadNetwork(); checkUpdates(); - loadPortHealth(); setInterval(refreshServices, POLL_INTERVAL_SERVICES); setInterval(checkUpdates, POLL_INTERVAL_UPDATES); - setInterval(loadPortHealth, POLL_INTERVAL_PORT_HEALTH); if (cfg.feature_manager) { loadFeatureManager(); @@ -1900,10 +1836,8 @@ async function init() { await refreshServices(); loadNetwork(); checkUpdates(); - loadPortHealth(); setInterval(refreshServices, POLL_INTERVAL_SERVICES); setInterval(checkUpdates, POLL_INTERVAL_UPDATES); - setInterval(loadPortHealth, POLL_INTERVAL_PORT_HEALTH); } } diff --git a/app/sovran_systemsos_web/static/style.css b/app/sovran_systemsos_web/static/style.css index f795d0e..b7c0ee4 100644 --- a/app/sovran_systemsos_web/static/style.css +++ b/app/sovran_systemsos_web/static/style.css @@ -64,10 +64,10 @@ body { } .header-logo { - height: 30px; + height: 46px; width: auto; vertical-align: middle; - margin-right: 8px; + margin-right: 10px; } .role-badge { @@ -196,120 +196,6 @@ button:disabled { color: var(--border-color); } -/* ── System status banner ────────────────────────────────────────── */ - -.status-banner { - padding: 10px 24px; - font-size: 0.85rem; - font-weight: 600; - text-align: center; - transition: opacity 0.5s ease, max-height 0.3s ease; - overflow: hidden; -} - -.status-banner--ok { - background-color: rgba(46, 194, 126, 0.15); - border-bottom: 1px solid var(--green); - color: var(--green); -} - -.status-banner--warn { - background-color: rgba(229, 165, 10, 0.08); - border-bottom: 1px solid rgba(229, 165, 10, 0.4); - color: var(--text-secondary); -} - -.status-banner--critical { - background-color: rgba(224, 27, 36, 0.08); - border-bottom: 1px solid rgba(224, 27, 36, 0.4); - color: var(--text-secondary); -} - -.status-banner--fade-out { - opacity: 0; - pointer-events: none; -} - -.status-banner-details { - margin-top: 8px; - text-align: left; - max-width: 720px; - margin-left: auto; - margin-right: auto; -} - -.status-banner-toggle { - background: none; - border: none; - font: inherit; - color: inherit; - font-size: 0.82rem; - font-weight: 600; - cursor: pointer; - text-decoration: underline; - padding: 0; - opacity: 0.8; -} - -.status-banner-toggle:hover { - opacity: 1; -} - -.status-banner-table { - width: 100%; - border-collapse: collapse; - margin-top: 8px; - font-size: 0.8rem; - color: var(--text-secondary); -} - -.status-banner-table th { - text-align: left; - padding: 4px 8px; - opacity: 0.7; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; - font-size: 0.72rem; - border-bottom: 1px solid var(--border-color); -} - -.status-banner-table td { - padding: 4px 8px; - vertical-align: top; -} - -.status-banner-table td:first-child { - font-weight: 600; - white-space: nowrap; -} - -.status-banner-port { - font-family: 'JetBrains Mono', 'Fira Code', 'Source Code Pro', monospace; - font-size: 0.78rem; - font-weight: 600; -} - -@media (max-width: 768px) { - .status-banner { - padding: 10px 16px; - } - .status-banner-details { - max-width: 100%; - } -} - -@media (max-width: 600px) { - .status-banner { - padding: 10px 14px; - font-size: 0.82rem; - } - .status-banner-table th, - .status-banner-table td { - padding: 4px 4px; - } -} - /* ── Main content ───────────────────────────────────────────────── */ .main-content { @@ -317,6 +203,10 @@ button:disabled { align-items: flex-start; flex: 1; overflow: hidden; + max-width: 1400px; + width: 100%; + margin-left: auto; + margin-right: auto; } /* ── Sidebar ────────────────────────────────────────────────────── */ @@ -505,11 +395,12 @@ button:disabled { background-color: var(--grey); } -.status-dot.active { background-color: var(--green); } -.status-dot.inactive { background-color: var(--red); } -.status-dot.loading { background-color: var(--yellow); animation: pulse-badge 1s infinite; } -.status-dot.failed { background-color: var(--red); } -.status-dot.disabled { background-color: var(--grey); } +.status-dot.active { background-color: var(--green); } +.status-dot.inactive { background-color: var(--red); } +.status-dot.loading { background-color: var(--yellow); animation: pulse-badge 1s infinite; } +.status-dot.failed { background-color: var(--red); } +.status-dot.disabled { background-color: var(--grey); } +.status-dot.needs-attention { background-color: var(--yellow); } /* ── Update modal ─────────────────────────────────���─────────────── */ diff --git a/app/sovran_systemsos_web/templates/index.html b/app/sovran_systemsos_web/templates/index.html index c3fca03..69fded9 100644 --- a/app/sovran_systemsos_web/templates/index.html +++ b/app/sovran_systemsos_web/templates/index.html @@ -35,9 +35,6 @@
- -
-