Add Presence tab, RSSI chart, heartbeat; fix SSE flood, chart timezone, N+1 clients query

This commit is contained in:
bot
2026-04-03 21:50:41 +03:00
parent a73aae21ad
commit cbef4143ee
5 changed files with 376 additions and 9 deletions
+129 -7
View File
@@ -170,14 +170,18 @@ def build_clients() -> list[dict]:
GROUP BY src_mac
ORDER BY times_seen DESC
""")
# Attach probed SSIDs and OUI vendor info to each row
# Fetch all probed SSIDs in one query, group in Python — avoids N+1
ssid_rows = query("""
SELECT DISTINCT src_mac, ssid FROM probe_events
WHERE src_mac IS NOT NULL AND ssid IS NOT NULL AND ssid != ''
ORDER BY src_mac, ssid
""")
ssids_by_mac: dict = {}
for s in ssid_rows:
ssids_by_mac.setdefault(s["src_mac"], []).append(s["ssid"])
for r in rows:
ssid_rows = query("""
SELECT DISTINCT ssid FROM probe_events
WHERE src_mac = ? AND ssid IS NOT NULL AND ssid != ''
ORDER BY ssid
""", (r["src_mac"],))
r["probed_ssids"] = [s["ssid"] for s in ssid_rows]
r["probed_ssids"] = ssids_by_mac.get(r["src_mac"], [])
vendor, randomized = oui_lookup(r["src_mac"])
r["vendor"] = vendor
r["randomized"] = randomized
@@ -237,6 +241,119 @@ def build_cross_node() -> dict:
return {"node_ids": node_ids, "networks": result}
def build_presence() -> dict:
now = datetime.datetime.utcnow()
cutoff_1h = (now - datetime.timedelta(hours=1)).isoformat(timespec="seconds")
cutoff_24h = (now - datetime.timedelta(hours=24)).isoformat(timespec="seconds")
# ── Present now: real MACs seen 2+ times in the last hour ────────────────
present_rows = query("""
SELECT src_mac,
COUNT(*) AS times_seen,
MAX(rssi) AS best_rssi,
ROUND(AVG(rssi), 1) AS avg_rssi,
MAX(received_at) AS last_seen,
COUNT(DISTINCT node_id) AS node_count
FROM probe_events
WHERE src_mac IS NOT NULL AND received_at > ?
GROUP BY src_mac
HAVING COUNT(*) > 1
ORDER BY last_seen DESC
""", (cutoff_1h,))
present = []
for r in present_rows:
vendor, randomized = oui_lookup(r["src_mac"])
if not randomized:
r["vendor"] = vendor
present.append(r)
# ── New arrivals: first seen in last 24h ─────────────────────────────────
new_mac_rows = query("""
SELECT src_mac,
MIN(received_at) AS first_seen,
MAX(rssi) AS best_rssi,
COUNT(*) AS times_seen,
COUNT(DISTINCT node_id) AS node_count
FROM probe_events
WHERE src_mac IS NOT NULL
GROUP BY src_mac
HAVING MIN(received_at) > ?
ORDER BY first_seen DESC
LIMIT 100
""", (cutoff_24h,))
new_macs = []
for r in new_mac_rows:
vendor, randomized = oui_lookup(r["src_mac"])
r["vendor"] = vendor
r["randomized"] = randomized
new_macs.append(r)
new_networks = query("""
SELECT bssid,
MAX(ssid) AS ssid,
MIN(received_at) AS first_seen,
MAX(rssi) AS best_rssi,
MAX(channel) AS channel,
MAX(encryption) AS encryption,
COUNT(DISTINCT node_id) AS node_count
FROM beacon_events
WHERE bssid IS NOT NULL
GROUP BY bssid
HAVING MIN(received_at) > ?
ORDER BY first_seen DESC
LIMIT 100
""", (cutoff_24h,))
# ── Regulars: seen on 2+ distinct calendar days ───────────────────────────
reg_mac_rows = query("""
SELECT src_mac,
COUNT(DISTINCT date(received_at)) AS days_seen,
MIN(received_at) AS first_seen,
MAX(received_at) AS last_seen,
MAX(rssi) AS best_rssi,
COUNT(*) AS times_seen,
COUNT(DISTINCT node_id) AS node_count
FROM probe_events
WHERE src_mac IS NOT NULL
GROUP BY src_mac
HAVING COUNT(DISTINCT date(received_at)) >= 2
ORDER BY days_seen DESC, times_seen DESC
LIMIT 50
""")
reg_macs = []
for r in reg_mac_rows:
vendor, randomized = oui_lookup(r["src_mac"])
r["vendor"] = vendor
r["randomized"] = randomized
reg_macs.append(r)
reg_networks = query("""
SELECT bssid,
MAX(ssid) AS ssid,
COUNT(DISTINCT date(received_at)) AS days_seen,
MIN(received_at) AS first_seen,
MAX(received_at) AS last_seen,
MAX(rssi) AS best_rssi,
MAX(channel) AS channel,
MAX(encryption) AS encryption,
COUNT(DISTINCT node_id) AS node_count
FROM beacon_events
WHERE bssid IS NOT NULL
GROUP BY bssid
HAVING COUNT(DISTINCT date(received_at)) >= 2
ORDER BY days_seen DESC, best_rssi DESC
LIMIT 50
""")
return {
"present": present,
"new_macs": new_macs,
"new_networks": new_networks,
"reg_macs": reg_macs,
"reg_networks": reg_networks,
}
def build_node_detail(node_id: str) -> dict | None:
rows = query("""
SELECT
@@ -349,6 +466,11 @@ async def api_cross_node():
return build_cross_node()
@app.get("/api/presence")
async def api_presence():
return build_presence()
@app.get("/api/rssi-history")
async def api_rssi_history(bssid: str, hours: int = 2):
hours = min(max(hours, 1), 48)