auto-sync: 2026-04-19 18:50:01
This commit is contained in:
@@ -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."""
|
||||
|
||||
@@ -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">
|
||||
|
||||
169
tasks/flightradar24/frontend/static/monitoring.html
Normal file
169
tasks/flightradar24/frontend/static/monitoring.html
Normal 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>
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user