170 lines
5.7 KiB
HTML
170 lines
5.7 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>FR24 Monitoring</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: monospace; background: #0d1117; color: #c9d1d9; padding: 20px; }
|
|
|
|
header { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; }
|
|
header a { color: #58a6ff; text-decoration: none; font-size: 13px; }
|
|
header a:hover { text-decoration: underline; }
|
|
header h1 { font-size: 18px; color: #c9d1d9; }
|
|
#last-updated { margin-left: auto; font-size: 12px; color: #8b949e; }
|
|
|
|
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 32px; }
|
|
|
|
.card {
|
|
background: rgba(22,27,34,0.9);
|
|
border: 1px solid #30363d;
|
|
border-radius: 8px;
|
|
padding: 16px 20px;
|
|
}
|
|
.card .label { font-size: 11px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; }
|
|
.card .value { font-size: 32px; font-weight: bold; }
|
|
.card .unit { font-size: 13px; color: #8b949e; margin-top: 4px; }
|
|
|
|
.green { color: #3fb950; }
|
|
.yellow { color: #d29922; }
|
|
.red { color: #f85149; }
|
|
.blue { color: #58a6ff; }
|
|
|
|
.card { border-left: 3px solid #30363d; }
|
|
.card.green { border-left-color: #3fb950; }
|
|
.card.yellow { border-left-color: #d29922; }
|
|
.card.red { border-left-color: #f85149; }
|
|
.card.blue { border-left-color: #58a6ff; }
|
|
|
|
h2 { font-size: 14px; color: #8b949e; margin-bottom: 12px; }
|
|
|
|
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
th { text-align: left; color: #8b949e; padding: 6px 10px; border-bottom: 1px solid #21262d; }
|
|
td { padding: 6px 10px; border-bottom: 1px solid #161b22; color: #c9d1d9; }
|
|
tr:hover td { background: rgba(255,255,255,0.03); }
|
|
|
|
#error-msg { color: #f85149; font-size: 13px; margin-bottom: 16px; display: none; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<a href="/">← Live Map</a>
|
|
<h1>📊 FR24 Monitoring</h1>
|
|
<span id="last-updated">—</span>
|
|
</header>
|
|
|
|
<div id="error-msg"></div>
|
|
|
|
<div class="cards">
|
|
<div class="card" id="card-disk">
|
|
<div class="label">Disk Usage</div>
|
|
<div class="value" id="val-disk">—</div>
|
|
<div class="unit">percent used</div>
|
|
</div>
|
|
<div class="card blue" id="card-db">
|
|
<div class="label">DB Size</div>
|
|
<div class="value blue" id="val-db">—</div>
|
|
<div class="unit">megabytes</div>
|
|
</div>
|
|
<div class="card" id="card-lag">
|
|
<div class="label">Capture Lag</div>
|
|
<div class="value" id="val-lag">—</div>
|
|
<div class="unit">seconds since last packet</div>
|
|
</div>
|
|
<div class="card" id="card-tput">
|
|
<div class="label">Throughput</div>
|
|
<div class="value" id="val-tput">—</div>
|
|
<div class="unit">packets / 5 min</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h2>Last 20 measurements</h2>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Time</th>
|
|
<th>Disk %</th>
|
|
<th>DB Size (MB)</th>
|
|
<th>Capture Lag (s)</th>
|
|
<th>Throughput (5m)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="history-body"></tbody>
|
|
</table>
|
|
|
|
<script>
|
|
function colorClass(type, value) {
|
|
if (value == null) return '';
|
|
if (type === 'disk') {
|
|
if (value < 70) return 'green';
|
|
if (value < 80) return 'yellow';
|
|
return 'red';
|
|
}
|
|
if (type === 'lag') {
|
|
if (value < 60) return 'green';
|
|
if (value < 300) return 'yellow';
|
|
return 'red';
|
|
}
|
|
if (type === 'tput') {
|
|
return value > 0 ? 'green' : 'red';
|
|
}
|
|
return 'blue';
|
|
}
|
|
|
|
function applyCard(cardId, valId, type, value, display) {
|
|
const card = document.getElementById(cardId);
|
|
const val = document.getElementById(valId);
|
|
const cls = colorClass(type, value);
|
|
val.textContent = display;
|
|
val.className = 'value ' + cls;
|
|
card.className = 'card ' + (type === 'db' ? 'blue' : cls);
|
|
}
|
|
|
|
async function refresh() {
|
|
try {
|
|
const res = await fetch('/api/monitoring/status');
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
const data = await res.json();
|
|
|
|
const errEl = document.getElementById('error-msg');
|
|
errEl.style.display = 'none';
|
|
|
|
const m = data.latest;
|
|
if (m) {
|
|
applyCard('card-disk', 'val-disk', 'disk', m.disk_pct, m.disk_pct != null ? m.disk_pct + '%' : '—');
|
|
applyCard('card-db', 'val-db', 'db', m.db_size_mb, m.db_size_mb != null ? m.db_size_mb.toFixed(1) : '—');
|
|
applyCard('card-lag', 'val-lag', 'lag', m.capture_lag_sec, m.capture_lag_sec != null ? m.capture_lag_sec : '—');
|
|
applyCard('card-tput', 'val-tput', 'tput', m.throughput_5min, m.throughput_5min != null ? m.throughput_5min : '—');
|
|
}
|
|
|
|
const tbody = document.getElementById('history-body');
|
|
tbody.innerHTML = '';
|
|
for (const row of (data.history || [])) {
|
|
const tr = document.createElement('tr');
|
|
const ts = row.collected_at ? new Date(row.collected_at).toLocaleTimeString() : '—';
|
|
tr.innerHTML = `
|
|
<td>${ts}</td>
|
|
<td class="${colorClass('disk', row.disk_pct)}">${row.disk_pct != null ? row.disk_pct + '%' : '—'}</td>
|
|
<td class="blue">${row.db_size_mb != null ? row.db_size_mb.toFixed(1) : '—'}</td>
|
|
<td class="${colorClass('lag', row.capture_lag_sec)}">${row.capture_lag_sec != null ? row.capture_lag_sec : '—'}</td>
|
|
<td class="${colorClass('tput', row.throughput_5min)}">${row.throughput_5min != null ? row.throughput_5min : '—'}</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
}
|
|
|
|
document.getElementById('last-updated').textContent = 'Updated: ' + new Date().toLocaleTimeString();
|
|
} catch (e) {
|
|
const errEl = document.getElementById('error-msg');
|
|
errEl.textContent = '⚠ ' + e.message;
|
|
errEl.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
refresh();
|
|
setInterval(refresh, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|