towerops/priv/repo/migrations/20260509214247_create_wireless_neighbor_scans.exs
Graham McIntire f41f5fa1aa feat(insights): AP frequency-change recommendation rule
When a wireless AP has a strong same-channel neighbor (RSSI >= -75 dBm)
and a measurably cleaner alternative channel exists in the same band,
the system now emits an ap_frequency_change insight with a specific
recommended channel and the top interfering BSSIDs as evidence.

- New schema wireless_neighbor_scans (append-only) storing observed
  neighboring APs per device: bssid, ssid, channel, frequency_mhz,
  channel_width_mhz, rssi_dbm, is_own_fleet, seen_at
- New columns on snmp_devices: current_channel, current_frequency_mhz,
  current_channel_width_mhz, last_radio_seen_at — populated by vendor
  SNMP profiles for APs that expose wireless OIDs
- Towerops.Recommendations.Rules.FrequencyChange — pure rule that scores
  each candidate channel using sum-of-linear-power over the last 24h of
  neighbor scans, recommends the cleanest if it beats the current by at
  least 6 dB. Critical urgency when current score >= 100.
- Towerops.Workers.RecommendationsRunWorker — hourly Oban cron, iterates
  organizations and runs all rules, uses insert_insight_if_new/1 for
  idempotent upsert
- LiveView /insights renders a purple frequency-change card with current
  vs recommended channel, dB-cleaner badge, and a top-offender list
  (BSSID, SSID, RSSI, own-fleet flag)
- LLM enrichment automatically applies to the new insight type

Also fixes two pre-existing flaky tests that surfaced in CI:

- test/towerops_web/live/agent_live_test.exs:567 — now sets an explicit
  Req.Test transport_error stub for ReleaseChecker and uses Req.Test.allow
  so the LiveView process inherits it. Previously relied on absence of a
  stub, which leaked across tests and produced different behavior under
  full-suite parallel runs.
- test/towerops_web/telemetry_test.exs:140 — relaxes the "no log" check
  to refute the specific messages the function under test would emit,
  rather than asserting an empty capture_log/1 buffer (which catches
  stray logs from concurrent tests, e.g. Postgrex sandbox disconnects).
2026-05-09 16:56:19 -05:00

31 lines
1.1 KiB
Elixir

defmodule Towerops.Repo.Migrations.CreateWirelessNeighborScans do
use Ecto.Migration
def change do
create table(:wireless_neighbor_scans, primary_key: false) do
add :id, :binary_id, primary_key: true
add :device_id, references(:devices, type: :binary_id, on_delete: :delete_all), null: false
add :organization_id,
references(:organizations, type: :binary_id, on_delete: :delete_all),
null: false
add :bssid, :string, null: false
add :ssid, :string
add :channel, :integer
add :frequency_mhz, :integer
add :channel_width_mhz, :integer
add :rssi_dbm, :integer
add :is_own_fleet, :boolean, default: false, null: false
add :seen_at, :utc_datetime, null: false
timestamps(type: :utc_datetime, updated_at: false)
end
create index(:wireless_neighbor_scans, [:device_id, :seen_at])
create index(:wireless_neighbor_scans, [:device_id, :channel])
create index(:wireless_neighbor_scans, [:bssid])
create index(:wireless_neighbor_scans, [:organization_id, :seen_at])
end
end