From 62b096c60174f70141fb39bcb4c45f71e87556f8 Mon Sep 17 00:00:00 2001 From: l5y <220195275+l5yth@users.noreply.github.com> Date: Sun, 14 Sep 2025 09:12:57 +0200 Subject: [PATCH] Add first_heard timestamp (#23) --- data/migrations/001_add_first_heard.sql | 47 +++++++++++++++++++++++++ data/nodes.py | 8 +++-- data/nodes.sql | 1 + test/test_nodes_serialization.py | 15 ++++++-- web/app.rb | 8 +++-- 5 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 data/migrations/001_add_first_heard.sql diff --git a/data/migrations/001_add_first_heard.sql b/data/migrations/001_add_first_heard.sql new file mode 100644 index 0000000..1e67757 --- /dev/null +++ b/data/migrations/001_add_first_heard.sql @@ -0,0 +1,47 @@ +-- Add first_heard column to nodes table and backfill with last_heard +BEGIN TRANSACTION; + +CREATE TABLE nodes_new ( + node_id TEXT PRIMARY KEY, + num INTEGER, + short_name TEXT, + long_name TEXT, + macaddr TEXT, + hw_model TEXT, + role TEXT, + public_key TEXT, + is_unmessagable BOOLEAN, + is_favorite BOOLEAN, + hops_away INTEGER, + snr REAL, + last_heard INTEGER, + first_heard INTEGER NOT NULL, + battery_level REAL, + voltage REAL, + channel_utilization REAL, + air_util_tx REAL, + uptime_seconds INTEGER, + position_time INTEGER, + location_source TEXT, + latitude REAL, + longitude REAL, + altitude REAL +); + +INSERT INTO nodes_new(node_id,num,short_name,long_name,macaddr,hw_model,role,public_key,is_unmessagable,is_favorite, + hops_away,snr,last_heard,first_heard,battery_level,voltage,channel_utilization,air_util_tx,uptime_seconds, + position_time,location_source,latitude,longitude,altitude) +SELECT node_id,num,short_name,long_name,macaddr,hw_model,role,public_key,is_unmessagable,is_favorite, + hops_away,snr,last_heard,last_heard,battery_level,voltage,channel_utilization,air_util_tx,uptime_seconds, + position_time,location_source,latitude,longitude,altitude +FROM nodes; + +DROP TABLE nodes; +ALTER TABLE nodes_new RENAME TO nodes; + +CREATE INDEX IF NOT EXISTS idx_nodes_last_heard ON nodes(last_heard); +CREATE INDEX IF NOT EXISTS idx_nodes_hw_model ON nodes(hw_model); +CREATE INDEX IF NOT EXISTS idx_nodes_latlon ON nodes(latitude, longitude); + +COMMIT; + diff --git a/data/nodes.py b/data/nodes.py index 980a5ff..3425894 100644 --- a/data/nodes.py +++ b/data/nodes.py @@ -27,6 +27,7 @@ def upsert_node(node_id, n): user = _get(n, "user") or {} met = _get(n, "deviceMetrics") or {} pos = _get(n, "position") or {} + lh = _get(n, "lastHeard") row = ( node_id, _get(n, "num"), @@ -40,7 +41,8 @@ def upsert_node(node_id, n): _get(n, "isFavorite"), _get(n, "hopsAway"), _get(n, "snr"), - _get(n, "lastHeard"), + lh, + lh, _get(met, "batteryLevel"), _get(met, "voltage"), _get(met, "channelUtilization"), @@ -55,9 +57,9 @@ def upsert_node(node_id, n): conn.execute( """ INSERT INTO nodes(node_id,num,short_name,long_name,macaddr,hw_model,role,public_key,is_unmessagable,is_favorite, - hops_away,snr,last_heard,battery_level,voltage,channel_utilization,air_util_tx,uptime_seconds, + hops_away,snr,last_heard,first_heard,battery_level,voltage,channel_utilization,air_util_tx,uptime_seconds, position_time,location_source,latitude,longitude,altitude) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(node_id) DO UPDATE SET num=excluded.num, short_name=excluded.short_name, long_name=excluded.long_name, macaddr=excluded.macaddr, hw_model=excluded.hw_model, role=excluded.role, public_key=excluded.public_key, is_unmessagable=excluded.is_unmessagable, diff --git a/data/nodes.sql b/data/nodes.sql index a163230..f8816d5 100644 --- a/data/nodes.sql +++ b/data/nodes.sql @@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS nodes ( hops_away INTEGER, snr REAL, last_heard INTEGER, -- unix seconds + first_heard INTEGER NOT NULL, -- first time we saw node battery_level REAL, voltage REAL, channel_utilization REAL, diff --git a/test/test_nodes_serialization.py b/test/test_nodes_serialization.py index d02f087..785defa 100644 --- a/test/test_nodes_serialization.py +++ b/test/test_nodes_serialization.py @@ -42,13 +42,24 @@ def test_upsert_node_handles_position(tmp_path): longitude: float = 13.4 altitude: float = 34.0 - n = {"num": 7, "position": Position()} + n = {"num": 7, "position": Position(), "lastHeard": 100} nodes.upsert_node("node1", n) nodes.conn.commit() row = nodes.conn.execute( - "SELECT latitude FROM nodes WHERE node_id=?", ("node1",) + "SELECT latitude, first_heard, last_heard FROM nodes WHERE node_id=?", + ("node1",), ).fetchone() assert row is not None assert row[0] == 52.5 + assert row[1] == 100 + assert row[2] == 100 + + n["lastHeard"] = 200 + nodes.upsert_node("node1", n) + nodes.conn.commit() + row2 = nodes.conn.execute( + "SELECT first_heard, last_heard FROM nodes WHERE node_id=?", ("node1",) + ).fetchone() + assert row2 == (100, 200) finally: os.chdir(cwd) diff --git a/web/app.rb b/web/app.rb index 450b594..8c724b9 100644 --- a/web/app.rb +++ b/web/app.rb @@ -44,6 +44,7 @@ def upsert_node(db, node_id, n) met = n["deviceMetrics"] || {} pos = n["position"] || {} role = user["role"] || "CLIENT" + lh = n["lastHeard"] row = [ node_id, n["num"], @@ -57,7 +58,8 @@ def upsert_node(db, node_id, n) n["isFavorite"], n["hopsAway"], n["snr"], - n["lastHeard"], + lh, + lh, met["batteryLevel"], met["voltage"], met["channelUtilization"], @@ -71,9 +73,9 @@ def upsert_node(db, node_id, n) ] db.execute <<~SQL, row INSERT INTO nodes(node_id,num,short_name,long_name,macaddr,hw_model,role,public_key,is_unmessagable,is_favorite, - hops_away,snr,last_heard,battery_level,voltage,channel_utilization,air_util_tx,uptime_seconds, + hops_away,snr,last_heard,first_heard,battery_level,voltage,channel_utilization,air_util_tx,uptime_seconds, position_time,location_source,latitude,longitude,altitude) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(node_id) DO UPDATE SET num=excluded.num, short_name=excluded.short_name, long_name=excluded.long_name, macaddr=excluded.macaddr, hw_model=excluded.hw_model, role=excluded.role, public_key=excluded.public_key, is_unmessagable=excluded.is_unmessagable,