Compare commits

..

1 Commits

Author SHA1 Message Date
l5y 13b2ce9067 web: fix meshcore node misclassification (#748)
* web: fix meshcore node misclassification

* web: address review comments

* web: address review comments
2026-04-15 12:38:50 +02:00
4 changed files with 135 additions and 4 deletions
@@ -171,16 +171,20 @@ module PotatoMesh
return if existing
long_name = "#{protocol_display_label(protocol)} #{short_id}"
default_role = case protocol
when "meshcore" then "COMPANION"
else "CLIENT_HIDDEN"
end
heard_time = coerce_integer(heard_time)
inserted = false
with_busy_retry do
db.execute(
<<~SQL,
INSERT OR IGNORE INTO nodes(node_id,num,short_name,long_name,role,last_heard,first_heard)
VALUES (?,?,?,?,?,?,?)
INSERT OR IGNORE INTO nodes(node_id,num,short_name,long_name,role,last_heard,first_heard,protocol)
VALUES (?,?,?,?,?,?,?,?)
SQL
[node_id, node_num, short_id, long_name, "CLIENT_HIDDEN", heard_time, heard_time],
[node_id, node_num, short_id, long_name, default_role, heard_time, heard_time, protocol],
)
inserted = db.changes.positive?
end
@@ -147,6 +147,14 @@ module PotatoMesh
db.execute("CREATE INDEX IF NOT EXISTS idx_nodes_long_name ON nodes(long_name)")
end
end
# Backfill #747: ensure_unknown_node previously omitted the protocol
# column and hardcoded role=CLIENT_HIDDEN, causing meshcore placeholder
# nodes to be stored as meshtastic/CLIENT_HIDDEN. Fix both in one pass.
if node_columns.include?("protocol")
db.execute("UPDATE nodes SET protocol = 'meshcore' WHERE long_name LIKE 'Meshcore %' AND protocol = 'meshtastic'")
db.execute("UPDATE nodes SET role = 'COMPANION' WHERE protocol = 'meshcore' AND role = 'CLIENT_HIDDEN'")
end
end
message_table_exists = db.get_first_value(
+71 -1
View File
@@ -3022,7 +3022,7 @@ RSpec.describe "Potato Mesh Sinatra app" do
db.results_as_hash = true
row = db.get_first_row(
<<~SQL,
SELECT short_name, long_name, role, last_heard, first_heard
SELECT short_name, long_name, role, protocol, last_heard, first_heard
FROM nodes
WHERE node_id = ?
SQL
@@ -3032,11 +3032,81 @@ RSpec.describe "Potato Mesh Sinatra app" do
expect(row["short_name"]).to eq("ABCD")
expect(row["long_name"]).to eq("Meshtastic ABCD")
expect(row["role"]).to eq("CLIENT_HIDDEN")
expect(row["protocol"]).to eq("meshtastic")
expect(row["last_heard"]).to eq(reference_time.to_i)
expect(row["first_heard"]).to eq(reference_time.to_i)
end
end
it "stores meshcore protocol and COMPANION role for meshcore nodes" do
with_db do |db|
created = ensure_unknown_node(db, "!abcd1234", nil, heard_time: reference_time.to_i, protocol: "meshcore")
expect(created).to be_truthy
end
with_db(readonly: true) do |db|
db.results_as_hash = true
row = db.get_first_row(
<<~SQL,
SELECT short_name, long_name, role, protocol
FROM nodes
WHERE node_id = ?
SQL
["!abcd1234"],
)
expect(row["short_name"]).to eq("1234")
expect(row["long_name"]).to eq("Meshcore 1234")
expect(row["role"]).to eq("COMPANION")
expect(row["protocol"]).to eq("meshcore")
end
end
it "defaults to meshtastic protocol and CLIENT_HIDDEN role" do
with_db do |db|
created = ensure_unknown_node(db, "!beef0000", nil)
expect(created).to be_truthy
end
with_db(readonly: true) do |db|
db.results_as_hash = true
row = db.get_first_row(
<<~SQL,
SELECT role, protocol
FROM nodes
WHERE node_id = ?
SQL
["!beef0000"],
)
expect(row["role"]).to eq("CLIENT_HIDDEN")
expect(row["protocol"]).to eq("meshtastic")
end
end
it "falls back to CLIENT_HIDDEN for an unknown protocol" do
with_db do |db|
created = ensure_unknown_node(db, "!cafe9999", nil, protocol: "reticulum")
expect(created).to be_truthy
end
with_db(readonly: true) do |db|
db.results_as_hash = true
row = db.get_first_row(
<<~SQL,
SELECT role, protocol, long_name
FROM nodes
WHERE node_id = ?
SQL
["!cafe9999"],
)
expect(row["role"]).to eq("CLIENT_HIDDEN")
expect(row["protocol"]).to eq("reticulum")
expect(row["long_name"]).to eq("Reticulum 9999")
end
end
it "leaves timestamps nil when no receive time is provided" do
with_db do |db|
created = ensure_unknown_node(db, "!1111beef", nil)
+49
View File
@@ -258,4 +258,53 @@ RSpec.describe PotatoMesh::App::Database do
expect(column_names_for("instances")).to include("contact_link")
end
it "backfills misclassified meshcore placeholder nodes" do
SQLite3::Database.new(PotatoMesh::Config.db_path) do |db|
db.execute(<<~SQL)
CREATE TABLE nodes(
node_id TEXT PRIMARY KEY, num INTEGER, short_name TEXT, long_name TEXT,
role TEXT, last_heard INTEGER, first_heard INTEGER,
protocol TEXT NOT NULL DEFAULT 'meshtastic', synthetic BOOLEAN NOT NULL DEFAULT 0
)
SQL
db.execute("CREATE TABLE messages(id INTEGER PRIMARY KEY)")
# Misclassified meshcore placeholder (bug #747)
db.execute(
"INSERT INTO nodes(node_id, short_name, long_name, role, protocol) VALUES (?, ?, ?, ?, ?)",
["!aabb0001", "0001", "Meshcore 0001", "CLIENT_HIDDEN", "meshtastic"],
)
# Meshcore node where protocol self-healed but role did not
db.execute(
"INSERT INTO nodes(node_id, short_name, long_name, role, protocol) VALUES (?, ?, ?, ?, ?)",
["!aabb0002", "0002", "SomeNode", "CLIENT_HIDDEN", "meshcore"],
)
# Meshtastic node that should remain untouched
db.execute(
"INSERT INTO nodes(node_id, short_name, long_name, role, protocol) VALUES (?, ?, ?, ?, ?)",
["!aabb0003", "0003", "Meshtastic 0003", "CLIENT_HIDDEN", "meshtastic"],
)
end
harness_class.ensure_schema_upgrades
SQLite3::Database.new(PotatoMesh::Config.db_path, readonly: true) do |db|
db.results_as_hash = true
fixed_proto = db.get_first_row("SELECT protocol, role FROM nodes WHERE node_id = '!aabb0001'")
expect(fixed_proto["protocol"]).to eq("meshcore")
expect(fixed_proto["role"]).to eq("COMPANION")
fixed_role = db.get_first_row("SELECT protocol, role FROM nodes WHERE node_id = '!aabb0002'")
expect(fixed_role["protocol"]).to eq("meshcore")
expect(fixed_role["role"]).to eq("COMPANION")
untouched = db.get_first_row("SELECT protocol, role FROM nodes WHERE node_id = '!aabb0003'")
expect(untouched["protocol"]).to eq("meshtastic")
expect(untouched["role"]).to eq("CLIENT_HIDDEN")
end
end
end