auto-sync: 2026-04-19 18:50:01

This commit is contained in:
Stream
2026-04-19 18:50:01 +03:00
parent 6de2909f2a
commit 6b195a493d
4 changed files with 254 additions and 0 deletions

View File

@@ -330,6 +330,36 @@ def aircraft_detail(icao24: str):
return err(str(e))
@app.get("/monitoring")
def monitoring_page():
return send_from_directory("/app/static", "monitoring.html")
@app.get("/api/monitoring/status")
def monitoring_status():
"""Return latest monitoring metrics + last 20 rows history."""
try:
latest = query_one(
"""
SELECT id, collected_at, disk_pct, db_size_mb, capture_lag_sec, throughput_5min
FROM fr24.monitoring_metrics
ORDER BY id DESC
LIMIT 1
"""
)
history = query(
"""
SELECT id, collected_at, disk_pct, db_size_mb, capture_lag_sec, throughput_5min
FROM fr24.monitoring_metrics
ORDER BY id DESC
LIMIT 20
"""
)
return ok({"latest": latest, "history": history})
except Exception as e:
return err(str(e))
@app.get("/api/tracks")
def tracks():
"""Track points as GeoJSON LineStrings, filtered by time and optional bbox."""

View File

@@ -17,6 +17,8 @@
#hud h2 { font-size: 13px; color: #58a6ff; margin-bottom: 6px; }
#hud .stat { font-size: 12px; color: #8b949e; }
#hud .stat span { color: #c9d1d9; }
#hud a { font-size: 12px; color: #58a6ff; text-decoration: none; display: block; margin-top: 8px; }
#hud a:hover { text-decoration: underline; }
#popup-box {
position: fixed; bottom: 20px; right: 20px; z-index: 1000;
background: rgba(13,17,23,0.92); border: 1px solid #30363d;
@@ -38,6 +40,7 @@
<div class="stat">Aircraft: <span id="ac-count"></span></div>
<div class="stat">Updated: <span id="last-update"></span></div>
<div class="stat" id="status-line"></div>
<a href="/monitoring">📊 Monitoring</a>
</div>
<div id="popup-box">

View File

@@ -0,0 +1,169 @@
<!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>

View File

@@ -24,6 +24,24 @@ DB_DSN = (
INTERVAL = int(os.environ.get("MONITORING_INTERVAL_SECONDS", "60"))
DISK_WARN_PCT = 80
LAG_WARN_SEC = 300 # 5 minutes
METRICS_KEEP = 100 # rows to retain in monitoring_metrics
def ensure_metrics_table(conn):
"""Create fr24.monitoring_metrics if it doesn't exist."""
with conn.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS fr24.monitoring_metrics (
id SERIAL PRIMARY KEY,
collected_at TIMESTAMPTZ DEFAULT now(),
disk_pct INTEGER,
db_size_mb FLOAT,
capture_lag_sec INTEGER,
throughput_5min INTEGER
)
""")
conn.commit()
log.info("monitoring_metrics table ready")
def get_disk_usage() -> str:
@@ -69,6 +87,27 @@ def run_checks():
)
throughput = cur.fetchone()[0]
# Write metrics row
disk_pct_int = int(disk_pct_str) if disk_pct_str not in ("?",) else None
db_size_mb = db_bytes / (1024 ** 2)
cur.execute(
"""
INSERT INTO fr24.monitoring_metrics (disk_pct, db_size_mb, capture_lag_sec, throughput_5min)
VALUES (%s, %s, %s, %s)
""",
(disk_pct_int, round(db_size_mb, 2), lag_sec, int(throughput)),
)
# Prune old rows, keep last METRICS_KEEP
cur.execute(
"""
DELETE FROM fr24.monitoring_metrics
WHERE id NOT IN (
SELECT id FROM fr24.monitoring_metrics ORDER BY id DESC LIMIT %s
)
""",
(METRICS_KEEP,),
)
cur.close()
conn.close()
@@ -99,6 +138,19 @@ def run_checks():
def main():
log.info("FR24 monitoring started (interval=%ds)", INTERVAL)
# Ensure metrics table exists before first check
for attempt in range(10):
try:
conn = psycopg2.connect(DB_DSN, connect_timeout=5)
conn.autocommit = False
ensure_metrics_table(conn)
conn.close()
break
except Exception as e:
log.warning("waiting for DB (%d/10): %s", attempt + 1, e)
time.sleep(3)
# Signal readiness
open("/tmp/monitoring-ready", "w").close()