Adds a vendor-agnostic radio-state extractor that lets the frequency-
aware rules actually fire on production data without per-vendor
plumbing for each profile.
- Towerops.Snmp.RadioState — pure module that maps a frequency in MHz
to the corresponding 802.11 channel across 2.4 GHz, 5 GHz, and 6 GHz
bands; reads the freshest "frequency" sensor (already emitted by
Cambium/MikroTik/AirOS/Mimosa/Exalt/etc. profiles) and persists the
AP-level current_frequency_mhz, current_channel, last_radio_seen_at
fields onto snmp_devices.
- DevicePollerWorker now calls RadioState.update_from_sensors/1 after
each sensor poll so frequency-aware rules read a single canonical
source instead of joining sensors at query time.
- Towerops.Recommendations.Rules.OwnFleetChannelConflict — new rule
that needs no neighbor-scan collection: detects when 2+ of our own
APs at the same site share a channel (self-interference). One
insight per (site, channel) with a clickable list of conflicting
APs and their channel widths. Critical when 3+ APs share a channel.
- Insight.@valid_types extended with own_fleet_channel_conflict.
- UI: amber card showing channel, frequency, AP count, and the linked
list of conflicting devices.
Wired into the existing hourly RecommendationsRunWorker. LLM
enrichment automatically applies via the Phase 1 worker.
Two new multi-source rules using data we already collect, both wired
into the existing hourly RecommendationsRunWorker.
SectorOverload — fires when a Preseem-monitored AP has <25% free
airtime AND active subscribers. Critical urgency when free airtime
drops below 15%, OR when free airtime <25% AND QoE <50. Reuses the
airtime/subscriber/QoE data already pulled by PreseemSyncWorker so
no new collection is needed. UI: orange evidence card with free
airtime, subscriber count, QoE score, and AP model.
CpeRealign — fires when a wireless client has BOTH signal_strength
<=-78 dBm AND SNR <=18 dB seen in the last 2 hours. This is the
classic alignment / obstruction signature, distinct from the
existing single-metric wireless_signal_weak / wireless_snr_low
alerts produced by WirelessInsightWorker. One insight per (AP, CPE)
pair via a metadata.dedup_key. Critical when signal <=-88 OR SNR
<=10. UI: rose evidence card with signal, SNR, TX/RX rate, distance,
hostname.
Insights.insert_insight_if_new/1 dedup logic now prefers a
metadata.dedup_key over device_id when explicitly set, allowing
per-CPE insights without collapsing multiple CPEs on the same AP
into a single insight. Existing rules without dedup_key are
unaffected.
Insight.@valid_types extended with sector_overload and cpe_realign.
LLM enrichment automatically applies to both new types via the
existing Phase 1 worker — no extra wiring.
Also adds k8s/secrets.yaml to .gitignore so operators can drop a
local Secret manifest with real values, kubectl apply manually, and
never accidentally commit it. Documented in k8s/README.md.
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).
Adds an optional plain-language summary and recommended action to every
active insight. A new Oban cron worker runs every 5 minutes, picks up
unenriched insights, and asks the configured LLM to restate the finding
and suggest one specific action grounded in the structured metadata.
- Migration: adds llm_summary, recommended_action, llm_model,
llm_enriched_at columns to preseem_insights
- Towerops.LLM context with swappable behaviour; real client uses Req
with the standard Req.Test plug for test isolation
- MockClient + InsightPrompt module (builds chat messages, parses JSON
with code-fence stripping and a raw-text fallback)
- Worker logs and skips on rate limits / missing key / parse errors so
insights still render without an AI summary when the LLM is unavailable
- Optional towerops-llm k8s secret with example template; deployment.yaml
references it as optional in both init and main containers
- UI renders summary + recommended action callout on /insights and on
the org-level insights page
Adds 8 tests targeting AgentChannel branches missed by existing tests:
- resolve_any_stuck_down_alerts: device_down and device_up stuck alerts
resolved on next successful poll
- handle_in result with :PING job_type (deprecated SnmpResult path used
by older agents) creates a monitoring check without crashing
- handle_live_poll_result with malformed job_id (missing reply_topic)
hits the warning catch-all branch
- build_mikrotik_job is included in the initial dispatch when the device
has a discovered SNMP profile whose manufacturer is MikroTik or whose
sys_descr mentions RouterOS, and excluded when mikrotik_enabled is false
- process_additional_polling_data path with extra OIDs runs without
crashing the channel
- Adds TcpExecutor receive timeout test using a server that accepts
but never responds, exercising recv timeout branch
- Adds Preseem.get_device_qoe_data tests covering both no-APs and
with-metrics paths through the AP query + metric formatting
Adds 4 tests that drive the nil → nil fallbacks in
load_single_sensor_chart_data, load_storage_chart_data,
load_interface_errors_chart_data, and load_interface_traffic_chart_data
by navigating to graph routes with non-existent IDs.
Adds direct call to MobileController.get_device with a snmp_device that
has interfaces — drives the Enum.map over interfaces in
format_equipment_details that previously had no fixture coverage.
Adds tests across five files:
- Towerops.Monitoring.CheckQuery (full coverage of all composable filter
fragments via inspect+Repo.all)
- ToweropsWeb.Admin.SecurityLive.Index change_filter event for both
permanent and temporary filter values
- Towerops.Agents.AgentCache cleanup_expired handle_info path that
prunes stale device entries while preserving :global_default
- ToweropsWeb.Api.V1.AgentReleaseWebhookController happy path
(notified/skipped/version) and error path (Codeberg 503)
- ToweropsWeb.DeviceLive.Index reorder_device / reorder_site
unauthorized branches via cross-org device/site fixtures
Adds an untagged loopback test (127.0.0.1) so System.cmd /
Task.async / Task.yield branches in run_ping/3 are covered in CI
runs that exclude :network-tagged tests.