added multi node functionality
This commit is contained in:
@@ -155,6 +155,12 @@ body {
|
||||
background: rgba(255,255,255,0.07); color: var(--text);
|
||||
width: fit-content; white-space: nowrap;
|
||||
}
|
||||
.issue-node {
|
||||
font-size: 10px; font-weight: 600; letter-spacing: 0.04em;
|
||||
padding: 2px 7px; border-radius: 5px; margin-top: 5px;
|
||||
background: rgba(59,130,246,0.12); color: var(--blue);
|
||||
width: fit-content; white-space: nowrap;
|
||||
}
|
||||
.issue-body {}
|
||||
.issue-desc { font-size: 13px; font-weight: 500; line-height: 1.4; }
|
||||
.issue-rem { font-size: 11px; color: var(--muted); margin-top: 3px; line-height: 1.4; }
|
||||
@@ -469,6 +475,7 @@ function renderDetail(cat) {
|
||||
<div class="issue-nf">${esc(iss.nf)}</div>
|
||||
<div class="issue-body">
|
||||
<div class="issue-desc">${esc(iss.description)}</div>
|
||||
${iss.node ? `<div class="issue-node">${esc(iss.node)}</div>` : ''}
|
||||
<div class="issue-rem">⤷ ${esc(iss.remediation||'')}</div>
|
||||
<span class="issue-source">${esc(iss.source||'log')}</span>
|
||||
</div>
|
||||
|
||||
@@ -60,8 +60,10 @@ header h1 span { color: var(--muted); font-weight: 400; }
|
||||
/* ── Left panel ─────────────────────────────────────────────────── */
|
||||
.left {
|
||||
background: var(--surface); border-right: 1px solid var(--border);
|
||||
display: flex; flex-direction: column; overflow: hidden;
|
||||
display: flex; flex-direction: column; overflow-y: auto; overflow-x: hidden;
|
||||
}
|
||||
.left::-webkit-scrollbar { width: 5px; }
|
||||
.left::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
||||
.section { padding: 14px 16px; border-bottom: 1px solid var(--border); }
|
||||
.section-title {
|
||||
font-size: 10px; font-weight: 700; text-transform: uppercase;
|
||||
@@ -88,6 +90,79 @@ header h1 span { color: var(--muted); font-weight: 400; }
|
||||
.nf-card.up .nf-state { color: var(--green); }
|
||||
.nf-card.down .nf-state { color: var(--red); }
|
||||
|
||||
/* Cluster nodes */
|
||||
.node-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.node-card {
|
||||
background: var(--card); border: 1px solid var(--border); border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.node-summary {
|
||||
display: flex; align-items: center; gap: 8px; padding: 10px 12px; cursor: pointer;
|
||||
}
|
||||
.node-summary:hover { background: rgba(255,255,255,.02); }
|
||||
.node-top { display: flex; align-items: center; gap: 8px; width: 100%; }
|
||||
.node-name { font-size: 13px; font-weight: 700; }
|
||||
.node-addr { font-size: 11px; color: var(--muted); margin-top: 3px; }
|
||||
.node-caret {
|
||||
margin-left: 8px; font-size: 11px; color: var(--muted); transition: transform .15s;
|
||||
}
|
||||
.node-card.open .node-caret { transform: rotate(180deg); }
|
||||
.node-role {
|
||||
margin-left: auto; font-size: 10px; font-weight: 700; letter-spacing: .08em;
|
||||
border-radius: 999px; padding: 3px 8px; border: 1px solid var(--border);
|
||||
color: var(--blue); background: rgba(59,130,246,.12);
|
||||
}
|
||||
.node-role.current {
|
||||
color: var(--green); border-color: rgba(16,185,129,.5); background: rgba(16,185,129,.12);
|
||||
}
|
||||
.node-meta {
|
||||
display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px;
|
||||
}
|
||||
.node-chip {
|
||||
font-size: 10px; color: var(--muted); padding: 2px 7px;
|
||||
border-radius: 999px; background: rgba(255,255,255,.04); border: 1px solid var(--border);
|
||||
}
|
||||
.node-services {
|
||||
margin-top: 8px; font-size: 11px; color: var(--text); line-height: 1.4;
|
||||
}
|
||||
.node-services b,
|
||||
.node-profile b {
|
||||
color: var(--muted); font-weight: 600;
|
||||
}
|
||||
.node-profile {
|
||||
margin-top: 6px; font-size: 11px; color: var(--text); line-height: 1.4;
|
||||
}
|
||||
.node-empty {
|
||||
color: var(--muted); font-size: 12px;
|
||||
}
|
||||
.node-details {
|
||||
display: none; padding: 0 12px 12px; border-top: 1px solid rgba(255,255,255,.04);
|
||||
}
|
||||
.node-card.open .node-details { display: block; }
|
||||
.node-nf-grid {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-top: 10px;
|
||||
}
|
||||
.node-nf {
|
||||
background: rgba(255,255,255,.03);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 7px 5px;
|
||||
border-left: 3px solid var(--border);
|
||||
text-align: center;
|
||||
}
|
||||
.node-nf.up { border-left-color: var(--green); }
|
||||
.node-nf.down { border-left-color: var(--red); }
|
||||
.node-nf.unknown { border-left-color: var(--yellow); }
|
||||
.node-nf-name {
|
||||
font-size: 10px; font-weight: 700; color: var(--text); letter-spacing: .04em;
|
||||
}
|
||||
.node-nf-state {
|
||||
margin-top: 3px; font-size: 9px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
|
||||
}
|
||||
.node-nf.up .node-nf-state { color: var(--green); }
|
||||
.node-nf.down .node-nf-state { color: var(--red); }
|
||||
.node-nf.unknown .node-nf-state { color: var(--yellow); }
|
||||
|
||||
/* Alerts panel */
|
||||
.alerts-scroll { flex: 1; overflow-y: auto; padding: 14px 16px; }
|
||||
.alerts-scroll::-webkit-scrollbar { width: 4px; }
|
||||
@@ -101,6 +176,7 @@ header h1 span { color: var(--muted); font-weight: 400; }
|
||||
.alert-row.critical { border-left-color: var(--red); }
|
||||
.alert-row-name { font-size: 12px; font-weight: 600; }
|
||||
.alert-row-desc { font-size: 11px; color: var(--muted); margin-top: 2px; }
|
||||
.alert-row-node { font-size: 10px; color: var(--blue); margin-top: 5px; }
|
||||
|
||||
/* ── Chat panel ─────────────────────────────────────────────────── */
|
||||
.chat { display: flex; flex-direction: column; overflow: hidden; }
|
||||
@@ -185,13 +261,19 @@ header h1 span { color: var(--muted); font-weight: 400; }
|
||||
<div class="left">
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
Network Functions
|
||||
Cluster Overview
|
||||
<button class="refresh-btn" onclick="refresh()" title="Refresh">↻</button>
|
||||
</div>
|
||||
<div class="nf-grid" id="nfGrid">
|
||||
<div class="nf-card"><div class="nf-name">···</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title">Discovered Nodes</div>
|
||||
<div class="node-list" id="nodeList">
|
||||
<div class="node-empty">Loading cluster inventory…</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alerts-scroll">
|
||||
<div class="section-title" style="margin-bottom:10px">Active Alerts</div>
|
||||
<div id="alertsContent"><div style="color:var(--muted);font-size:12px">Loading…</div></div>
|
||||
@@ -221,6 +303,15 @@ header h1 span { color: var(--muted); font-weight: 400; }
|
||||
// ── Utilities ──────────────────────────────────────────────────────────────
|
||||
const $ = id => document.getElementById(id);
|
||||
const ts = () => new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
|
||||
const ROLE_LABELS = {
|
||||
'5GALL': '5G All',
|
||||
'4GALL': '4G All',
|
||||
'4GCP': '4G CP',
|
||||
'4GDCP': '4G DCP',
|
||||
'COMBOALL': 'Combo All',
|
||||
'COMBOCP': 'Combo CP',
|
||||
'COMBODCP': 'Combo DCP',
|
||||
};
|
||||
|
||||
function md(text) {
|
||||
// minimal markdown: **bold**, `code`, newlines
|
||||
@@ -261,15 +352,70 @@ async function loadNFs() {
|
||||
<div class="nf-state">${nf.state==='up'?'● up':nf.state==='down'?'● dn':'○ n/a'}</div>`;
|
||||
grid.appendChild(c);
|
||||
});
|
||||
renderNodes(d.cluster);
|
||||
$('dot').className = 'dot';
|
||||
$('connLabel').textContent = 'Live';
|
||||
} catch {
|
||||
$('dot').className = 'dot err';
|
||||
$('connLabel').textContent = 'Unreachable';
|
||||
$('nfGrid').innerHTML = '<div style="color:var(--muted);font-size:12px;grid-column:1/-1">Cannot reach API</div>';
|
||||
$('nodeList').innerHTML = '<div class="node-empty">Cannot reach cluster discovery API</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleNodeCard(button) {
|
||||
button.closest('.node-card')?.classList.toggle('open');
|
||||
}
|
||||
|
||||
function renderNodes(cluster) {
|
||||
const list = $('nodeList');
|
||||
const nodes = cluster?.nodes || [];
|
||||
if (!nodes.length) {
|
||||
list.innerHTML = '<div class="node-empty">No cluster nodes discovered</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = nodes.map(node => {
|
||||
const role = ROLE_LABELS[node.role] || node.role || 'AP';
|
||||
const repoChips = (node.repositories || []).slice(0, 3).map(repo =>
|
||||
`<span class="node-chip">${repo.type}:${repo.role}</span>`
|
||||
).join('');
|
||||
const running = (node.started_services || []).filter(name => !['alertmanager','prometheus','ncm','pls','fluent-bit','grafana','openvpn','ssh','node-exporter','podman-exporter','licensed','webconsole'].includes(name));
|
||||
const serviceText = running.length ? running.join(', ') : 'No managed NFs started';
|
||||
const expected = (node.expected_nfs || []).join(', ') || 'No NF profile mapped';
|
||||
const nfTiles = (node.nfs || []).map(nf => `
|
||||
<div class="node-nf ${nf.state}">
|
||||
<div class="node-nf-name">${nf.name}</div>
|
||||
<div class="node-nf-state">${nf.state === 'up' ? '● up' : nf.state === 'down' ? '● dn' : '○ n/a'}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
const downCount = (node.nfs || []).filter(nf => nf.state === 'down').length;
|
||||
const openClass = node.current ? 'open' : '';
|
||||
return `
|
||||
<div class="node-card ${openClass}">
|
||||
<div class="node-summary" onclick="toggleNodeCard(this)">
|
||||
<div class="node-top">
|
||||
<div>
|
||||
<div class="node-name">${node.hostname}</div>
|
||||
<div class="node-addr">${node.address} · ${node.nfs.filter(nf => nf.state === 'up').length} up${downCount ? `, ${downCount} down` : ''}</div>
|
||||
</div>
|
||||
<div class="node-role ${node.current ? 'current' : ''}">${role}${node.current ? ' · local' : ''}</div>
|
||||
<div class="node-caret">▾</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-details">
|
||||
<div class="node-meta">
|
||||
${repoChips || '<span class="node-chip">No repo data</span>'}
|
||||
</div>
|
||||
<div class="node-services"><b>Running:</b> ${serviceText}</div>
|
||||
<div class="node-profile"><b>Profile:</b> ${expected}</div>
|
||||
<div class="node-nf-grid">${nfTiles || '<div class="node-empty">No node-scoped NF data</div>'}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function loadAlerts() {
|
||||
try {
|
||||
const d = await (await fetch('./api/alerts')).json();
|
||||
@@ -281,6 +427,7 @@ async function loadAlerts() {
|
||||
`<div class="alert-row ${a.severity||'warning'}">
|
||||
<div class="alert-row-name">${a.name}</div>
|
||||
<div class="alert-row-desc">${a.summary||a.instance||''}</div>
|
||||
<div class="alert-row-node">${(a.nodes||[]).length ? 'Node: ' + a.nodes.map(n => n.hostname).join(', ') : 'Node: unresolved'}</div>
|
||||
</div>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
@@ -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 & 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 & 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() {
|
||||
|
||||
Reference in New Issue
Block a user