Revive project at new apartment — fixes, SSID update, gitignore
WiFi/firmware: - SSID updated to botnet in both node and ble_node config.h - Stale coordinator comment removed from node/config.h Coordinator fixes: - BLE-only nodes now visible in dashboard sidebar and node detail (build_nodes was querying beacon_events only; BLE nodes have no beacons) - ble_events and heartbeat_events added to pruning cycle - ble_events table added to ensure_schema() in dashboard - confidence column dropped from beacon_events (always 'high', never queried) - Ingestor commits batched per packet instead of per store call - wal_autocheckpoint=500 added to ingestor DB connection (writer was missing it) - python3 -u added to both service ExecStart lines (stdout was buffered, logs not appearing in journalctl) Repo hygiene: - .gitignore added (events.db, __pycache__, build artifacts) - events.db removed from git tracking - README updated: new apartment, node table, active investigations, BLE observations labelled as previous-location data, status checklist Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+89
-11
@@ -196,7 +196,9 @@ def build_nodes() -> list[dict]:
|
||||
now = datetime.datetime.utcnow().timestamp()
|
||||
if _nodes_cache and (now - _nodes_cache[0]) < _NODES_CACHE_TTL:
|
||||
return _nodes_cache[1]
|
||||
rows = query("""
|
||||
|
||||
# WiFi nodes: beacon-derived stats
|
||||
beacon_rows = query("""
|
||||
SELECT
|
||||
node_id,
|
||||
MAX(received_at) AS last_beacon,
|
||||
@@ -204,9 +206,9 @@ def build_nodes() -> list[dict]:
|
||||
MIN(received_at) AS first_seen
|
||||
FROM beacon_events
|
||||
GROUP BY node_id
|
||||
ORDER BY node_id ASC
|
||||
""")
|
||||
# Latest heartbeat per node
|
||||
|
||||
# Latest heartbeat per node — covers ALL node types (WiFi and BLE)
|
||||
hb_rows = query("""
|
||||
SELECT node_id, received_at AS last_heartbeat,
|
||||
uptime_ms, free_heap, wifi_rssi
|
||||
@@ -214,6 +216,21 @@ def build_nodes() -> list[dict]:
|
||||
WHERE id IN (SELECT MAX(id) FROM heartbeat_events GROUP BY node_id)
|
||||
""")
|
||||
hb_map = {r["node_id"]: r for r in hb_rows}
|
||||
|
||||
node_map: dict = {r["node_id"]: r for r in beacon_rows}
|
||||
|
||||
# BLE-only nodes have no beacon events — add them from heartbeat data
|
||||
for node_id, hb in hb_map.items():
|
||||
if node_id not in node_map:
|
||||
node_map[node_id] = {
|
||||
"node_id": node_id,
|
||||
"last_beacon": None,
|
||||
"total_events": 0,
|
||||
"first_seen": hb.get("last_heartbeat"),
|
||||
}
|
||||
|
||||
rows = sorted(node_map.values(), key=lambda r: r["node_id"])
|
||||
|
||||
for r in rows:
|
||||
hb = hb_map.get(r["node_id"], {})
|
||||
r["last_heartbeat"] = hb.get("last_heartbeat")
|
||||
@@ -221,11 +238,11 @@ def build_nodes() -> list[dict]:
|
||||
r["free_heap"] = hb.get("free_heap")
|
||||
r["wifi_rssi"] = hb.get("wifi_rssi")
|
||||
r["last_seen"] = r["last_heartbeat"] or r["last_beacon"]
|
||||
# Online if heartbeat within 30s; fall back to beacon within 60s if no heartbeat yet
|
||||
if r["last_heartbeat"]:
|
||||
r["status"] = _node_status(r["last_heartbeat"], threshold=30)
|
||||
else:
|
||||
r["status"] = _node_status(r["last_beacon"], threshold=60)
|
||||
|
||||
_nodes_cache = (now, rows)
|
||||
return rows
|
||||
|
||||
@@ -973,6 +990,7 @@ def build_search(q: str) -> dict:
|
||||
|
||||
|
||||
def build_node_detail(node_id: str) -> dict | None:
|
||||
# WiFi node path — beacon-derived stats
|
||||
rows = query("""
|
||||
SELECT
|
||||
node_id,
|
||||
@@ -988,9 +1006,51 @@ def build_node_detail(node_id: str) -> dict | None:
|
||||
WHERE node_id = ?
|
||||
GROUP BY node_id
|
||||
""", (node_id,))
|
||||
if not rows:
|
||||
return None
|
||||
detail = rows[0]
|
||||
|
||||
if rows:
|
||||
detail = rows[0]
|
||||
detail["node_type"] = "wifi"
|
||||
else:
|
||||
# BLE node path — stats from ble_events
|
||||
ble_rows = query("""
|
||||
SELECT
|
||||
node_id,
|
||||
MIN(received_at) AS first_seen,
|
||||
MAX(received_at) AS last_seen,
|
||||
COUNT(*) AS total_events,
|
||||
COUNT(DISTINCT mac) AS unique_macs,
|
||||
AVG(rssi) AS avg_rssi,
|
||||
MIN(rssi) AS min_rssi,
|
||||
MAX(rssi) AS max_rssi
|
||||
FROM ble_events
|
||||
WHERE node_id = ?
|
||||
GROUP BY node_id
|
||||
""", (node_id,))
|
||||
|
||||
if ble_rows:
|
||||
detail = ble_rows[0]
|
||||
else:
|
||||
# Node exists (heartbeats only) but no scan data yet
|
||||
hb_check = query(
|
||||
"SELECT node_id FROM heartbeat_events WHERE node_id = ? LIMIT 1", (node_id,)
|
||||
)
|
||||
if not hb_check:
|
||||
return None
|
||||
detail = {
|
||||
"node_id": node_id,
|
||||
"first_seen": None,
|
||||
"last_seen": None,
|
||||
"total_events": 0,
|
||||
"unique_macs": 0,
|
||||
"avg_rssi": None,
|
||||
"min_rssi": None,
|
||||
"max_rssi": None,
|
||||
}
|
||||
|
||||
detail["node_type"] = "ble"
|
||||
detail["unique_ssids"] = 0
|
||||
detail["unique_bssids"] = 0
|
||||
|
||||
hb_rows = query("""
|
||||
SELECT received_at AS last_heartbeat, uptime_ms, free_heap, wifi_rssi
|
||||
FROM heartbeat_events
|
||||
@@ -1005,8 +1065,8 @@ def build_node_detail(node_id: str) -> dict | None:
|
||||
if detail["last_heartbeat"]:
|
||||
detail["status"] = _node_status(detail["last_heartbeat"], threshold=30)
|
||||
else:
|
||||
detail["status"] = _node_status(detail["last_seen"], threshold=60)
|
||||
detail["avg_rssi"] = round(detail["avg_rssi"], 1) if detail["avg_rssi"] else None
|
||||
detail["status"] = _node_status(detail.get("last_seen"), threshold=60)
|
||||
detail["avg_rssi"] = round(detail["avg_rssi"], 1) if detail.get("avg_rssi") else None
|
||||
detail["events"] = build_events(limit=EVENT_CAP, node_id=node_id)
|
||||
return detail
|
||||
|
||||
@@ -1019,7 +1079,7 @@ def ensure_schema():
|
||||
CREATE TABLE IF NOT EXISTS beacon_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, received_at TEXT NOT NULL,
|
||||
node_id TEXT, node_ts INTEGER, ssid TEXT, bssid TEXT,
|
||||
rssi INTEGER, channel INTEGER, encryption TEXT, importance TEXT, confidence TEXT
|
||||
rssi INTEGER, channel INTEGER, encryption TEXT, importance TEXT
|
||||
)
|
||||
""")
|
||||
conn.execute("""
|
||||
@@ -1063,6 +1123,20 @@ def ensure_schema():
|
||||
rssi INTEGER
|
||||
)
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS ble_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
received_at TEXT NOT NULL,
|
||||
node_id TEXT,
|
||||
node_ts INTEGER,
|
||||
mac TEXT,
|
||||
addr_type TEXT,
|
||||
name TEXT,
|
||||
rssi INTEGER,
|
||||
mfr_id INTEGER,
|
||||
mfr_data TEXT
|
||||
)
|
||||
""")
|
||||
conn.executescript("""
|
||||
CREATE INDEX IF NOT EXISTS idx_beacon_received_at ON beacon_events (received_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_beacon_node_id ON beacon_events (node_id);
|
||||
@@ -1083,6 +1157,10 @@ def ensure_schema():
|
||||
CREATE INDEX IF NOT EXISTS idx_assoc_node_id ON assoc_events (node_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_assoc_src ON assoc_events (src);
|
||||
CREATE INDEX IF NOT EXISTS idx_assoc_bssid ON assoc_events (bssid);
|
||||
CREATE INDEX IF NOT EXISTS idx_ble_received_at ON ble_events (received_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_ble_node_id ON ble_events (node_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ble_mac ON ble_events (mac);
|
||||
CREATE INDEX IF NOT EXISTS idx_ble_mfr_id ON ble_events (mfr_id);
|
||||
""")
|
||||
conn.commit()
|
||||
|
||||
@@ -1094,7 +1172,7 @@ CHECKPOINT_INTERVAL_SECS = 30 * 60 # force WAL checkpoint every 30 minutes
|
||||
def prune_old_events() -> None:
|
||||
"""Delete events older than HOT_DAYS from all event tables, then checkpoint."""
|
||||
cutoff = _hot_cutoff()
|
||||
tables = ["beacon_events", "probe_events", "deauth_events", "assoc_events"]
|
||||
tables = ["beacon_events", "probe_events", "deauth_events", "assoc_events", "ble_events", "heartbeat_events"]
|
||||
with get_conn() as conn:
|
||||
for table in tables:
|
||||
conn.execute(f"DELETE FROM {table} WHERE received_at < ?", (cutoff,))
|
||||
|
||||
Reference in New Issue
Block a user