261 lines
6.3 KiB
HTML
261 lines
6.3 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>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
background: #0d1117;
|
|
color: #c9d1d9;
|
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
font-size: 14px;
|
|
min-height: 100vh;
|
|
padding: 24px;
|
|
}
|
|
|
|
header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
margin-bottom: 28px;
|
|
}
|
|
|
|
header a {
|
|
color: #58a6ff;
|
|
text-decoration: none;
|
|
font-size: 13px;
|
|
opacity: 0.8;
|
|
transition: opacity 0.15s;
|
|
}
|
|
header a:hover { opacity: 1; }
|
|
|
|
header h1 {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #e6edf3;
|
|
}
|
|
|
|
#last-updated {
|
|
margin-left: auto;
|
|
font-size: 12px;
|
|
color: #6e7681;
|
|
}
|
|
|
|
.metrics {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.card {
|
|
background: #161b22;
|
|
border: 1px solid #30363d;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
}
|
|
|
|
.card .label {
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: #6e7681;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.card .value {
|
|
font-size: 32px;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
}
|
|
|
|
.card .unit {
|
|
font-size: 13px;
|
|
color: #6e7681;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.green { color: #3fb950; }
|
|
.yellow { color: #d29922; }
|
|
.red { color: #f85149; }
|
|
.neutral { color: #e6edf3; }
|
|
|
|
h2 {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #8b949e;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.table-wrap {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 13px;
|
|
}
|
|
|
|
thead th {
|
|
text-align: left;
|
|
padding: 8px 12px;
|
|
color: #6e7681;
|
|
font-weight: 500;
|
|
border-bottom: 1px solid #21262d;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
tbody tr {
|
|
border-bottom: 1px solid #161b22;
|
|
transition: background 0.1s;
|
|
}
|
|
tbody tr:hover { background: #161b22; }
|
|
|
|
tbody td {
|
|
padding: 7px 12px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
#error-banner {
|
|
display: none;
|
|
background: #2d1b1b;
|
|
border: 1px solid #f85149;
|
|
border-radius: 6px;
|
|
color: #f85149;
|
|
padding: 10px 16px;
|
|
margin-bottom: 20px;
|
|
font-size: 13px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<a href="/">← Карта</a>
|
|
<h1>FR24 Monitoring</h1>
|
|
<span id="last-updated">—</span>
|
|
</header>
|
|
|
|
<div id="error-banner"></div>
|
|
|
|
<div class="metrics">
|
|
<div class="card">
|
|
<div class="label">Disk Usage</div>
|
|
<div class="value neutral" id="m-disk">—</div>
|
|
<div class="unit">%</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="label">DB Size</div>
|
|
<div class="value neutral" id="m-db">—</div>
|
|
<div class="unit" id="m-db-unit">MB</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="label">Capture Lag</div>
|
|
<div class="value neutral" id="m-lag">—</div>
|
|
<div class="unit">сек</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="label">Throughput 5min</div>
|
|
<div class="value neutral" id="m-tput">—</div>
|
|
<div class="unit">пакетов / 5 мин</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h2>История (последние 20)</h2>
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Время</th>
|
|
<th>Disk %</th>
|
|
<th>DB Size</th>
|
|
<th>Lag (сек)</th>
|
|
<th>Throughput</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="history-body">
|
|
<tr><td colspan="5" style="color:#6e7681;padding:16px 12px">Загрузка…</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<script>
|
|
function diskColor(v) { return v < 70 ? 'green' : v <= 80 ? 'yellow' : 'red'; }
|
|
function lagColor(v) { return v < 60 ? 'green' : v <= 300 ? 'yellow' : 'red'; }
|
|
|
|
function fmtDb(mb) {
|
|
if (mb >= 1024) return [(mb / 1024).toFixed(2), 'GB'];
|
|
return [mb.toFixed(1), 'MB'];
|
|
}
|
|
|
|
function fmtTime(iso) {
|
|
try {
|
|
return new Date(iso).toLocaleString('ru-RU', {
|
|
month: '2-digit', day: '2-digit',
|
|
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
|
});
|
|
} catch { return iso; }
|
|
}
|
|
|
|
function setMetric(id, value, colorClass) {
|
|
const el = document.getElementById(id);
|
|
el.textContent = value;
|
|
el.className = 'value ' + (colorClass || 'neutral');
|
|
}
|
|
|
|
function renderLatest(d) {
|
|
setMetric('m-disk', d.disk_pct, diskColor(d.disk_pct));
|
|
const [dbVal, dbUnit] = fmtDb(d.db_size_mb);
|
|
setMetric('m-db', dbVal);
|
|
document.getElementById('m-db-unit').textContent = dbUnit;
|
|
setMetric('m-lag', d.capture_lag_sec, lagColor(d.capture_lag_sec));
|
|
setMetric('m-tput', d.throughput_5min.toLocaleString());
|
|
}
|
|
|
|
function renderHistory(rows) {
|
|
const tbody = document.getElementById('history-body');
|
|
if (!rows || !rows.length) {
|
|
tbody.innerHTML = '<tr><td colspan="5" style="color:#6e7681;padding:16px 12px">Нет данных</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = rows.map(r => {
|
|
const [dbVal, dbUnit] = fmtDb(r.db_size_mb);
|
|
return `<tr>
|
|
<td>${fmtTime(r.collected_at)}</td>
|
|
<td class="${diskColor(r.disk_pct)}">${r.disk_pct}%</td>
|
|
<td>${dbVal} ${dbUnit}</td>
|
|
<td class="${lagColor(r.capture_lag_sec)}">${r.capture_lag_sec}</td>
|
|
<td>${r.throughput_5min.toLocaleString()}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
async function refresh() {
|
|
const banner = document.getElementById('error-banner');
|
|
try {
|
|
const res = await fetch('/api/monitoring/status');
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const data = await res.json();
|
|
banner.style.display = 'none';
|
|
if (data.latest) renderLatest(data.latest);
|
|
if (data.history) renderHistory(data.history);
|
|
document.getElementById('last-updated').textContent =
|
|
'Обновлено: ' + new Date().toLocaleTimeString('ru-RU');
|
|
} catch (e) {
|
|
banner.textContent = 'Ошибка загрузки данных: ' + e.message;
|
|
banner.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
refresh();
|
|
setInterval(refresh, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|