auto-sync: 2026-04-19 22:00:01

This commit is contained in:
Stream
2026-04-19 22:00:01 +03:00
parent cb6a779be7
commit c810516afc
2 changed files with 219 additions and 128 deletions

View File

@@ -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

View File

@@ -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>