Add Presence tab, RSSI chart, heartbeat; fix SSE flood, chart timezone, N+1 clients query
This commit is contained in:
+129
-7
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user