auto-sync: 2026-04-19 22:00:01
This commit is contained in:
@@ -151,7 +151,7 @@ services:
|
||||
api:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "test -f /tmp/monitoring-ready && pgrep -f main.py > /dev/null || exit 1"]
|
||||
test: ["CMD-SHELL", "test -f /tmp/monitoring-ready || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
@@ -1,169 +1,260 @@
|
||||
<!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; }
|
||||
<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; }
|
||||
|
||||
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; }
|
||||
body {
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
font-size: 14px;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 32px; }
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.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; }
|
||||
header a {
|
||||
color: #58a6ff;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
header a:hover { opacity: 1; }
|
||||
|
||||
.green { color: #3fb950; }
|
||||
.yellow { color: #d29922; }
|
||||
.red { color: #f85149; }
|
||||
.blue { color: #58a6ff; }
|
||||
header h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
.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; }
|
||||
#last-updated {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: #6e7681;
|
||||
}
|
||||
|
||||
h2 { font-size: 14px; color: #8b949e; margin-bottom: 12px; }
|
||||
.metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
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); }
|
||||
.card {
|
||||
background: #161b22;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#error-msg { color: #f85149; font-size: 13px; margin-bottom: 16px; display: none; }
|
||||
</style>
|
||||
.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="/">← Live Map</a>
|
||||
<h1>📊 FR24 Monitoring</h1>
|
||||
<a href="/">← Карта</a>
|
||||
<h1>FR24 Monitoring</h1>
|
||||
<span id="last-updated">—</span>
|
||||
</header>
|
||||
|
||||
<div id="error-msg"></div>
|
||||
<div id="error-banner"></div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card" id="card-disk">
|
||||
<div class="metrics">
|
||||
<div class="card">
|
||||
<div class="label">Disk Usage</div>
|
||||
<div class="value" id="val-disk">—</div>
|
||||
<div class="unit">percent used</div>
|
||||
<div class="value neutral" id="m-disk">—</div>
|
||||
<div class="unit">%</div>
|
||||
</div>
|
||||
<div class="card blue" id="card-db">
|
||||
<div class="card">
|
||||
<div class="label">DB Size</div>
|
||||
<div class="value blue" id="val-db">—</div>
|
||||
<div class="unit">megabytes</div>
|
||||
<div class="value neutral" id="m-db">—</div>
|
||||
<div class="unit" id="m-db-unit">MB</div>
|
||||
</div>
|
||||
<div class="card" id="card-lag">
|
||||
<div class="card">
|
||||
<div class="label">Capture Lag</div>
|
||||
<div class="value" id="val-lag">—</div>
|
||||
<div class="unit">seconds since last packet</div>
|
||||
<div class="value neutral" id="m-lag">—</div>
|
||||
<div class="unit">сек</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 class="card">
|
||||
<div class="label">Throughput 5min</div>
|
||||
<div class="value neutral" id="m-tput">—</div>
|
||||
<div class="unit">пакетов / 5 мин</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>
|
||||
<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 colorClass(type, value) {
|
||||
if (value == null) return '';
|
||||
if (type === 'disk') {
|
||||
if (value < 70) return 'green';
|
||||
if (value < 80) return 'yellow';
|
||||
return 'red';
|
||||
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'];
|
||||
}
|
||||
if (type === 'lag') {
|
||||
if (value < 60) return 'green';
|
||||
if (value < 300) return 'yellow';
|
||||
return 'red';
|
||||
|
||||
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; }
|
||||
}
|
||||
if (type === 'tput') {
|
||||
return value > 0 ? 'green' : 'red';
|
||||
|
||||
function setMetric(id, value, colorClass) {
|
||||
const el = document.getElementById(id);
|
||||
el.textContent = value;
|
||||
el.className = 'value ' + (colorClass || 'neutral');
|
||||
}
|
||||
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 : '—');
|
||||
}
|
||||
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');
|
||||
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);
|
||||
if (!rows || !rows.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="color:#6e7681;padding:16px 12px">Нет данных</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
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';
|
||||
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('');
|
||||
}
|
||||
}
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, 30000);
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user