mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-07-06 01:42:11 +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).
131 lines
5.5 KiB
Ruby
131 lines
5.5 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
|
|
# Normalise a traceroute hop entry to a numeric node identifier.
|
|
#
|
|
# @param hop [Object] raw hop entry from the payload.
|
|
# @return [Integer, nil] coerced node ID or nil when the value is unusable.
|
|
def coerce_trace_node_id(hop)
|
|
case hop
|
|
when Integer
|
|
return hop
|
|
when Numeric
|
|
return hop.to_i
|
|
when String
|
|
trimmed = hop.strip
|
|
return nil if trimmed.empty?
|
|
return Integer(trimmed, 10) if trimmed.match?(/\A-?\d+\z/)
|
|
|
|
parts = canonical_node_parts(trimmed)
|
|
return parts[1] if parts
|
|
when Hash
|
|
candidate = hop["node_id"] || hop[:node_id] || hop["id"] || hop[:id] || hop["num"] || hop[:num]
|
|
return coerce_trace_node_id(candidate)
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
# Extract hop identifiers from a traceroute payload preserving order.
|
|
#
|
|
# @param hops_value [Object] raw hops array or path collection.
|
|
# @return [Array<Integer>] ordered list of coerced hop identifiers.
|
|
def normalize_trace_hops(hops_value)
|
|
return [] if hops_value.nil?
|
|
|
|
hop_entries = hops_value.is_a?(Array) ? hops_value : [hops_value]
|
|
hop_entries.filter_map { |entry| coerce_trace_node_id(entry) }
|
|
end
|
|
|
|
# Persist a traceroute observation and its hop path.
|
|
#
|
|
# @param db [SQLite3::Database] open database handle.
|
|
# @param payload [Hash] traceroute payload as produced by the ingestor.
|
|
# @param protocol_cache [Hash, nil] optional per-batch ingestor protocol cache.
|
|
# @return [void]
|
|
def insert_trace(db, payload, protocol_cache: nil)
|
|
return unless payload.is_a?(Hash)
|
|
|
|
trace_identifier = coerce_integer(payload["id"] || payload["packet_id"] || payload["packetId"])
|
|
trace_identifier ||= coerce_integer(payload["trace_id"])
|
|
request_id = coerce_integer(payload["request_id"] || payload["req"])
|
|
trace_identifier ||= request_id
|
|
|
|
now = Time.now.to_i
|
|
rx_time = coerce_integer(payload["rx_time"])
|
|
rx_time = now if rx_time.nil? || rx_time > now
|
|
rx_iso = string_or_nil(payload["rx_iso"]) || Time.at(rx_time).utc.iso8601
|
|
|
|
metrics = normalize_json_object(payload["metrics"]) || {}
|
|
src = coerce_integer(payload["src"] || payload["source"] || payload["from"])
|
|
dest = coerce_integer(payload["dest"] || payload["destination"] || payload["to"])
|
|
rssi = coerce_integer(payload["rssi"]) || coerce_integer(metrics["rssi"])
|
|
snr = coerce_float(payload["snr"]) || coerce_float(metrics["snr"])
|
|
elapsed_ms = coerce_integer(
|
|
payload["elapsed_ms"] ||
|
|
payload["latency_ms"] ||
|
|
metrics&.[]("elapsed_ms") ||
|
|
metrics&.[]("latency_ms") ||
|
|
metrics&.[]("latencyMs"),
|
|
)
|
|
ingestor = string_or_nil(payload["ingestor"])
|
|
protocol = resolve_record_protocol(db, payload, ingestor, cache: protocol_cache)
|
|
|
|
hops_value = payload.key?("hops") ? payload["hops"] : payload["path"]
|
|
hops = normalize_trace_hops(hops_value)
|
|
|
|
all_nodes = [src, dest, *hops].compact.uniq
|
|
all_nodes.each do |node|
|
|
ensure_unknown_node(db, node, node, heard_time: rx_time, protocol: protocol)
|
|
touch_node_last_seen(db, node, node, rx_time: rx_time, source: :trace)
|
|
end
|
|
|
|
with_busy_retry do
|
|
db.execute <<~SQL, [trace_identifier, request_id, src, dest, rx_time, rx_iso, rssi, snr, elapsed_ms, ingestor, protocol]
|
|
INSERT INTO traces(id, request_id, src, dest, rx_time, rx_iso, rssi, snr, elapsed_ms, ingestor, protocol)
|
|
VALUES(?,?,?,?,?,?,?,?,?,?,?)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
request_id=COALESCE(excluded.request_id,traces.request_id),
|
|
src=COALESCE(excluded.src,traces.src),
|
|
dest=COALESCE(excluded.dest,traces.dest),
|
|
rx_time=excluded.rx_time,
|
|
rx_iso=excluded.rx_iso,
|
|
rssi=COALESCE(excluded.rssi,traces.rssi),
|
|
snr=COALESCE(excluded.snr,traces.snr),
|
|
elapsed_ms=COALESCE(excluded.elapsed_ms,traces.elapsed_ms),
|
|
ingestor=COALESCE(NULLIF(traces.ingestor,''), excluded.ingestor),
|
|
protocol=COALESCE(NULLIF(traces.protocol,'meshtastic'), excluded.protocol)
|
|
SQL
|
|
|
|
trace_id = trace_identifier || db.last_insert_row_id
|
|
return unless trace_id
|
|
|
|
db.execute("DELETE FROM trace_hops WHERE trace_id = ?", [trace_id])
|
|
hops.each_with_index do |hop_id, index|
|
|
db.execute(
|
|
"INSERT INTO trace_hops(trace_id, hop_index, node_id) VALUES(?,?,?)",
|
|
[trace_id, index, hop_id],
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|