added multi node functionality

This commit is contained in:
Jake Kasper
2026-04-24 12:33:52 -04:00
parent c4c081362e
commit 16e5f2ced2
30 changed files with 673 additions and 93 deletions

View File

@@ -59,6 +59,22 @@ header h1 span { color: var(--muted); font-weight: 400; }
}
.main::-webkit-scrollbar { width: 5px; }
.main::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.content-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 420px;
gap: 24px;
align-items: start;
}
.tasks-col {
display: flex;
flex-direction: column;
gap: 24px;
min-width: 0;
}
.log-col {
position: sticky;
top: 0;
}
/* ── Section headers ─────────────────────────────────────────────── */
.section-title {
@@ -171,6 +187,15 @@ header h1 span { color: var(--muted); font-weight: 400; }
}
.modal-confirm.danger { background: var(--red); }
.modal-confirm.warning { background: var(--yellow); color: #000; }
@media (max-width: 1100px) {
.content-grid {
grid-template-columns: 1fr;
}
.log-col {
position: static;
}
}
</style>
</head>
<body>
@@ -183,42 +208,46 @@ header h1 span { color: var(--muted); font-weight: 400; }
</header>
<div class="main">
<!-- Diagnostics -->
<div>
<div class="section-title">Diagnostics &amp; Health</div>
<div class="action-grid" id="diagGrid"></div>
</div>
<!-- Operations -->
<div>
<div class="section-title">Network Operations</div>
<div class="action-grid" id="opsGrid"></div>
</div>
<!-- Maintenance -->
<div>
<div class="section-title">Maintenance</div>
<div class="action-grid" id="maintGrid"></div>
</div>
<!-- Run log -->
<div class="log-panel">
<div class="log-header">
<div class="log-title">
▸ Run Log
<span class="log-badge" id="logCount">0 entries</span>
<div class="content-grid">
<div class="tasks-col">
<!-- Diagnostics -->
<div>
<div class="section-title">Diagnostics &amp; Health</div>
<div class="action-grid" id="diagGrid"></div>
</div>
<div class="log-header-actions">
<button class="expand-btn" id="expandBtn" onclick="toggleExpand()">⤢ Expand</button>
<button class="clear-btn" onclick="clearLog()">Clear</button>
<!-- Operations -->
<div>
<div class="section-title">Network Operations</div>
<div class="action-grid" id="opsGrid"></div>
</div>
<!-- Maintenance -->
<div>
<div class="section-title">Maintenance</div>
<div class="action-grid" id="maintGrid"></div>
</div>
</div>
<div class="log-body" id="logBody">
<div class="log-empty" id="logEmpty">No actions run yet.</div>
<div class="log-col">
<!-- Run log -->
<div class="log-panel" id="logPanel">
<div class="log-header">
<div class="log-title">
▸ Run Log
<span class="log-badge" id="logCount">0 entries</span>
</div>
<div class="log-header-actions">
<button class="expand-btn" id="expandBtn" onclick="toggleExpand()">⤢ Expand</button>
<button class="clear-btn" onclick="clearLog()">Clear</button>
</div>
</div>
<div class="log-body" id="logBody">
<div class="log-empty" id="logEmpty">No actions run yet.</div>
</div>
</div>
</div>
</div>
</div>
<!-- Confirm modal -->
@@ -255,6 +284,17 @@ const ACTIONS = {
],
};
function nfNodeLabel(nf) {
const nodes = nf?.nodes || [];
return nodes.length ? nodes.map(n => n.hostname).join(', ') : 'unresolved node';
}
async function fetchNetworkStatus() {
const r = await fetch('/api/network/status');
if (!r.ok) throw new Error('HTTP ' + r.status);
return await r.json();
}
// ── Render cards ──────────────────────────────────────────────────────────
function renderGrid(gridId, items) {
const g = document.getElementById(gridId);
@@ -289,6 +329,7 @@ function handleAction(id) {
const all = [...ACTIONS.diag, ...ACTIONS.ops, ...ACTIONS.maint];
const a = all.find(x => x.id === id);
if (!a) return;
revealLogPanel(true);
if (a.safe) { a.run(); return; }
pendingAction = a;
document.getElementById('modalTitle').textContent = a.name;
@@ -306,6 +347,7 @@ function closeModal() {
function runConfirmed() {
closeModal();
revealLogPanel(true);
if (pendingAction) { pendingAction.run(); pendingAction = null; }
}
@@ -326,6 +368,17 @@ function addLog(msg, type='info') {
renderLog();
}
function revealLogPanel(forceExpand=false) {
const panel = document.getElementById('logPanel');
const el = document.getElementById('logBody');
const btn = document.getElementById('expandBtn');
if (forceExpand && !el.classList.contains('expanded')) {
el.classList.add('expanded');
btn.textContent = '⤡ Collapse';
}
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function renderLog() {
const el = document.getElementById('logBody');
document.getElementById('logEmpty').style.display = logLines.length ? 'none' : '';
@@ -368,17 +421,16 @@ document.addEventListener('DOMContentLoaded', () => {
// ── Action implementations ─────────────────────────────────────────────────
async function pingNFs() {
addLog('▸ Pinging all NFs via Prometheus endpoint…', 'run');
addLog('▸ Checking all discovered NFs across cluster nodes…', 'run');
try {
const r = await fetch('/api/network/nf-status');
const d = await r.json();
const nfs = d.nf_status || [];
const d = await fetchNetworkStatus();
const nfs = d.nfs || [];
const up = nfs.filter(n => n.state === 'up').length;
const down = nfs.filter(n => n.state === 'down').length;
nfs.forEach(n => addLog(` ${n.name}: ${n.state.toUpperCase()}`, n.state === 'up' ? 'ok' : 'err'));
addLog(`Ping complete — ${up} up, ${down} down`, down > 0 ? 'warn' : 'ok');
nfs.forEach(n => addLog(` ${n.name}: ${n.state.toUpperCase()} on ${nfNodeLabel(n)}`, n.state === 'up' ? 'ok' : n.state === 'down' ? 'err' : 'warn'));
addLog(`Cluster check complete — ${up} up, ${down} down`, down > 0 ? 'warn' : 'ok');
} catch(e) {
addLog('✗ Failed to reach Prometheus: ' + e.message, 'err');
addLog('✗ Failed to reach network status API: ' + e.message, 'err');
}
}
@@ -392,7 +444,7 @@ async function refreshAlerts() {
addLog('✓ No active alerts — network is healthy', 'ok');
} else {
addLog(`${alerts.length} active alert(s):`, 'warn');
alerts.forEach(a => addLog(` [${(a.labels?.severity||'info').toUpperCase()}] ${a.labels?.alertname||'Unknown'}`, 'warn'));
alerts.forEach(a => addLog(` [${(a.severity||'info').toUpperCase()}] ${a.name} on ${(a.nodes||[]).map(n => n.hostname).join(', ') || 'unresolved node'}`, 'warn'));
}
} catch(e) {
addLog('✗ Failed to reach Alertmanager: ' + e.message, 'err');
@@ -400,15 +452,18 @@ async function refreshAlerts() {
}
async function nfReport() {
addLog('▸ Generating full NF status report…', 'run');
addLog('▸ Generating cluster-wide NF status report…', 'run');
try {
const r = await fetch('/api/network/nf-status');
const d = await r.json();
const nfs = d.nf_status || [];
const d = await fetchNetworkStatus();
const nfs = d.nfs || [];
const up = nfs.filter(n => n.state === 'up').length;
addLog(`✓ Report: ${up}/${nfs.length} NFs operational`, up === nfs.length ? 'ok' : 'warn');
(d.cluster?.nodes || []).forEach(node => {
const running = (node.nfs || []).filter(nf => nf.state === 'up').map(nf => nf.name);
addLog(` ${node.hostname} (${node.role}): ${running.join(', ') || 'no active NFs'}`, 'info');
});
addLog(` Timestamp: ${new Date().toISOString()}`, 'info');
addLog(` Source: Prometheus metrics`, 'info');
addLog(` Source: PLS cluster discovery + Prometheus`, 'info');
} catch(e) {
addLog('✗ Report generation failed: ' + e.message, 'err');
}
@@ -453,16 +508,15 @@ async function emulatedSession() {
}
async function checkDevices() {
addLog('▸ Fetching connected device list…', 'run');
addLog('▸ Checking cluster nodes for subscriber-serving NFs…', 'run');
try {
const r = await fetch('/api/network/nf-status');
const d = await r.json();
const nfs = d.nf_status || [];
const d = await fetchNetworkStatus();
const nfs = d.nfs || [];
const amf = nfs.find(n => n.name === 'AMF');
addLog(` AMF state: ${amf ? amf.state.toUpperCase() : 'UNKNOWN'}`, amf?.state === 'up' ? 'ok' : 'warn');
addLog(` AMF state: ${amf ? amf.state.toUpperCase() : 'UNKNOWN'} on ${nfNodeLabel(amf)}`, amf?.state === 'up' ? 'ok' : 'warn');
const upf = nfs.find(n => n.name === 'UPF');
addLog(` UPF state: ${upf ? upf.state.toUpperCase() : 'UNKNOWN'}`, upf?.state === 'up' ? 'ok' : 'warn');
addLog('✓ Device registry checked — see Prometheus for per-device detail', 'ok');
addLog(` UPF state: ${upf ? upf.state.toUpperCase() : 'UNKNOWN'} on ${nfNodeLabel(upf)}`, upf?.state === 'up' ? 'ok' : 'warn');
addLog('✓ Cluster subscriber path checked — see Marvis AI for node-scoped health', 'ok');
} catch(e) {
addLog('✗ Could not reach network status endpoint: ' + e.message, 'err');
}
@@ -486,10 +540,12 @@ function clearSessions() {
}
function backupConfig() {
addLog('▸ Exporting configuration for all NFs…', 'run');
const nfs = ['AMF','SMF','UPF','NRF','AUSF','UDM','UDR','PCF','CHF','SMSF','AAA','MME'];
nfs.forEach((nf, i) => setTimeout(() => addLog(` ${nf}: config exported`, 'ok'), 300 + i*120));
setTimeout(() => addLog(`✓ Backup archive: p5g-config-${new Date().toISOString().slice(0,10)}.tar.gz`, 'ok'), 300 + nfs.length*120 + 200);
addLog('▸ Exporting configuration plan for all discovered nodes…', 'run');
fetchNetworkStatus().then(d => {
const nodes = d.cluster?.nodes || [];
nodes.forEach((node, i) => setTimeout(() => addLog(` ${node.hostname}: profile ${node.role}, services ${node.started_services.join(', ') || 'none'}`, 'ok'), 300 + i*160));
setTimeout(() => addLog(`✓ Backup archive plan ready: p5g-config-${new Date().toISOString().slice(0,10)}.tar.gz`, 'ok'), 300 + nodes.length*160 + 200);
}).catch(e => addLog('✗ Could not inspect cluster before backup: ' + e.message, 'err'));
}
function reloadConfig() {