mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 15:37:03 +00:00
- Add ARP-based IP collision detection for multi-device environments - MAC-based pseudo-random IP offset to spread devices across range - Gratuitous ARP announcement to prevent IP conflicts - Fix EspressoBin DSA network: target wan interface, not eth0 CPU port - Static IP fallback 192.168.255.250 when DHCP unavailable - Sync all build scripts to version 1.7.0 - Add screenshot script with 90+ module URLs - Add mock HTML screenshots for documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
537 lines
24 KiB
HTML
537 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>SecuBox - Monitoring</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="/shared/crt-light.css">
|
|
<link rel="stylesheet" href="/shared/sidebar-light.css">
|
|
<style>
|
|
:root {
|
|
/* P31 Phosphor spectrum */
|
|
--p31-peak: #00dd44;
|
|
--p31-hot: #00ff55;
|
|
--p31-mid: #009933;
|
|
--p31-dim: #006622;
|
|
--p31-ghost: #003311;
|
|
--p31-decay: #ffb347;
|
|
--p31-decay-dim: #cc7722;
|
|
/* Tube glass */
|
|
--tube-light: #e8f5e9;
|
|
--tube-pale: #c8e6c9;
|
|
--tube-soft: #a5d6a7;
|
|
/* Legacy mappings */
|
|
--bg-dark: var(--tube-light);
|
|
--bg-card: var(--tube-pale);
|
|
--bg-sidebar: var(--tube-black);
|
|
--border: var(--tube-soft);
|
|
--text: var(--tube-dark);
|
|
--text-dim: var(--p31-dim);
|
|
--primary: var(--p31-peak);
|
|
--cyan: var(--p31-peak);
|
|
--green: var(--p31-peak);
|
|
--red: #ff4466;
|
|
--yellow: var(--p31-decay);
|
|
--orange: var(--p31-decay-dim);
|
|
--purple: #a371f7;
|
|
/* Bloom effects */
|
|
--bloom-text: 0 0 2px var(--p31-peak), 0 0 6px var(--p31-peak), 0 0 14px rgba(51,255,102,0.5);
|
|
--bloom-soft: 0 0 6px var(--p31-peak), 0 0 14px rgba(51,255,102,0.5);
|
|
--bloom-amber: 0 0 3px var(--p31-decay), 0 0 10px rgba(255,179,71,0.4);
|
|
}
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: 'Courier Prime', 'Courier New', monospace;
|
|
background: var(--tube-light);
|
|
background-image: radial-gradient(ellipse at 50% 40%, rgba(51,255,102,0.025) 0%, transparent 70%);
|
|
color: var(--tube-dark);
|
|
display: flex;
|
|
min-height: 100vh;
|
|
}
|
|
.sidebar { width: 220px; background: var(--bg-sidebar); border-right: 1px solid var(--border); position: fixed; height: 100vh; overflow-y: auto; }
|
|
.sidebar-header { padding: 1rem; border-bottom: 1px solid var(--border); }
|
|
.logo { font-weight: bold; font-size: 1.2rem; }
|
|
.logo span:nth-child(1) { color: var(--cyan); } .logo span:nth-child(2) { color: var(--red); } .logo span:nth-child(3) { color: var(--green); } .logo span:nth-child(4) { color: var(--yellow); }
|
|
.nav-section { padding: 0.5rem 0; }
|
|
.nav-section-title { padding: 0.5rem 1rem; font-size: 0.75rem; color: var(--text-dim); text-transform: uppercase; }
|
|
.nav-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 1rem; color: var(--text-dim); text-decoration: none; font-size: 0.9rem; border-left: 3px solid transparent; }
|
|
.nav-item:hover { background: rgba(255,255,255,0.05); color: var(--text); }
|
|
.nav-item.active { background: rgba(88,166,255,0.1); color: var(--primary); border-left-color: var(--primary); }
|
|
.nav-item .icon { width: 20px; text-align: center; }
|
|
.main { flex: 1; margin-left: 220px; }
|
|
.header { display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.5rem; border-bottom: 1px solid var(--border); background: var(--bg-card); }
|
|
.header-title { display: flex; align-items: center; gap: 1rem; }
|
|
.header-title h1 { font-size: 1.5rem; font-weight: 600; }
|
|
.header-actions { display: flex; gap: 0.75rem; align-items: center; }
|
|
.btn { padding: 0.5rem 1rem; border-radius: 6px; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); cursor: pointer; font-size: 0.875rem; }
|
|
.btn:hover { background: var(--bg-dark); }
|
|
.btn.primary { background: rgba(88,166,255,0.1); border-color: var(--primary); color: var(--primary); }
|
|
.content { padding: 1.5rem; }
|
|
.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.5rem; }
|
|
.stat-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; text-align: center; }
|
|
.stat-card .value { font-size: 2.5rem; font-weight: bold; margin-bottom: 0.25rem; }
|
|
.stat-card .label { font-size: 0.75rem; color: var(--text-dim); text-transform: uppercase; }
|
|
.stat-card.cyan .value { color: var(--cyan); }
|
|
.stat-card.purple .value { color: var(--purple); }
|
|
.stat-card.green .value { color: var(--green); }
|
|
.stat-card.yellow .value { color: var(--yellow); }
|
|
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
|
|
.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; margin-bottom: 1.5rem; }
|
|
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
|
|
.card-header h2 { font-size: 1rem; font-weight: 600; }
|
|
.chart-container { height: 180px; position: relative; background: var(--bg-dark); border-radius: 6px; padding: 0.5rem; }
|
|
.chart { width: 100%; height: 100%; }
|
|
.gauge-container { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
|
|
.gauge { text-align: center; }
|
|
.gauge-ring { width: 100px; height: 100px; margin: 0 auto; position: relative; }
|
|
.gauge-ring svg { transform: rotate(-90deg); }
|
|
.gauge-ring circle { fill: none; stroke-width: 8; }
|
|
.gauge-ring .bg { stroke: var(--border); }
|
|
.gauge-ring .value { stroke-linecap: round; transition: stroke-dasharray 0.5s; }
|
|
.gauge-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 1.25rem; font-weight: bold; }
|
|
.gauge-label { margin-top: 0.5rem; color: var(--text-dim); font-size: 0.85rem; }
|
|
.process-table { width: 100%; border-collapse: collapse; }
|
|
.process-table th { text-align: left; padding: 0.5rem; color: var(--text-dim); font-size: 0.75rem; text-transform: uppercase; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg-card); }
|
|
.process-table td { padding: 0.5rem; border-bottom: 1px solid var(--border); font-size: 0.85rem; }
|
|
.scroll-list { max-height: 280px; overflow-y: auto; }
|
|
.load-box { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; text-align: center; padding: 1rem; background: var(--bg-dark); border-radius: 6px; }
|
|
.load-box .value { font-size: 1.75rem; font-weight: bold; color: var(--cyan); }
|
|
.load-box .label { font-size: 0.75rem; color: var(--text-dim); }
|
|
.alert-item { padding: 0.75rem; background: rgba(248,81,73,0.1); border-left: 3px solid var(--red); border-radius: 0 6px 6px 0; margin-bottom: 0.5rem; }
|
|
.alert-item.warning { background: rgba(210,153,34,0.1); border-left-color: var(--yellow); }
|
|
@media (max-width: 1200px) { .stats-row { grid-template-columns: repeat(2, 1fr); } }
|
|
@media (max-width: 900px) { .grid-2 { grid-template-columns: 1fr; } .sidebar { display: none; } .main { margin-left: 0; } }
|
|
|
|
/* CRT Enhancements */
|
|
.main { margin-left: 220px; padding: 1.5rem; }
|
|
|
|
.header h1 {
|
|
color: var(--p31-hot);
|
|
text-shadow: var(--bloom-text);
|
|
font-weight: 700;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.badge {
|
|
font-family: 'Courier Prime', monospace;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
border: 1px solid;
|
|
}
|
|
.badge-blue, .badge-primary { background: rgba(51,255,102,0.1); border-color: var(--p31-peak); color: var(--p31-peak); }
|
|
.badge-green { background: rgba(51,255,102,0.15); border-color: var(--p31-peak); color: var(--p31-peak); text-shadow: var(--bloom-soft); }
|
|
.badge-cyan { background: rgba(51,255,102,0.1); border-color: var(--p31-mid); color: var(--p31-mid); }
|
|
.badge-purple { background: rgba(163,113,247,0.1); border-color: #a371f7; color: #a371f7; }
|
|
.badge-yellow, .badge-amber { background: rgba(255,179,71,0.1); border-color: var(--p31-decay); color: var(--p31-decay); }
|
|
.badge-red { background: rgba(255,68,102,0.1); border-color: #ff4466; color: #ff4466; }
|
|
|
|
.btn {
|
|
font-family: 'Courier Prime', monospace;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
border: 1px solid var(--tube-soft);
|
|
background: var(--tube-pale);
|
|
color: var(--p31-mid);
|
|
transition: all 0.2s;
|
|
}
|
|
.btn:hover {
|
|
border-color: var(--p31-dim);
|
|
color: var(--p31-peak);
|
|
text-shadow: var(--bloom-soft);
|
|
box-shadow: 0 0 12px rgba(51,255,102,0.1);
|
|
}
|
|
.btn-green, .btn-primary, .btn.primary, .btn.success {
|
|
border-color: var(--p31-peak);
|
|
color: var(--p31-peak);
|
|
background: rgba(51,255,102,0.1);
|
|
}
|
|
.btn-green:hover, .btn-primary:hover, .btn.primary:hover, .btn.success:hover {
|
|
background: rgba(51,255,102,0.2);
|
|
box-shadow: 0 0 16px rgba(51,255,102,0.2);
|
|
}
|
|
.btn-red, .btn.danger {
|
|
border-color: #ff4466;
|
|
color: #ff4466;
|
|
background: rgba(255,68,102,0.1);
|
|
}
|
|
.btn-red:hover, .btn.danger:hover {
|
|
background: rgba(255,68,102,0.2);
|
|
}
|
|
.btn-blue {
|
|
border-color: var(--p31-mid);
|
|
color: var(--p31-mid);
|
|
background: rgba(51,255,102,0.05);
|
|
}
|
|
.btn-blue:hover {
|
|
border-color: var(--p31-peak);
|
|
color: var(--p31-peak);
|
|
}
|
|
|
|
.card {
|
|
background: var(--tube-pale);
|
|
border: 1px solid var(--tube-soft);
|
|
box-shadow: inset 0 0 20px rgba(0,0,0,0.3);
|
|
}
|
|
.card:hover {
|
|
border-color: var(--p31-dim);
|
|
box-shadow: 0 0 8px rgba(51,255,102,0.05), inset 0 0 20px rgba(0,0,0,0.3);
|
|
}
|
|
.card-title, .card-header h2 {
|
|
color: var(--p31-decay);
|
|
text-shadow: var(--bloom-amber);
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--tube-pale);
|
|
border: 1px solid var(--tube-soft);
|
|
}
|
|
.stat-value, .stat-card .value {
|
|
text-shadow: 0 0 10px currentColor;
|
|
}
|
|
.stat-label, .stat-card .label {
|
|
color: var(--p31-dim);
|
|
letter-spacing: 0.15em;
|
|
}
|
|
.stat-card.cyan .value, .stat-card.green .value { color: var(--p31-peak); }
|
|
.stat-card.yellow .value { color: var(--p31-decay); }
|
|
.stat-card.red .value { color: #ff4466; }
|
|
|
|
table, .table {
|
|
font-family: 'Courier Prime', monospace;
|
|
}
|
|
th, .table th {
|
|
color: var(--p31-decay);
|
|
text-shadow: var(--bloom-amber);
|
|
letter-spacing: 0.15em;
|
|
border-bottom: 1px solid var(--p31-ghost);
|
|
}
|
|
td, .table td {
|
|
border-bottom: 1px solid var(--p31-ghost);
|
|
color: var(--p31-mid);
|
|
}
|
|
tr:hover, .table tr:hover {
|
|
background: rgba(51,255,102,0.03);
|
|
}
|
|
|
|
.status-dot.active, .status-running {
|
|
color: var(--p31-peak);
|
|
text-shadow: var(--bloom-soft);
|
|
}
|
|
.status-dot.active {
|
|
background: var(--p31-peak);
|
|
box-shadow: 0 0 8px var(--p31-peak);
|
|
}
|
|
|
|
.toast {
|
|
background: var(--tube-pale);
|
|
border: 1px solid var(--p31-dim);
|
|
font-family: 'Courier Prime', monospace;
|
|
}
|
|
.toast-success { border-color: var(--p31-peak); }
|
|
.toast-error { border-color: #ff4466; }
|
|
|
|
.modal {
|
|
background: rgba(0,0,0,0.85);
|
|
}
|
|
.modal-content {
|
|
background: var(--tube-pale);
|
|
border: 1px solid var(--p31-dim);
|
|
box-shadow: 0 0 30px rgba(51,255,102,0.1);
|
|
}
|
|
.modal-title {
|
|
color: var(--p31-decay);
|
|
text-shadow: var(--bloom-amber);
|
|
letter-spacing: 0.1em;
|
|
}
|
|
|
|
.tabs {
|
|
background: var(--tube-pale);
|
|
border-bottom: 1px solid var(--p31-ghost);
|
|
}
|
|
.tab {
|
|
color: var(--p31-dim);
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
font-size: 0.8rem;
|
|
}
|
|
.tab:hover { color: var(--p31-mid); }
|
|
.tab.active {
|
|
color: var(--p31-peak);
|
|
text-shadow: var(--bloom-soft);
|
|
border-bottom-color: var(--p31-peak);
|
|
}
|
|
|
|
input, select, textarea {
|
|
font-family: 'Courier Prime', monospace;
|
|
background: var(--tube-light);
|
|
border: 1px solid var(--tube-soft);
|
|
color: var(--p31-mid);
|
|
padding: 0.5rem;
|
|
}
|
|
input:focus, select:focus, textarea:focus {
|
|
outline: none;
|
|
border-color: var(--p31-dim);
|
|
box-shadow: 0 0 8px rgba(51,255,102,0.1);
|
|
}
|
|
|
|
/* Scrollbar */
|
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
::-webkit-scrollbar-track { background: var(--tube-light); }
|
|
::-webkit-scrollbar-thumb { background: var(--p31-dim); }
|
|
::-webkit-scrollbar-thumb:hover { background: var(--p31-mid); }
|
|
|
|
/* Empty states */
|
|
.empty { color: var(--p31-dim); }
|
|
|
|
@media (max-width: 768px) {
|
|
.main { margin-left: 0; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="crt-light">
|
|
<nav class="sidebar" id="sidebar"></nav>
|
|
<script src="/shared/sidebar.js"></script>
|
|
|
|
<main class="main">
|
|
<header class="header">
|
|
<div class="header-title">
|
|
<h1>📊 System Monitoring</h1>
|
|
<span id="last-update" style="color: var(--text-dim); font-size: 0.85rem;">-</span>
|
|
</div>
|
|
<div class="header-actions">
|
|
<button class="btn" onclick="refresh()">🔄 Refresh</button>
|
|
<button class="btn primary" onclick="openNetdata()">Open Netdata UI</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="content">
|
|
<div class="stats-row">
|
|
<div class="stat-card cyan">
|
|
<div class="value" id="cpu-value">-</div>
|
|
<div class="label">CPU Usage</div>
|
|
</div>
|
|
<div class="stat-card purple">
|
|
<div class="value" id="mem-value">-</div>
|
|
<div class="label">Memory Usage</div>
|
|
</div>
|
|
<div class="stat-card green">
|
|
<div class="value" id="disk-value">-</div>
|
|
<div class="label">Disk Usage</div>
|
|
</div>
|
|
<div class="stat-card yellow">
|
|
<div class="value" id="net-value">-</div>
|
|
<div class="label">Network I/O</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid-2">
|
|
<div class="card">
|
|
<div class="card-header"><h2>📈 CPU History</h2></div>
|
|
<div class="chart-container">
|
|
<canvas id="cpu-chart" class="chart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header"><h2>🧠 Memory History</h2></div>
|
|
<div class="chart-container">
|
|
<canvas id="mem-chart" class="chart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header"><h2>🖥️ System Gauges</h2></div>
|
|
<div class="gauge-container">
|
|
<div class="gauge">
|
|
<div class="gauge-ring">
|
|
<svg viewBox="0 0 100 100">
|
|
<circle class="bg" cx="50" cy="50" r="40"/>
|
|
<circle class="value" id="cpu-gauge" cx="50" cy="50" r="40" stroke="var(--cyan)" stroke-dasharray="0 251"/>
|
|
</svg>
|
|
<div class="gauge-text" id="cpu-gauge-text">-</div>
|
|
</div>
|
|
<div class="gauge-label">CPU</div>
|
|
</div>
|
|
<div class="gauge">
|
|
<div class="gauge-ring">
|
|
<svg viewBox="0 0 100 100">
|
|
<circle class="bg" cx="50" cy="50" r="40"/>
|
|
<circle class="value" id="mem-gauge" cx="50" cy="50" r="40" stroke="var(--purple)" stroke-dasharray="0 251"/>
|
|
</svg>
|
|
<div class="gauge-text" id="mem-gauge-text">-</div>
|
|
</div>
|
|
<div class="gauge-label">Memory</div>
|
|
</div>
|
|
<div class="gauge">
|
|
<div class="gauge-ring">
|
|
<svg viewBox="0 0 100 100">
|
|
<circle class="bg" cx="50" cy="50" r="40"/>
|
|
<circle class="value" id="disk-gauge" cx="50" cy="50" r="40" stroke="var(--green)" stroke-dasharray="0 251"/>
|
|
</svg>
|
|
<div class="gauge-text" id="disk-gauge-text">-</div>
|
|
</div>
|
|
<div class="gauge-label">Disk</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header"><h2>📊 Load Average</h2></div>
|
|
<div class="load-box">
|
|
<div><div class="value" id="load1">-</div><div class="label">1 min</div></div>
|
|
<div><div class="value" id="load5">-</div><div class="label">5 min</div></div>
|
|
<div><div class="value" id="load15">-</div><div class="label">15 min</div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header"><h2>🔄 Top Processes</h2></div>
|
|
<div class="scroll-list">
|
|
<table class="process-table">
|
|
<thead><tr><th>Process</th><th>CPU %</th><th>MEM %</th><th>PID</th></tr></thead>
|
|
<tbody id="process-list"><tr><td colspan="4">Loading...</td></tr></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header"><h2>⚠️ Alerts</h2></div>
|
|
<div id="alerts-list">
|
|
<p style="color: var(--green);">No active alerts</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
const API = '/api/v1/netdata';
|
|
let cpuHistory = [], memHistory = [];
|
|
|
|
async function api(endpoint) {
|
|
try { const res = await fetch(API + endpoint); return res.json(); } catch (e) { return {}; }
|
|
}
|
|
|
|
function formatBytes(b) {
|
|
if (!b) return '0';
|
|
const k = 1024, s = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(b) / Math.log(k));
|
|
return (b / Math.pow(k, i)).toFixed(1) + s[i];
|
|
}
|
|
|
|
function setGauge(id, percent) {
|
|
const circumference = 251;
|
|
const offset = (percent / 100) * circumference;
|
|
document.getElementById(id).setAttribute('stroke-dasharray', `${offset} ${circumference}`);
|
|
document.getElementById(id + '-text').textContent = percent.toFixed(0) + '%';
|
|
}
|
|
|
|
function drawChart(canvasId, data, color) {
|
|
const canvas = document.getElementById(canvasId);
|
|
const ctx = canvas.getContext('2d');
|
|
const w = canvas.width = canvas.offsetWidth;
|
|
const h = canvas.height = canvas.offsetHeight;
|
|
|
|
ctx.clearRect(0, 0, w, h);
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
|
|
const step = w / (data.length - 1 || 1);
|
|
data.forEach((v, i) => {
|
|
const x = i * step;
|
|
const y = h - (v / 100) * h;
|
|
if (i === 0) ctx.moveTo(x, y);
|
|
else ctx.lineTo(x, y);
|
|
});
|
|
ctx.stroke();
|
|
|
|
ctx.lineTo(w, h);
|
|
ctx.lineTo(0, h);
|
|
ctx.closePath();
|
|
ctx.fillStyle = color.replace(')', ', 0.1)').replace('rgb', 'rgba');
|
|
ctx.fill();
|
|
}
|
|
|
|
async function loadStats() {
|
|
const data = await api('/stats');
|
|
const cpu = data.cpu_percent || 0;
|
|
const mem = data.memory_percent || 0;
|
|
const disk = data.disk_percent || 0;
|
|
|
|
document.getElementById('cpu-value').textContent = cpu.toFixed(1) + '%';
|
|
document.getElementById('mem-value').textContent = mem.toFixed(1) + '%';
|
|
document.getElementById('disk-value').textContent = disk.toFixed(1) + '%';
|
|
document.getElementById('net-value').textContent = formatBytes((data.net_rx || 0) + (data.net_tx || 0)) + '/s';
|
|
|
|
setGauge('cpu-gauge', cpu);
|
|
setGauge('mem-gauge', mem);
|
|
setGauge('disk-gauge', disk);
|
|
|
|
const load = data.load_avg || [0, 0, 0];
|
|
document.getElementById('load1').textContent = load[0]?.toFixed(2) || '-';
|
|
document.getElementById('load5').textContent = load[1]?.toFixed(2) || '-';
|
|
document.getElementById('load15').textContent = load[2]?.toFixed(2) || '-';
|
|
|
|
cpuHistory.push(cpu);
|
|
memHistory.push(mem);
|
|
if (cpuHistory.length > 60) cpuHistory.shift();
|
|
if (memHistory.length > 60) memHistory.shift();
|
|
|
|
drawChart('cpu-chart', cpuHistory, 'rgb(0, 212, 255)');
|
|
drawChart('mem-chart', memHistory, 'rgb(163, 113, 247)');
|
|
|
|
document.getElementById('last-update').textContent = 'Updated: ' + new Date().toLocaleTimeString();
|
|
}
|
|
|
|
async function loadProcesses() {
|
|
const data = await api('/processes');
|
|
const list = document.getElementById('process-list');
|
|
const processes = data.processes || [];
|
|
|
|
list.innerHTML = processes.slice(0, 12).map(p => `
|
|
<tr>
|
|
<td>${p.name}</td>
|
|
<td style="color: var(--cyan);">${p.cpu_percent?.toFixed(1) || 0}%</td>
|
|
<td style="color: var(--purple);">${p.memory_percent?.toFixed(1) || 0}%</td>
|
|
<td style="color: var(--text-dim);">${p.pid}</td>
|
|
</tr>
|
|
`).join('') || '<tr><td colspan="4">No processes</td></tr>';
|
|
}
|
|
|
|
async function loadAlerts() {
|
|
const data = await api('/alerts');
|
|
const list = document.getElementById('alerts-list');
|
|
const alerts = data.alerts || [];
|
|
|
|
if (alerts.length === 0) {
|
|
list.innerHTML = '<p style="color: var(--green);">No active alerts</p>';
|
|
} else {
|
|
list.innerHTML = alerts.map(a => `
|
|
<div class="alert-item ${a.severity}">
|
|
<strong>${a.type}</strong>: ${a.message}
|
|
</div>
|
|
`).join('');
|
|
}
|
|
}
|
|
|
|
function openNetdata() { window.open('/netdata-ui/', '_blank'); }
|
|
|
|
function refresh() {
|
|
loadStats();
|
|
loadProcesses();
|
|
loadAlerts();
|
|
}
|
|
|
|
refresh();
|
|
setInterval(refresh, 5000);
|
|
</script>
|
|
<script src="/shared/crt-engine.js"></script>
|
|
</body>
|
|
</html>
|