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>
This commit is contained in:
committed by
GitHub
parent
cf176ea2db
commit
7361047b48
@@ -1198,6 +1198,12 @@ async def api_services():
|
|||||||
# Read runtime feature overrides from custom.nix Hub Managed section
|
# Read runtime feature overrides from custom.nix Hub Managed section
|
||||||
overrides, _ = await loop.run_in_executor(None, _read_hub_overrides)
|
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):
|
async def get_status(entry):
|
||||||
unit = entry.get("unit", "")
|
unit = entry.get("unit", "")
|
||||||
scope = entry.get("type", "system")
|
scope = entry.get("type", "system")
|
||||||
@@ -1235,6 +1241,34 @@ async def api_services():
|
|||||||
except OSError:
|
except OSError:
|
||||||
domain = None
|
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 {
|
return {
|
||||||
"name": entry.get("name", ""),
|
"name": entry.get("name", ""),
|
||||||
"unit": unit,
|
"unit": unit,
|
||||||
@@ -1243,6 +1277,7 @@ async def api_services():
|
|||||||
"enabled": enabled,
|
"enabled": enabled,
|
||||||
"category": entry.get("category", "other"),
|
"category": entry.get("category", "other"),
|
||||||
"status": status,
|
"status": status,
|
||||||
|
"health": health,
|
||||||
"has_credentials": has_credentials,
|
"has_credentials": has_credentials,
|
||||||
"port_requirements": port_requirements,
|
"port_requirements": port_requirements,
|
||||||
"needs_domain": needs_domain,
|
"needs_domain": needs_domain,
|
||||||
@@ -1424,11 +1459,31 @@ async def api_service_detail(unit: str):
|
|||||||
"description": p.get("description", ""),
|
"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 {
|
return {
|
||||||
"name": entry.get("name", ""),
|
"name": entry.get("name", ""),
|
||||||
"unit": unit,
|
"unit": unit,
|
||||||
"icon": icon,
|
"icon": icon,
|
||||||
"status": status,
|
"status": status,
|
||||||
|
"health": health,
|
||||||
"enabled": enabled,
|
"enabled": enabled,
|
||||||
"description": SERVICE_DESCRIPTIONS.get(unit, ""),
|
"description": SERVICE_DESCRIPTIONS.get(unit, ""),
|
||||||
"has_credentials": has_credentials and bool(resolved_creds),
|
"has_credentials": has_credentials and bool(resolved_creds),
|
||||||
|
|||||||
@@ -4,12 +4,9 @@
|
|||||||
|
|
||||||
const POLL_INTERVAL_SERVICES = 5000;
|
const POLL_INTERVAL_SERVICES = 5000;
|
||||||
const POLL_INTERVAL_UPDATES = 1800000;
|
const POLL_INTERVAL_UPDATES = 1800000;
|
||||||
const POLL_INTERVAL_PORT_HEALTH = 15000;
|
|
||||||
const UPDATE_POLL_INTERVAL = 2000;
|
const UPDATE_POLL_INTERVAL = 2000;
|
||||||
const REBOOT_CHECK_INTERVAL = 5000;
|
const REBOOT_CHECK_INTERVAL = 5000;
|
||||||
const SUPPORT_TIMER_INTERVAL = 1000;
|
const SUPPORT_TIMER_INTERVAL = 1000;
|
||||||
const BANNER_AUTO_FADE_DELAY = 5000;
|
|
||||||
const BANNER_FADE_TRANSITION_MS = 550;
|
|
||||||
|
|
||||||
const CATEGORY_ORDER = [
|
const CATEGORY_ORDER = [
|
||||||
"infrastructure",
|
"infrastructure",
|
||||||
@@ -124,26 +121,34 @@ const $portReqBody = document.getElementById("port-req-body");
|
|||||||
const $portReqClose = document.getElementById("port-req-close-btn");
|
const $portReqClose = document.getElementById("port-req-close-btn");
|
||||||
|
|
||||||
// System status banner
|
// System status banner
|
||||||
const $statusBanner = document.getElementById("system-status-banner");
|
// (removed — health is now shown per-tile via the composite health field)
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
function tileId(svc) { return svc.unit + "::" + svc.name; }
|
function tileId(svc) { return svc.unit + "::" + svc.name; }
|
||||||
|
|
||||||
function statusClass(status) {
|
function statusClass(health) {
|
||||||
if (!status) return "unknown";
|
if (!health) return "unknown";
|
||||||
if (status === "active") return "active";
|
if (health === "healthy") return "active";
|
||||||
if (status === "inactive") return "inactive";
|
if (health === "needs_attention") return "needs-attention";
|
||||||
if (status === "failed") return "failed";
|
if (health === "active") return "active"; // backwards compat
|
||||||
if (status === "disabled") return "disabled";
|
if (health === "inactive") return "inactive";
|
||||||
if (STATUS_LOADING_STATES.has(status)) return "loading";
|
if (health === "failed") return "failed";
|
||||||
|
if (health === "disabled") return "disabled";
|
||||||
|
if (STATUS_LOADING_STATES.has(health)) return "loading";
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusText(status, enabled) {
|
function statusText(health, enabled) {
|
||||||
if (!enabled) return "disabled";
|
if (!enabled) return "Disabled";
|
||||||
if (!status || status === "unknown") return "unknown";
|
if (health === "healthy") return "Active";
|
||||||
return status;
|
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) {
|
function escHtml(str) {
|
||||||
@@ -242,8 +247,8 @@ function renderSidebarSupport(supportServices) {
|
|||||||
|
|
||||||
function buildTile(svc) {
|
function buildTile(svc) {
|
||||||
var isSupport = svc.type === "support";
|
var isSupport = svc.type === "support";
|
||||||
var sc = statusClass(svc.status);
|
var sc = statusClass(svc.health || svc.status);
|
||||||
var st = statusText(svc.status, svc.enabled);
|
var st = statusText(svc.health || svc.status, svc.enabled);
|
||||||
var dis = !svc.enabled;
|
var dis = !svc.enabled;
|
||||||
|
|
||||||
var tile = document.createElement("div");
|
var tile = document.createElement("div");
|
||||||
@@ -279,8 +284,8 @@ function updateTiles(services) {
|
|||||||
var id = CSS.escape(tileId(svc));
|
var id = CSS.escape(tileId(svc));
|
||||||
var tile = $tilesArea.querySelector('.service-tile[data-tile-id="' + id + '"]');
|
var tile = $tilesArea.querySelector('.service-tile[data-tile-id="' + id + '"]');
|
||||||
if (!tile) continue;
|
if (!tile) continue;
|
||||||
var sc = statusClass(svc.status);
|
var sc = statusClass(svc.health || svc.status);
|
||||||
var st = statusText(svc.status, svc.enabled);
|
var st = statusText(svc.health || svc.status, svc.enabled);
|
||||||
var dot = tile.querySelector(".status-dot");
|
var dot = tile.querySelector(".status-dot");
|
||||||
var text = tile.querySelector(".status-text");
|
var text = tile.querySelector(".status-text");
|
||||||
if (dot) dot.className = "status-dot " + sc;
|
if (dot) dot.className = "status-dot " + sc;
|
||||||
@@ -396,8 +401,8 @@ async function openServiceDetailModal(unit, name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Section B: Status
|
// Section B: Status
|
||||||
var sc = statusClass(data.status);
|
var sc = statusClass(data.health || data.status);
|
||||||
var st = statusText(data.status, data.enabled);
|
var st = statusText(data.health || data.status, data.enabled);
|
||||||
html += '<div class="svc-detail-section">' +
|
html += '<div class="svc-detail-section">' +
|
||||||
'<div class="svc-detail-section-title">Status</div>' +
|
'<div class="svc-detail-section-title">Status</div>' +
|
||||||
'<div class="svc-detail-status">' +
|
'<div class="svc-detail-status">' +
|
||||||
@@ -425,26 +430,69 @@ async function openServiceDetailModal(unit, name) {
|
|||||||
statusIcon = "— Unknown";
|
statusIcon = "— Unknown";
|
||||||
statusClass2 = "port-status-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 += '<tr>' +
|
portTableRows += '<tr>' +
|
||||||
'<td class="svc-detail-port-table-port">' + escHtml(p.port) + '</td>' +
|
'<td class="svc-detail-port-table-port">' + escHtml(p.port) + '</td>' +
|
||||||
'<td class="svc-detail-port-table-proto">' + escHtml(p.protocol) + '</td>' +
|
'<td class="svc-detail-port-table-proto">' + escHtml(p.protocol) + '</td>' +
|
||||||
'<td class="svc-detail-port-table-desc">' + escHtml(p.description) + '</td>' +
|
'<td class="svc-detail-port-table-desc">' + escHtml(desc) + '</td>' +
|
||||||
'<td class="svc-detail-port-table-status ' + statusClass2 + '">' + statusIcon + '</td>' +
|
'<td class="svc-detail-port-table-status ' + statusClass2 + '">' + statusIcon + '</td>' +
|
||||||
'</tr>';
|
'</tr>';
|
||||||
});
|
});
|
||||||
|
|
||||||
var troubleshootHtml = "";
|
var troubleshootHtml = "";
|
||||||
if (anyPortClosed) {
|
if (anyPortClosed) {
|
||||||
troubleshootHtml = '<div class="svc-detail-troubleshoot">' +
|
var sharedPorts = [];
|
||||||
'<strong>⚠️ Some ports are not open yet. Here\'s how to fix it:</strong>' +
|
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(
|
||||||
|
'<strong>⚠️ Ports 80 and 443 need to be forwarded on your router.</strong>' +
|
||||||
|
'<p style="margin-top:8px">These are <strong>shared system ports</strong> — you only need to set them up once and they cover all your domain-based services ' +
|
||||||
|
'(BTCPayServer, Nextcloud, Matrix, WordPress, etc.).</p>' +
|
||||||
|
'<p style="margin-top:8px">If you already forwarded these ports during onboarding, you don\'t need to do it again. Otherwise:</p>' +
|
||||||
'<ol>' +
|
'<ol>' +
|
||||||
'<li>Log into your router\'s admin panel (usually <a href="http://192.168.1.1" target="_blank">http://192.168.1.1</a>)</li>' +
|
'<li>Log into your router\'s admin panel (usually <code>http://192.168.1.1</code>)</li>' +
|
||||||
'<li>Find the <strong>Port Forwarding</strong> section</li>' +
|
'<li>Find the <strong>Port Forwarding</strong> section</li>' +
|
||||||
'<li>Forward each closed port below to this machine\'s internal IP: <code>' + escHtml(data.internal_ip || "—") + '</code></li>' +
|
'<li>Forward port <strong>80 (TCP)</strong> and port <strong>443 (TCP)</strong> to your machine\'s internal IP: <code>' + escHtml(data.internal_ip || "—") + '</code></li>' +
|
||||||
'<li>Save your router settings</li>' +
|
'<li>Save your router settings</li>' +
|
||||||
'</ol>' +
|
'</ol>' +
|
||||||
'<p style="margin-top:10px">💡 Search <em>"how to set up port forwarding on [your router model]"</em> for step-by-step instructions.</p>' +
|
'<p style="margin-top:8px">💡 Once these two ports are forwarded, you won\'t see this warning on any service again.</p>'
|
||||||
'</div>';
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (specificPorts.length > 0) {
|
||||||
|
var portList = specificPorts.map(function(p) {
|
||||||
|
return '<strong>' + escHtml(p.port) + ' (' + escHtml(p.protocol) + ')</strong> — ' + escHtml(p.description);
|
||||||
|
}).join('<br>');
|
||||||
|
|
||||||
|
troubleParts.push(
|
||||||
|
'<strong>⚠️ This service requires additional ports to be forwarded:</strong>' +
|
||||||
|
'<p style="margin-top:8px">' + portList + '</p>' +
|
||||||
|
'<ol>' +
|
||||||
|
'<li>Log into your router\'s admin panel</li>' +
|
||||||
|
'<li>Forward each port listed above to your machine\'s internal IP: <code>' + escHtml(data.internal_ip || "—") + '</code></li>' +
|
||||||
|
'<li>Save your router settings</li>' +
|
||||||
|
'</ol>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
troubleshootHtml = '<div class="svc-detail-troubleshoot">' + troubleParts.join('<hr style="border:none;border-top:1px solid rgba(255,255,255,0.1);margin:16px 0">') + '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '<div class="svc-detail-section">' +
|
html += '<div class="svc-detail-section">' +
|
||||||
@@ -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 ($credsModal) $credsModal.addEventListener("click", function(e) { if (e.target === $credsModal) closeCredsModal(); });
|
||||||
if ($supportModal) $supportModal.addEventListener("click", function(e) { if (e.target === $supportModal) closeSupportModal(); });
|
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 '🔴 <span class="status-banner-port">' + escHtml(p.port) + '/' + escHtml(p.protocol) + '</span>'
|
|
||||||
+ (p.description ? ' <span style="opacity:0.7">— ' + escHtml(p.description) + '</span>' : '');
|
|
||||||
}).join(", ");
|
|
||||||
return '<tr><td>' + escHtml(svc.name) + '</td><td>' + portList + '</td></tr>';
|
|
||||||
}).join("");
|
|
||||||
return '<table class="status-banner-table">'
|
|
||||||
+ '<thead><tr><th>Service</th><th>Closed Ports</th></tr></thead>'
|
|
||||||
+ '<tbody>' + rows + '</tbody>'
|
|
||||||
+ '</table>';
|
|
||||||
}
|
|
||||||
|
|
||||||
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 += ' <button class="status-banner-toggle" id="' + toggleId + '">'
|
|
||||||
+ (_bannerDetailsOpen ? "Hide Details ▲" : "View Details ▼")
|
|
||||||
+ '</button>'
|
|
||||||
+ '<div class="status-banner-details" id="' + detailsId + '" style="display:'
|
|
||||||
+ (_bannerDetailsOpen ? "block" : "none") + '">'
|
|
||||||
+ detailsHtml
|
|
||||||
+ '</div>';
|
|
||||||
|
|
||||||
$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 ──────────────────────────────────────────────────────────
|
// ── Init ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
@@ -1887,11 +1825,9 @@ async function init() {
|
|||||||
await refreshServices();
|
await refreshServices();
|
||||||
loadNetwork();
|
loadNetwork();
|
||||||
checkUpdates();
|
checkUpdates();
|
||||||
loadPortHealth();
|
|
||||||
|
|
||||||
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
|
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
|
||||||
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
|
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
|
||||||
setInterval(loadPortHealth, POLL_INTERVAL_PORT_HEALTH);
|
|
||||||
|
|
||||||
if (cfg.feature_manager) {
|
if (cfg.feature_manager) {
|
||||||
loadFeatureManager();
|
loadFeatureManager();
|
||||||
@@ -1900,10 +1836,8 @@ async function init() {
|
|||||||
await refreshServices();
|
await refreshServices();
|
||||||
loadNetwork();
|
loadNetwork();
|
||||||
checkUpdates();
|
checkUpdates();
|
||||||
loadPortHealth();
|
|
||||||
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
|
setInterval(refreshServices, POLL_INTERVAL_SERVICES);
|
||||||
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
|
setInterval(checkUpdates, POLL_INTERVAL_UPDATES);
|
||||||
setInterval(loadPortHealth, POLL_INTERVAL_PORT_HEALTH);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,10 +64,10 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-logo {
|
.header-logo {
|
||||||
height: 30px;
|
height: 46px;
|
||||||
width: auto;
|
width: auto;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 8px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.role-badge {
|
.role-badge {
|
||||||
@@ -196,120 +196,6 @@ button:disabled {
|
|||||||
color: var(--border-color);
|
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 ───────────────────────────────────────────────── */
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
@@ -317,6 +203,10 @@ button:disabled {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
max-width: 1400px;
|
||||||
|
width: 100%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Sidebar ────────────────────────────────────────────────────── */
|
/* ── Sidebar ────────────────────────────────────────────────────── */
|
||||||
@@ -510,6 +400,7 @@ button:disabled {
|
|||||||
.status-dot.loading { background-color: var(--yellow); animation: pulse-badge 1s infinite; }
|
.status-dot.loading { background-color: var(--yellow); animation: pulse-badge 1s infinite; }
|
||||||
.status-dot.failed { background-color: var(--red); }
|
.status-dot.failed { background-color: var(--red); }
|
||||||
.status-dot.disabled { background-color: var(--grey); }
|
.status-dot.disabled { background-color: var(--grey); }
|
||||||
|
.status-dot.needs-attention { background-color: var(--yellow); }
|
||||||
|
|
||||||
/* ── Update modal ─────────────────────────────────<E29480><E29480><EFBFBD>─────────────── */
|
/* ── Update modal ─────────────────────────────────<E29480><E29480><EFBFBD>─────────────── */
|
||||||
|
|
||||||
|
|||||||
@@ -35,9 +35,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- System status banner -->
|
|
||||||
<div id="system-status-banner" class="status-banner" style="display:none"></div>
|
|
||||||
|
|
||||||
<!-- Service tiles -->
|
<!-- Service tiles -->
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<aside class="sidebar" id="sidebar">
|
<aside class="sidebar" id="sidebar">
|
||||||
|
|||||||
Reference in New Issue
Block a user