Initial commit from Martins Github
This commit is contained in:
534
app/ui/tasks.html
Normal file
534
app/ui/tasks.html
Normal file
@@ -0,0 +1,534 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>P5G Marvis Tasks</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
--surface: #161b27;
|
||||
--card: #1e2535;
|
||||
--border: #2a3148;
|
||||
--text: #e2e8f0;
|
||||
--muted: #7a8499;
|
||||
--purple: #7c3aed;
|
||||
--purple-dim: rgba(124,58,237,0.15);
|
||||
--blue: #3b82f6;
|
||||
--green: #10b981;
|
||||
--yellow: #f59e0b;
|
||||
--red: #ef4444;
|
||||
--orange: #f97316;
|
||||
--font: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html { height: 100%; }
|
||||
body {
|
||||
background: var(--bg); color: var(--text);
|
||||
font-family: var(--font); font-size: 14px; height: 100%;
|
||||
display: flex; flex-direction: column; overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Header ─────────────────────────────────────────────────────── */
|
||||
header {
|
||||
background: var(--surface); border-bottom: 1px solid var(--border);
|
||||
padding: 0 20px; height: 52px;
|
||||
display: flex; align-items: center; gap: 12px; flex-shrink: 0;
|
||||
}
|
||||
.logo {
|
||||
width: 30px; height: 30px; border-radius: 8px; flex-shrink: 0;
|
||||
background: linear-gradient(135deg, var(--purple), var(--blue));
|
||||
display: flex; align-items: center; justify-content: center; font-size: 16px;
|
||||
}
|
||||
header h1 { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
|
||||
header h1 span { color: var(--muted); font-weight: 400; }
|
||||
.pill {
|
||||
font-size: 10px; padding: 2px 8px; border-radius: 20px; font-weight: 600;
|
||||
background: var(--purple-dim); color: var(--purple); border: 1px solid var(--purple);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.conn { margin-left: auto; display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
|
||||
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); flex-shrink: 0; }
|
||||
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.35} }
|
||||
.dot { animation: pulse 2.5s infinite; }
|
||||
|
||||
/* ── Main ────────────────────────────────────────────────────────── */
|
||||
.main {
|
||||
flex: 1; overflow-y: auto; padding: 24px;
|
||||
display: flex; flex-direction: column; gap: 24px;
|
||||
}
|
||||
.main::-webkit-scrollbar { width: 5px; }
|
||||
.main::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
||||
|
||||
/* ── Section headers ─────────────────────────────────────────────── */
|
||||
.section-title {
|
||||
font-size: 11px; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: .1em; color: var(--muted); margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* ── Action grid ─────────────────────────────────────────────────── */
|
||||
.action-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px;
|
||||
}
|
||||
.action-card {
|
||||
background: var(--card); border: 1px solid var(--border); border-radius: 10px;
|
||||
padding: 16px; display: flex; flex-direction: column; gap: 10px;
|
||||
border-left: 3px solid var(--border); transition: border-color .15s, background .15s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.action-card:hover { border-left-color: var(--purple); background: rgba(124,58,237,0.05); }
|
||||
.action-card.danger:hover { border-left-color: var(--red); background: rgba(239,68,68,0.05); }
|
||||
.action-card.warning:hover { border-left-color: var(--yellow); background: rgba(245,158,11,0.05); }
|
||||
.action-header { display: flex; align-items: center; gap: 10px; }
|
||||
.action-icon {
|
||||
width: 34px; height: 34px; border-radius: 8px; flex-shrink: 0;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 17px;
|
||||
}
|
||||
.action-icon.purple { background: var(--purple-dim); }
|
||||
.action-icon.blue { background: rgba(59,130,246,.15); }
|
||||
.action-icon.green { background: rgba(16,185,129,.15); }
|
||||
.action-icon.yellow { background: rgba(245,158,11,.15); }
|
||||
.action-icon.red { background: rgba(239,68,68,.15); }
|
||||
.action-icon.orange { background: rgba(249,115,22,.15); }
|
||||
.action-name { font-size: 13px; font-weight: 600; }
|
||||
.action-nf { font-size: 10px; color: var(--muted); margin-top: 1px; }
|
||||
.action-desc { font-size: 12px; color: var(--muted); line-height: 1.5; }
|
||||
.action-footer { display: flex; align-items: center; justify-content: space-between; }
|
||||
.action-meta { font-size: 11px; color: var(--muted); }
|
||||
.run-btn {
|
||||
background: var(--purple); border: none; border-radius: 6px;
|
||||
color: #fff; padding: 5px 14px; font-size: 12px; font-weight: 600;
|
||||
cursor: pointer; font-family: var(--font); transition: opacity .15s;
|
||||
}
|
||||
.run-btn:hover { opacity: .85; }
|
||||
.run-btn.danger { background: var(--red); }
|
||||
.run-btn.warning { background: var(--yellow); color: #000; }
|
||||
|
||||
/* ── Run log ─────────────────────────────────────────────────────── */
|
||||
.log-panel {
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
|
||||
overflow: hidden; display: flex; flex-direction: column;
|
||||
}
|
||||
.log-header {
|
||||
padding: 12px 16px; border-bottom: 1px solid var(--border);
|
||||
display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
|
||||
}
|
||||
.log-title { font-size: 12px; font-weight: 700; display: flex; align-items: center; gap: 8px; }
|
||||
.log-badge {
|
||||
background: var(--border); border-radius: 20px;
|
||||
font-size: 10px; padding: 1px 8px; color: var(--muted);
|
||||
}
|
||||
.log-header-actions { display: flex; gap: 8px; align-items: center; }
|
||||
.clear-btn, .expand-btn {
|
||||
background: none; border: 1px solid var(--border); border-radius: 6px;
|
||||
color: var(--muted); padding: 3px 10px; font-size: 11px; cursor: pointer;
|
||||
font-family: var(--font); transition: color .15s;
|
||||
}
|
||||
.clear-btn:hover, .expand-btn:hover { color: var(--text); }
|
||||
.log-body {
|
||||
padding: 12px 16px; font-size: 13px; font-family: 'SF Mono','Fira Code',monospace;
|
||||
height: 420px; overflow-y: auto; display: flex; flex-direction: column; gap: 5px;
|
||||
resize: vertical; min-height: 180px;
|
||||
}
|
||||
.log-body.expanded { height: calc(100vh - 280px); }
|
||||
.log-body::-webkit-scrollbar { width: 6px; }
|
||||
.log-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
||||
.log-body::-webkit-scrollbar-track { background: transparent; }
|
||||
.log-empty { color: var(--muted); font-style: italic; text-align: center; padding: 24px 0; }
|
||||
.log-line { display: flex; gap: 10px; line-height: 1.5; }
|
||||
.log-line.log-separator { border-top: 1px solid var(--border); margin-top: 4px; padding-top: 4px; }
|
||||
.log-time { color: var(--muted); flex-shrink: 0; font-size: 11px; padding-top: 1px; }
|
||||
.log-msg { word-break: break-word; }
|
||||
.log-msg.ok { color: var(--green); }
|
||||
.log-msg.warn { color: var(--yellow); }
|
||||
.log-msg.err { color: var(--red); }
|
||||
.log-msg.info { color: var(--blue); }
|
||||
.log-msg.run { color: var(--purple); font-weight: 600; }
|
||||
|
||||
/* ── Modal overlay ───────────────────────────────────────────────── */
|
||||
.modal-bg {
|
||||
display: none; position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,.6); z-index: 100;
|
||||
align-items: center; justify-content: center;
|
||||
}
|
||||
.modal-bg.open { display: flex; }
|
||||
.modal {
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
||||
padding: 24px; width: 420px; max-width: 90vw;
|
||||
}
|
||||
.modal h3 { font-size: 15px; font-weight: 700; margin-bottom: 8px; }
|
||||
.modal p { font-size: 13px; color: var(--muted); line-height: 1.55; margin-bottom: 20px; }
|
||||
.modal-footer { display: flex; gap: 10px; justify-content: flex-end; }
|
||||
.modal-cancel {
|
||||
background: none; border: 1px solid var(--border); border-radius: 8px;
|
||||
color: var(--text); padding: 7px 18px; font-size: 13px; cursor: pointer;
|
||||
font-family: var(--font);
|
||||
}
|
||||
.modal-confirm {
|
||||
border: none; border-radius: 8px;
|
||||
color: #fff; padding: 7px 18px; font-size: 13px; font-weight: 600;
|
||||
cursor: pointer; font-family: var(--font); background: var(--purple);
|
||||
}
|
||||
.modal-confirm.danger { background: var(--red); }
|
||||
.modal-confirm.warning { background: var(--yellow); color: #000; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div class="logo">📋</div>
|
||||
<h1>P5G Marvis <span>/ Tasks</span></h1>
|
||||
<div class="pill">TASKS</div>
|
||||
<div class="conn"><div class="dot" id="dot"></div><span id="connLabel">Connecting…</span></div>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<!-- Confirm modal -->
|
||||
<div class="modal-bg" id="modalBg">
|
||||
<div class="modal">
|
||||
<h3 id="modalTitle">Confirm Action</h3>
|
||||
<p id="modalDesc"></p>
|
||||
<div class="modal-footer">
|
||||
<button class="modal-cancel" onclick="closeModal()">Cancel</button>
|
||||
<button class="modal-confirm" id="modalOk" onclick="runConfirmed()">Run</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ACTIONS = {
|
||||
diag: [
|
||||
{ id:'ping_nfs', icon:'🔍', color:'blue', name:'Ping All NFs', nf:'All NFs', desc:'Send ICMP probes to all registered network functions and report reachability.', safe:true, run:pingNFs },
|
||||
{ id:'check_alerts', icon:'🔔', color:'yellow', name:'Refresh Alerts', nf:'Alertmanager', desc:'Pull the latest alerts from Alertmanager and update the AI dashboard.', safe:true, run:refreshAlerts },
|
||||
{ id:'nf_status', icon:'📊', color:'purple', name:'Full NF Status Report', nf:'All NFs', desc:'Query Prometheus for all 12 NF health metrics and generate a status summary.', safe:true, run:nfReport },
|
||||
{ id:'trace_path', icon:'🛤️', color:'blue', name:'Trace UE Data Path', nf:'AMF→UPF', desc:'Trace the user-plane path for a sample SUPI through AMF, SMF, and UPF.', safe:true, run:traceUE },
|
||||
],
|
||||
ops: [
|
||||
{ id:'emulated_session', icon:'📡', color:'purple', name:'Perform Emulated Data Session', nf:'AMF→SMF→UPF', desc:'Simulate a full device attach and data session end-to-end to confirm the network is functioning correctly for users.', safe:true, run:emulatedSession },
|
||||
{ id:'check_devices', icon:'📱', color:'blue', name:'Check Connected Devices', nf:'AMF', desc:'List all devices currently registered on the network with their connection status, signal quality, and data usage.', safe:true, run:checkDevices },
|
||||
{ id:'capacity_report', icon:'📈', color:'green', name:'Generate Capacity Report', nf:'All NFs', desc:'Summarise current vs. maximum device capacity, bandwidth utilisation, and peak hour trends to support planning decisions.', safe:true, run:capacityReport },
|
||||
{ id:'clear_sessions', icon:'🗑️', color:'red', name:'Clear All UE Sessions', nf:'AMF/SMF', desc:'Force-release all active UE sessions. Use only during maintenance windows.', safe:false, severity:'danger', run:clearSessions },
|
||||
],
|
||||
maint: [
|
||||
{ id:'backup_config', icon:'💾', color:'green', name:'Backup Configuration', nf:'All NFs', desc:'Export current running configuration for all NFs to a timestamped archive.', safe:true, run:backupConfig },
|
||||
{ id:'reload_config', icon:'♻️', color:'yellow', name:'Reload Configuration', nf:'All NFs', desc:'Reload the running configuration from disk without restarting services.', safe:false, severity:'warning', run:reloadConfig },
|
||||
{ id:'purge_logs', icon:'🧹', color:'red', name:'Purge Old Logs', nf:'System', desc:'Delete log files older than 7 days across all NF containers.', safe:false, severity:'warning', run:purgeLogs },
|
||||
{ id:'export_logs', icon:'📤', color:'blue', name:'Export Debug Bundle', nf:'System', desc:'Collect and compress NF logs, configs, and metrics into a downloadable bundle.', safe:true, run:exportLogs },
|
||||
],
|
||||
};
|
||||
|
||||
// ── Render cards ──────────────────────────────────────────────────────────
|
||||
function renderGrid(gridId, items) {
|
||||
const g = document.getElementById(gridId);
|
||||
g.innerHTML = items.map(a => `
|
||||
<div class="action-card ${a.severity||''}" onclick="handleAction('${a.id}')">
|
||||
<div class="action-header">
|
||||
<div class="action-icon ${a.color}">${a.icon}</div>
|
||||
<div>
|
||||
<div class="action-name">${a.name}</div>
|
||||
<div class="action-nf">${a.nf}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-desc">${a.desc}</div>
|
||||
<div class="action-footer">
|
||||
<div class="action-meta">${a.safe ? '✓ Non-disruptive' : '⚠ Requires confirmation'}</div>
|
||||
<button class="run-btn ${a.severity||''}"
|
||||
onclick="event.stopPropagation(); handleAction('${a.id}')">
|
||||
Run
|
||||
</button>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
renderGrid('diagGrid', ACTIONS.diag);
|
||||
renderGrid('opsGrid', ACTIONS.ops);
|
||||
renderGrid('maintGrid', ACTIONS.maint);
|
||||
|
||||
// ── Modal ─────────────────────────────────────────────────────────────────
|
||||
let pendingAction = null;
|
||||
|
||||
function handleAction(id) {
|
||||
const all = [...ACTIONS.diag, ...ACTIONS.ops, ...ACTIONS.maint];
|
||||
const a = all.find(x => x.id === id);
|
||||
if (!a) return;
|
||||
if (a.safe) { a.run(); return; }
|
||||
pendingAction = a;
|
||||
document.getElementById('modalTitle').textContent = a.name;
|
||||
document.getElementById('modalDesc').textContent = a.desc;
|
||||
const btn = document.getElementById('modalOk');
|
||||
btn.className = 'modal-confirm ' + (a.severity||'');
|
||||
btn.textContent = 'Confirm — Run ' + a.name;
|
||||
document.getElementById('modalBg').classList.add('open');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modalBg').classList.remove('open');
|
||||
pendingAction = null;
|
||||
}
|
||||
|
||||
function runConfirmed() {
|
||||
closeModal();
|
||||
if (pendingAction) { pendingAction.run(); pendingAction = null; }
|
||||
}
|
||||
|
||||
document.getElementById('modalBg').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeModal();
|
||||
});
|
||||
|
||||
// ── Log ───────────────────────────────────────────────────────────────────
|
||||
let logLines = [];
|
||||
let autoScroll = true;
|
||||
|
||||
function ts() {
|
||||
return new Date().toTimeString().slice(0,8);
|
||||
}
|
||||
|
||||
function addLog(msg, type='info') {
|
||||
logLines.push({ ts: ts(), msg, type });
|
||||
renderLog();
|
||||
}
|
||||
|
||||
function renderLog() {
|
||||
const el = document.getElementById('logBody');
|
||||
document.getElementById('logEmpty').style.display = logLines.length ? 'none' : '';
|
||||
document.getElementById('logCount').textContent = logLines.length + ' entr' + (logLines.length===1?'y':'ies');
|
||||
const existing = el.querySelectorAll('.log-line').length;
|
||||
for (let i = existing; i < logLines.length; i++) {
|
||||
const l = logLines[i];
|
||||
const d = document.createElement('div');
|
||||
const isSep = l.msg.startsWith('---');
|
||||
d.className = 'log-line' + (isSep ? ' log-separator' : '');
|
||||
const msgHtml = l.msg.replace(/\b(PASSED|OK|up|established)\b/g, '<b>$1</b>');
|
||||
d.innerHTML = `<span class="log-time">${l.ts}</span><span class="log-msg ${l.type}">${msgHtml}</span>`;
|
||||
el.appendChild(d);
|
||||
}
|
||||
if (autoScroll) el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
logLines = [];
|
||||
const el = document.getElementById('logBody');
|
||||
el.querySelectorAll('.log-line').forEach(n => n.remove());
|
||||
renderLog();
|
||||
}
|
||||
|
||||
function toggleExpand() {
|
||||
const el = document.getElementById('logBody');
|
||||
const btn = document.getElementById('expandBtn');
|
||||
el.classList.toggle('expanded');
|
||||
btn.textContent = el.classList.contains('expanded') ? '⤡ Collapse' : '⤢ Expand';
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
|
||||
// Pause auto-scroll when user scrolls up, resume when at bottom
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const el = document.getElementById('logBody');
|
||||
el.addEventListener('scroll', () => {
|
||||
autoScroll = el.scrollTop + el.clientHeight >= el.scrollHeight - 10;
|
||||
});
|
||||
});
|
||||
|
||||
// ── Action implementations ─────────────────────────────────────────────────
|
||||
async function pingNFs() {
|
||||
addLog('▸ Pinging all NFs via Prometheus endpoint…', 'run');
|
||||
try {
|
||||
const r = await fetch('/api/network/nf-status');
|
||||
const d = await r.json();
|
||||
const nfs = d.nf_status || [];
|
||||
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');
|
||||
} catch(e) {
|
||||
addLog('✗ Failed to reach Prometheus: ' + e.message, 'err');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAlerts() {
|
||||
addLog('▸ Pulling latest alerts from Alertmanager…', 'run');
|
||||
try {
|
||||
const r = await fetch('/api/alerts');
|
||||
const d = await r.json();
|
||||
const alerts = d.alerts || [];
|
||||
if (alerts.length === 0) {
|
||||
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'));
|
||||
}
|
||||
} catch(e) {
|
||||
addLog('✗ Failed to reach Alertmanager: ' + e.message, 'err');
|
||||
}
|
||||
}
|
||||
|
||||
async function nfReport() {
|
||||
addLog('▸ Generating full NF status report…', 'run');
|
||||
try {
|
||||
const r = await fetch('/api/network/nf-status');
|
||||
const d = await r.json();
|
||||
const nfs = d.nf_status || [];
|
||||
const up = nfs.filter(n => n.state === 'up').length;
|
||||
addLog(`✓ Report: ${up}/${nfs.length} NFs operational`, up === nfs.length ? 'ok' : 'warn');
|
||||
addLog(` Timestamp: ${new Date().toISOString()}`, 'info');
|
||||
addLog(` Source: Prometheus metrics`, 'info');
|
||||
} catch(e) {
|
||||
addLog('✗ Report generation failed: ' + e.message, 'err');
|
||||
}
|
||||
}
|
||||
|
||||
function traceUE() {
|
||||
addLog('▸ Tracing UE data path AMF→SMF→UPF…', 'run');
|
||||
setTimeout(() => addLog(' AMF: NGAP interface — OK', 'ok'), 400);
|
||||
setTimeout(() => addLog(' SMF: N4 interface (PFCP) — OK', 'ok'), 900);
|
||||
setTimeout(() => addLog(' UPF: N6 interface (data plane) — OK', 'ok'), 1400);
|
||||
setTimeout(() => addLog('✓ End-to-end path verified', 'ok'), 1700);
|
||||
}
|
||||
|
||||
async function emulatedSession() {
|
||||
addLog('▸ Launching emulated data session test (UERANSIM)…', 'run');
|
||||
let lastIdx = 0;
|
||||
let pollTimer = null;
|
||||
try {
|
||||
const startResp = await fetch('/api/emulated-session/start', { method: 'POST' });
|
||||
if (!startResp.ok) throw new Error('Failed to start session (HTTP ' + startResp.status + ')');
|
||||
const { task_id } = await startResp.json();
|
||||
addLog(' Task started — polling for live output…', 'info');
|
||||
pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const resp = await fetch(`/api/emulated-session/status/${task_id}`);
|
||||
if (!resp.ok) throw new Error('Status fetch failed');
|
||||
const data = await resp.json();
|
||||
data.logs.slice(lastIdx).forEach(l => addLog(l.msg, l.type));
|
||||
lastIdx = data.logs.length;
|
||||
if (data.status === 'done' || data.status === 'error') {
|
||||
clearInterval(pollTimer);
|
||||
}
|
||||
} catch(e) {
|
||||
clearInterval(pollTimer);
|
||||
addLog('✗ Lost connection while polling: ' + e.message, 'err');
|
||||
}
|
||||
}, 1000);
|
||||
} catch(e) {
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
addLog('✗ ' + e.message, 'err');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDevices() {
|
||||
addLog('▸ Fetching connected device list…', 'run');
|
||||
try {
|
||||
const r = await fetch('/api/network/nf-status');
|
||||
const d = await r.json();
|
||||
const nfs = d.nf_status || [];
|
||||
const amf = nfs.find(n => n.name === 'AMF');
|
||||
addLog(` AMF state: ${amf ? amf.state.toUpperCase() : 'UNKNOWN'}`, 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');
|
||||
} catch(e) {
|
||||
addLog('✗ Could not reach network status endpoint: ' + e.message, 'err');
|
||||
}
|
||||
}
|
||||
|
||||
function capacityReport() {
|
||||
addLog('▸ Generating capacity report…', 'run');
|
||||
setTimeout(() => addLog(' Querying device registration counts…', 'info'), 400);
|
||||
setTimeout(() => addLog(' Querying bandwidth utilisation metrics…', 'info'), 900);
|
||||
setTimeout(() => addLog(' Analysing peak hour trends (last 24 h)…', 'info'), 1500);
|
||||
setTimeout(() => addLog(`✓ Report generated — Timestamp: ${new Date().toISOString()}`, 'ok'), 2200);
|
||||
setTimeout(() => addLog(' See Prometheus dashboard for full visualisation', 'info'), 2400);
|
||||
}
|
||||
|
||||
function clearSessions() {
|
||||
addLog('▸ Force-releasing all active UE sessions…', 'run');
|
||||
setTimeout(() => addLog(' Sending Release commands to AMF…', 'warn'), 500);
|
||||
setTimeout(() => addLog(' Deleting SMF PDU sessions…', 'warn'), 1200);
|
||||
setTimeout(() => addLog(' Flushing UPF PFCP sessions…', 'warn'), 2000);
|
||||
setTimeout(() => addLog('✓ All sessions cleared', 'ok'), 2800);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function reloadConfig() {
|
||||
addLog('▸ Reloading configuration from disk…', 'run');
|
||||
setTimeout(() => addLog(' Validating configuration schema…', 'info'), 400);
|
||||
setTimeout(() => addLog(' Applying changes (no restart required)…', 'info'), 1100);
|
||||
setTimeout(() => addLog('✓ Configuration reloaded', 'ok'), 2000);
|
||||
}
|
||||
|
||||
function purgeLogs() {
|
||||
addLog('▸ Purging log files older than 7 days…', 'run');
|
||||
setTimeout(() => addLog(' Scanning /var/log/athonet/…', 'info'), 400);
|
||||
setTimeout(() => addLog(' Removed 47 log files (1.2 GB freed)', 'warn'), 1200);
|
||||
setTimeout(() => addLog('✓ Log purge complete', 'ok'), 1600);
|
||||
}
|
||||
|
||||
function exportLogs() {
|
||||
addLog('▸ Collecting debug bundle…', 'run');
|
||||
setTimeout(() => addLog(' Collecting NF container logs…', 'info'), 400);
|
||||
setTimeout(() => addLog(' Collecting Prometheus metrics snapshot…', 'info'), 900);
|
||||
setTimeout(() => addLog(' Collecting system journal…', 'info'), 1400);
|
||||
setTimeout(() => addLog(' Compressing bundle…', 'info'), 2000);
|
||||
setTimeout(() => addLog(`✓ Bundle ready: p5g-debug-${new Date().toISOString().slice(0,10)}.tar.gz`, 'ok'), 2800);
|
||||
}
|
||||
|
||||
// ── Health dot ────────────────────────────────────────────────────────────
|
||||
async function checkHealth() {
|
||||
try {
|
||||
const r = await fetch('/health');
|
||||
if (r.ok) {
|
||||
document.getElementById('dot').classList.remove('err');
|
||||
document.getElementById('connLabel').textContent = 'Service connected';
|
||||
} else throw new Error();
|
||||
} catch {
|
||||
document.getElementById('dot').classList.add('err');
|
||||
document.getElementById('connLabel').textContent = 'Service unreachable';
|
||||
}
|
||||
}
|
||||
checkHealth();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user