diff --git a/web/lib/potato_mesh/application/data_processing.rb b/web/lib/potato_mesh/application/data_processing.rb index d46aa30..aa0fc8a 100644 --- a/web/lib/potato_mesh/application/data_processing.rb +++ b/web/lib/potato_mesh/application/data_processing.rb @@ -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 diff --git a/web/lib/potato_mesh/application/database.rb b/web/lib/potato_mesh/application/database.rb index ce0a9d5..7f9f762 100644 --- a/web/lib/potato_mesh/application/database.rb +++ b/web/lib/potato_mesh/application/database.rb @@ -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( diff --git a/web/spec/app_spec.rb b/web/spec/app_spec.rb index 7aab983..778770b 100644 --- a/web/spec/app_spec.rb +++ b/web/spec/app_spec.rb @@ -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) diff --git a/web/spec/database_spec.rb b/web/spec/database_spec.rb index b5864f6..f64505b 100644 --- a/web/spec/database_spec.rb +++ b/web/spec/database_spec.rb @@ -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