mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-07-06 09:52:02 +02:00
512b4f157b
* Fix regression where Meshcore chat senders show as Meshtastic * Address review feedback for protocol misclassification fix - ingest.rb: exclude wrapper ``protocol`` key from /api/nodes batch-limit count so the documented 1000-node maximum still applies after the Python ingestor started stamping protocol at the wrapper level. - Drop plan-file references from production and test comments per the repo guidelines; the why is already explained inline. * Address protocol-fallback review feedback - Neighbor placeholder now inherits the source node's protocol from the surrounding /api/neighbors entry, so the badge tracks the radio the peer lives on instead of collapsing to the neutral "Unknown" label (review item #1). - resolve_record_protocol logs one warn_log line when an explicit protocol stamp is rejected as malformed, making misbehaving custom protocol adapters visible in the operator log instead of silently falling back (review item #3). * Extract buildNodePlaceholder helper for testability The neighbor placeholder logic in main.js lives inside an untested closure, so codecov reported the protocol-propagation lines as uncovered. Extract the small placeholder builder into long-link-router so it can be unit tested directly; the closure-internal call site stays trivial (one factory call + one fallback call).
145 lines
5.7 KiB
Ruby
145 lines
5.7 KiB
Ruby
# Copyright © 2025-26 l5yth & contributors
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
# frozen_string_literal: true
|
|
|
|
module PotatoMesh
|
|
module App
|
|
module DataProcessing
|
|
# Persist a neighbours snapshot for a single reporting node.
|
|
#
|
|
# @param db [SQLite3::Database] open database handle.
|
|
# @param payload [Hash] inbound NeighborInfo payload.
|
|
# @param protocol_cache [Hash, nil] optional per-batch ingestor protocol cache.
|
|
# @return [void]
|
|
def insert_neighbors(db, payload, protocol_cache: nil)
|
|
return unless payload.is_a?(Hash)
|
|
|
|
now = Time.now.to_i
|
|
rx_time = coerce_integer(payload["rx_time"])
|
|
rx_time = now if rx_time.nil? || rx_time > now
|
|
|
|
raw_node_id = payload["node_id"] || payload["node"] || payload["from_id"]
|
|
raw_node_num = coerce_integer(payload["node_num"]) || coerce_integer(payload["num"])
|
|
|
|
canonical_parts = canonical_node_parts(raw_node_id, raw_node_num)
|
|
if canonical_parts
|
|
node_id, node_num, = canonical_parts
|
|
else
|
|
node_id = string_or_nil(raw_node_id)
|
|
canonical = normalize_node_id(db, node_id || raw_node_num)
|
|
node_id = canonical if canonical
|
|
if node_id&.start_with?("!") && raw_node_num.nil?
|
|
begin
|
|
node_num = Integer(node_id.delete_prefix("!"), 16)
|
|
rescue ArgumentError
|
|
node_num = nil
|
|
end
|
|
else
|
|
node_num = raw_node_num
|
|
end
|
|
end
|
|
|
|
return unless node_id
|
|
|
|
node_id = "!#{node_id.delete_prefix("!").downcase}" if node_id.start_with?("!")
|
|
|
|
ingestor = string_or_nil(payload["ingestor"])
|
|
protocol = resolve_record_protocol(db, payload, ingestor, cache: protocol_cache)
|
|
|
|
ensure_unknown_node(db, node_id || node_num, node_num, heard_time: rx_time, protocol: protocol)
|
|
touch_node_last_seen(db, node_id || node_num, node_num, rx_time: rx_time, source: :neighborinfo)
|
|
|
|
neighbor_entries = []
|
|
neighbors_payload = payload["neighbors"]
|
|
neighbors_list = neighbors_payload.is_a?(Array) ? neighbors_payload : []
|
|
|
|
neighbors_list.each do |neighbor|
|
|
next unless neighbor.is_a?(Hash)
|
|
|
|
neighbor_ref = neighbor["neighbor_id"] || neighbor["node_id"] || neighbor["nodeId"] || neighbor["id"]
|
|
neighbor_num = coerce_integer(
|
|
neighbor["neighbor_num"] || neighbor["node_num"] || neighbor["nodeId"] || neighbor["id"],
|
|
)
|
|
|
|
canonical_neighbor = canonical_node_parts(neighbor_ref, neighbor_num)
|
|
if canonical_neighbor
|
|
neighbor_id, neighbor_num, = canonical_neighbor
|
|
else
|
|
neighbor_id = string_or_nil(neighbor_ref)
|
|
canonical_neighbor_id = normalize_node_id(db, neighbor_id || neighbor_num)
|
|
neighbor_id = canonical_neighbor_id if canonical_neighbor_id
|
|
if neighbor_id&.start_with?("!") && neighbor_num.nil?
|
|
begin
|
|
neighbor_num = Integer(neighbor_id.delete_prefix("!"), 16)
|
|
rescue ArgumentError
|
|
neighbor_num = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
next unless neighbor_id
|
|
|
|
neighbor_id = "!#{neighbor_id.delete_prefix("!").downcase}" if neighbor_id.start_with?("!")
|
|
|
|
entry_rx_time = coerce_integer(neighbor["rx_time"]) || rx_time
|
|
entry_rx_time = now if entry_rx_time && entry_rx_time > now
|
|
snr = coerce_float(neighbor["snr"])
|
|
|
|
ensure_unknown_node(db, neighbor_id || neighbor_num, neighbor_num, heard_time: entry_rx_time, protocol: protocol)
|
|
|
|
neighbor_entries << [neighbor_id, snr, entry_rx_time, ingestor, protocol]
|
|
end
|
|
|
|
with_busy_retry do
|
|
db.transaction do
|
|
if neighbor_entries.empty?
|
|
db.execute("DELETE FROM neighbors WHERE node_id = ?", [node_id])
|
|
else
|
|
expected_neighbors = neighbor_entries.map(&:first).uniq
|
|
existing_neighbors = db.execute(
|
|
"SELECT neighbor_id FROM neighbors WHERE node_id = ?",
|
|
[node_id],
|
|
).flatten
|
|
stale_neighbors = existing_neighbors - expected_neighbors
|
|
stale_neighbors.each_slice(500) do |slice|
|
|
placeholders = slice.map { "?" }.join(",")
|
|
db.execute(
|
|
"DELETE FROM neighbors WHERE node_id = ? AND neighbor_id IN (#{placeholders})",
|
|
[node_id] + slice,
|
|
)
|
|
end
|
|
end
|
|
|
|
neighbor_entries.each do |neighbor_id, snr_value, heard_time, reporter_id, proto|
|
|
db.execute(
|
|
<<~SQL,
|
|
INSERT INTO neighbors(node_id, neighbor_id, snr, rx_time, ingestor, protocol)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(node_id, neighbor_id) DO UPDATE SET
|
|
snr = excluded.snr,
|
|
rx_time = excluded.rx_time,
|
|
ingestor = COALESCE(NULLIF(neighbors.ingestor,''), excluded.ingestor),
|
|
protocol = COALESCE(NULLIF(neighbors.protocol,'meshtastic'), excluded.protocol)
|
|
SQL
|
|
[node_id, neighbor_id, snr_value, heard_time, reporter_id, proto],
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|