mirror of
https://github.com/ced4568/ced-portfolio.git
synced 2026-08-12 22:34:03 +00:00
Add NOC alert panel V3.5
This commit is contained in:
+13
-1
@@ -91,7 +91,7 @@
|
||||
Built using real infrastructure: Proxmox • K3s • TrueNAS • VLAN segmentation • APRS RF Edge Systems
|
||||
</p>
|
||||
|
||||
<!-- ✅ FIXED METRICS -->
|
||||
<!-- METRICS -->
|
||||
<div class="noc-metrics-grid">
|
||||
<div class="noc-metric">
|
||||
<h3 id="metric-nodes">--</h3>
|
||||
@@ -114,6 +114,18 @@
|
||||
</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 -->
|
||||
<div id="noc-system-grid" class="noc-system-grid"></div>
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
|
||||
loadNocStatus();
|
||||
|
||||
// Auto-refresh NOC dashboard every 30 seconds
|
||||
setInterval(loadNocStatus, 30000);
|
||||
});
|
||||
|
||||
@@ -26,6 +24,7 @@ async function loadNocStatus() {
|
||||
const vlans = document.getElementById("metric-vlans");
|
||||
const edge = document.getElementById("metric-edge");
|
||||
const grid = document.getElementById("noc-system-grid");
|
||||
const alertList = document.getElementById("noc-alert-list");
|
||||
|
||||
if (!statusText || !nodes || !services || !vlans || !edge || !grid) {
|
||||
return;
|
||||
@@ -33,10 +32,10 @@ async function loadNocStatus() {
|
||||
|
||||
statusText.textContent = `${data.nocName} — ${data.status} — Last updated: ${data.lastUpdated}`;
|
||||
|
||||
nodes.textContent = data.summary.nodes;
|
||||
services.textContent = data.summary.services;
|
||||
vlans.textContent = data.summary.vlans;
|
||||
edge.textContent = data.summary.edgeSystems;
|
||||
animateMetric(nodes, data.summary.nodes);
|
||||
animateMetric(services, data.summary.services);
|
||||
animateMetric(vlans, data.summary.vlans);
|
||||
animateMetric(edge, data.summary.edgeSystems);
|
||||
|
||||
grid.innerHTML = "";
|
||||
|
||||
@@ -78,6 +77,10 @@ async function loadNocStatus() {
|
||||
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
if (alertList) {
|
||||
renderAlerts(data.systems, alertList);
|
||||
}
|
||||
} catch (error) {
|
||||
const statusText = document.getElementById("noc-status-text");
|
||||
|
||||
@@ -87,4 +90,59 @@ async function loadNocStatus() {
|
||||
|
||||
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("");
|
||||
}
|
||||
@@ -647,4 +647,81 @@ a:hover {
|
||||
color: #ff5f56;
|
||||
background: rgba(255, 95, 86, 0.1);
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user