1 Commits
Author SHA1 Message Date
agessaman e4f882d6b3 feat(analytics): break packet stats, routes and neighbour links down per radio
On a Fabric node the analytics endpoints merged every radio together, with
one transmission per logical packet and nothing to tell a reception on one
radio from the other. Every packet row already records its ingress radio
and each radio that sent it, so the split needs no new data.

The attribution rules get_airtime_buckets introduced move into shared
helpers: packet_carriers() says which stored radio ids a row occupied, and
RadioResolver maps them onto the configured radios (one radio owns every
row; with two, an unknown id is unattributed, never guessed).
get_airtime_buckets now uses them with no change in output.

With two or more radio profiles:

- /packet_stats adds radios[] (received, duplicates, dropped, physical
  transmissions, average RSSI/SNR) and unattributed counts. The legacy
  totals come from the same grouped pass, so the 24 h aggregate the
  WebSocket broadcasts does not gain a second scan. Totals still count
  one transmission per packet.
- /route_stats adds the route mix of each radio's receptions, plus
  originated for packets no radio received.
- /radio_packet_rates (new) returns receptions and transmissions per
  radio per bucket, grouped in SQL on columns idx_packets_airtime covers.
  A week-long window is an index-only scan instead of a Python loop over
  every row: 0.17 s against 1.11 s for the airtime loop on a synthetic
  1.55M-row table.
- /neighbor_links gives each link radios[], its stats as each radio heard
  it. The tracker keeps them beside the merged figures without changing
  its keys or entry count.
- /neighbor_link_history rows carry rx_radio_id, and the endpoint takes a
  radio_id filter and a by_radio bucket split.

A single-radio node gets exactly the answers it had: the legacy queries
still run for it and every new field is omitted.
2026-09-12 16:53:10 -07:00