Add NOC alert panel V3.5

This commit is contained in:
2026-04-25 18:33:21 -05:00
parent e11463a0e4
commit cee5f45252
3 changed files with 154 additions and 7 deletions
+13 -1
View File
@@ -91,7 +91,7 @@
Built using real infrastructure: Proxmox • K3s • TrueNAS • VLAN segmentation • APRS RF Edge Systems Built using real infrastructure: Proxmox • K3s • TrueNAS • VLAN segmentation • APRS RF Edge Systems
</p> </p>
<!-- ✅ FIXED METRICS --> <!-- METRICS -->
<div class="noc-metrics-grid"> <div class="noc-metrics-grid">
<div class="noc-metric"> <div class="noc-metric">
<h3 id="metric-nodes">--</h3> <h3 id="metric-nodes">--</h3>
@@ -114,6 +114,18 @@
</div> </div>
</div> </div>
<!-- 🚨 ALERT PANEL -->
<div class="noc-alert-panel">
<div class="noc-alert-header">
<h3>🚨 NOC Alert Panel</h3>
<span>Top 3 System Notes</span>
</div>
<div id="noc-alert-list" class="noc-alert-list">
<p>Loading alerts...</p>
</div>
</div>
<!-- SYSTEM GRID --> <!-- SYSTEM GRID -->
<div id="noc-system-grid" class="noc-system-grid"></div> <div id="noc-system-grid" class="noc-system-grid"></div>
+64 -6
View File
@@ -5,8 +5,6 @@ document.addEventListener("DOMContentLoaded", () => {
} }
loadNocStatus(); loadNocStatus();
// Auto-refresh NOC dashboard every 30 seconds
setInterval(loadNocStatus, 30000); setInterval(loadNocStatus, 30000);
}); });
@@ -26,6 +24,7 @@ async function loadNocStatus() {
const vlans = document.getElementById("metric-vlans"); const vlans = document.getElementById("metric-vlans");
const edge = document.getElementById("metric-edge"); const edge = document.getElementById("metric-edge");
const grid = document.getElementById("noc-system-grid"); const grid = document.getElementById("noc-system-grid");
const alertList = document.getElementById("noc-alert-list");
if (!statusText || !nodes || !services || !vlans || !edge || !grid) { if (!statusText || !nodes || !services || !vlans || !edge || !grid) {
return; return;
@@ -33,10 +32,10 @@ async function loadNocStatus() {
statusText.textContent = `${data.nocName}${data.status} — Last updated: ${data.lastUpdated}`; statusText.textContent = `${data.nocName}${data.status} — Last updated: ${data.lastUpdated}`;
nodes.textContent = data.summary.nodes; animateMetric(nodes, data.summary.nodes);
services.textContent = data.summary.services; animateMetric(services, data.summary.services);
vlans.textContent = data.summary.vlans; animateMetric(vlans, data.summary.vlans);
edge.textContent = data.summary.edgeSystems; animateMetric(edge, data.summary.edgeSystems);
grid.innerHTML = ""; grid.innerHTML = "";
@@ -78,6 +77,10 @@ async function loadNocStatus() {
grid.appendChild(card); grid.appendChild(card);
}); });
if (alertList) {
renderAlerts(data.systems, alertList);
}
} catch (error) { } catch (error) {
const statusText = document.getElementById("noc-status-text"); const statusText = document.getElementById("noc-status-text");
@@ -88,3 +91,58 @@ async function loadNocStatus() {
console.error("Failed to load NOC status:", error); console.error("Failed to load NOC status:", error);
} }
} }
function animateMetric(element, value) {
element.classList.remove("metric-pop");
element.textContent = value;
void element.offsetWidth;
element.classList.add("metric-pop");
}
function renderAlerts(systems, alertList) {
const priority = systems
.map((system) => {
const severity = (system.severity || "Unknown").toLowerCase();
const status = (system.status || "").toLowerCase();
let score = 0;
if (severity.includes("critical") || status.includes("offline")) {
score = 3;
} else if (severity.includes("degraded")) {
score = 2;
} else if (severity.includes("unknown")) {
score = 1;
}
return { ...system, score };
})
.filter((system) => system.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 3);
if (priority.length === 0) {
alertList.innerHTML = `
<div class="noc-alert healthy">
<strong>All monitored systems healthy</strong>
<span>No critical or degraded systems detected.</span>
</div>
`;
return;
}
alertList.innerHTML = priority
.map((system) => {
const alertClass =
system.score === 3 ? "critical" : system.score === 2 ? "degraded" : "unknown";
return `
<div class="noc-alert ${alertClass}">
<strong>${system.name}</strong>
<span>${system.severity || system.status}${system.layer}${system.latency || "N/A"}</span>
</div>
`;
})
.join("");
}
+77
View File
@@ -648,3 +648,80 @@ a:hover {
background: rgba(255, 95, 86, 0.1); background: rgba(255, 95, 86, 0.1);
border: 1px solid rgba(255, 95, 86, 0.28); border: 1px solid rgba(255, 95, 86, 0.28);
} }
.noc-alert-panel {
margin: 1.5rem 0;
padding: 1.25rem;
border-radius: var(--radius);
background: rgba(0, 0, 0, 0.22);
border: 1px solid var(--line);
box-shadow: var(--shadow);
}
.noc-alert-header {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
margin-bottom: 1rem;
}
.noc-alert-header h3 {
margin: 0;
}
.noc-alert-header span {
color: var(--muted);
font-size: 0.9rem;
}
.noc-alert-list {
display: grid;
gap: 0.75rem;
}
.noc-alert {
display: grid;
gap: 0.2rem;
padding: 0.9rem 1rem;
border-radius: 14px;
border-left: 4px solid var(--accent-2);
background: rgba(255, 255, 255, 0.035);
}
.noc-alert span {
color: var(--muted);
font-size: 0.9rem;
}
.noc-alert.healthy {
border-left-color: var(--accent-2);
}
.noc-alert.degraded {
border-left-color: var(--warning);
}
.noc-alert.critical {
border-left-color: #ff5f56;
}
.noc-alert.unknown {
border-left-color: var(--accent);
}
.metric-pop {
animation: metricPop 0.35s ease;
}
@keyframes metricPop {
0% {
transform: scale(0.96);
opacity: 0.7;
}
100% {
transform: scale(1);
opacity: 1;
}
}