Commit graph

1127 commits

Author SHA1 Message Date
c18f034997 improve agent status display and fix two pre-existing test failures (#83)
- Wrap agent name + icon inside the offline amber badge so it's clear
  what is offline on the device detail page
- Add "Agent:" label before the agent indicator for all states
- Fix TimeSeriesTest deadlock: switch to async: false since check_results
  is a TimescaleDB hypertable and concurrent chunk creation deadlocks
- Fix unicode property test: String.length/1 counts grapheme clusters,
  not code points; combining chars can make a 2-codepoint string have
  only 1 grapheme, legitimately failing the min: 2 validation

Reviewed-on: graham/towerops-web#83
2026-03-19 11:43:42 -05:00
2b50fc0732 fix agent check cascade and at-risk subscriber double-counting (#79)
## Summary

- **`list_checks_for_agent` cascade**: the agent query only matched checks with `agent_token_id` set directly on the check record — checks on devices inheriting their agent via site/org/global default were never sent to the Go agent. Now uses left joins with the full cascade (device assignment → site → org default → global default cloud poller), matching the pattern from `list_agent_polling_targets`.
- **At-risk summary double-counting**: `get_impact_summary` called `analyze_device_impact` per down AP, and each returned the full site subscriber count. With 3 APs down at a 147-sub site it showed 441 subs / $31k instead of 147 / $10k. Fixed by deduplicating impact per site. Same fix applied to `calculate_down_impact` in site impact summaries.

## Test plan

- [x] 3 new cascade tests for `list_checks_for_agent` (site, org default, global default)
- [x] 3 new tests for `get_impact_summary` (single site dedup, multi-site sum, zero case)
- [x] All 8,607 existing tests pass
- [ ] Deploy to staging, verify At Risk numbers on dashboard
- [ ] Verify Go agent receives DNS/HTTP checks via cascade

Reviewed-on: graham/towerops-web#79
2026-03-18 20:17:39 -05:00
d6f7dc570a simplify check skip logic to use runtime checks (#78)
## Summary
- Replace guard-clause-based `should_skip_snmp/service/ping_check?` with a single `should_skip_check?` function using runtime `in` operator checks
- Guard clauses with module attributes (`@service_check_types`) were not working in Dokku buildpack deployments despite code being correct locally
- Also update `CheckWorker` (dead code safety net) to check `device_has_effective_agent?`
- Use `Logger.info` for skip messages so they appear in staging logs

## Context
PR #77 introduced `device_has_effective_agent?/1` and updated all workers, but the guard-clause-based skip functions in `CheckExecutorWorker` weren't matching at runtime on staging despite passing all tests locally. The DNS check kept producing NXDOMAIN errors from Erlang's `:inet_res` resolver.

Root cause: The `when check_type in @service_check_types` guard clause compiled against module attributes appears to have stale evaluation in the Dokku Heroku buildpack release. Moving the check to a runtime function body fixes the issue.

## Test plan
- [x] All 230 worker tests pass
- [x] All 99 agents tests pass
- [x] Verified on staging: "Skipping check eceb3d51... (dns)" appears in info logs
- [x] No new NXDOMAIN results created after deploy

Reviewed-on: graham/towerops-web#78
2026-03-18 19:58:31 -05:00
f3cf1a9b3c skip all server-side checks when any agent is assigned (#77)
## Summary
- Add `device_has_effective_agent?/1` to Agents context that checks full cascade (device → site → org → global default) without distinguishing between cloud pollers and local agents
- Update check_executor_worker, device_poller_worker, and device_monitor_worker to use new function
- Phoenix never executes checks (DNS, ping, HTTP, TCP, SSL, SNMP) when any agent handles the device

## Problem
The global default cloud poller was not being checked in `should_phoenix_poll_device?/1`, causing Phoenix to run DNS/ping/service checks server-side even when the Go agent was handling the device. This produced duplicate results with NXDOMAIN errors from the Erlang DNS resolver.

## Test plan
- [x] 6 new tests for `device_has_effective_agent?/1` covering all cascade levels
- [x] Full test suite passes (8601 tests)
- [ ] Verify staging DNS check stops showing NXDOMAIN errors after deploy

Reviewed-on: graham/towerops-web#77
2026-03-18 17:29:36 -05:00
5493a83114 show decimal precision and units on graph y-axis (#76)
Chart.js was showing integer-rounded values like '2' instead of '2.1 ms'. Uses toFixed(1) in tick callbacks for proper decimal display with units.

Reviewed-on: graham/towerops-web#76
2026-03-18 15:49:20 -05:00
c4156ae334 fix graph 500 error on dns check page (#75)
Float.round/2 crashes on integer values from the status dataset. Adds integer guard clause to format_value/2.

Reviewed-on: graham/towerops-web#75
2026-03-18 15:33:29 -05:00
9d0c824aa5 add dual-axis graph for dns checks (#74)
DNS check graph now shows two datasets: Response Time (ms) on left y-axis and Status (Up/Down) on right y-axis with stepped green fill. Also adds space before units in chart labels.

Reviewed-on: graham/towerops-web#74
2026-03-18 15:13:31 -05:00
272d4bcdc9 fix: check state never persisted due to missing cast fields (#73)
Check.changeset/2 did not include state fields (current_state,
current_state_type, current_check_attempt, last_check_at, etc.) in
its cast list. Ecto silently dropped all state updates, so
update_check_state succeeded but wrote nothing to the database.

Added Check.state_changeset/2 for internal state machine updates,
keeping the regular changeset safe from user input. Updated
Monitoring.update_check_state/3 to use it directly.

Reviewed-on: graham/towerops-web#73
2026-03-18 14:54:52 -05:00
f05cc32ef6 fix dns/ping check execution and checks table alignment (#72)
## Summary
- **DNS checks**: `parse_server` returned a flat 5-tuple `{a,b,c,d,53}` instead of `{{a,b,c,d}, 53}` — the format `:inet_res` requires for nameservers. All DNS checks using a custom server failed with argument error.
- **Ping checks**: `System.cmd` `:timeout` option not supported on Elixir 1.19.4 (staging). Replaced with `Task.async`/`Task.yield` wrapper for cross-version compatibility.
- **Checks table UI**: Removed unnecessary flex div wrapper on status badge column that caused vertical misalignment with other table cells.

## Test plan
- [x] DNS executor tests pass (custom server resolution now succeeds)
- [x] Ping executor tests pass (no more ArgumentError)
- [x] Full test suite passes (8593 tests, 0 new failures)

Reviewed-on: graham/towerops-web#72
2026-03-18 14:05:37 -05:00
997bc46cbf fix: dns and ping checks not executing in production (#71)
## Summary

- **Ping checks never scheduled**: `ensure_default_ping_check` created check records but never called `schedule_check`, so auto-created ping checks never executed
- **Skipped checks permanently orphaned**: `CheckExecutorWorker` only rescheduled checks that actually executed — skipped checks (agent-assigned, SNMP disabled) broke the scheduling chain permanently
- **No recovery mechanism**: `JobHealthCheckWorker` only recovered `DeviceMonitorWorker` jobs, not `CheckExecutorWorker` jobs — any lost check job was gone forever

## Changes

- `monitoring.ex`: Call `schedule_check` after creating ping check in `ensure_default_ping_check`
- `check_executor_worker.ex`: Reschedule skipped-but-enabled checks so conditions are re-evaluated next cycle
- `job_health_check_worker.ex`: Recover orphaned `CheckExecutorWorker` jobs for enabled checks (runs every 10 min)
- `devices.ex`: Wrap `ensure_default_ping_check` in try/rescue for deadlock resilience

## Test plan

- [x] New tests: ping check scheduling on creation, no duplicate scheduling on idempotent call
- [x] Updated tests: skipped checks now assert rescheduling instead of orphaning
- [x] New tests: health check worker recovers orphaned check executor jobs, skips disabled checks
- [x] All 177 targeted tests pass
- [x] Compile clean with warnings-as-errors
- [x] Pre-commit hooks pass (credo, format)

Reviewed-on: graham/towerops-web#71
2026-03-18 08:43:37 -05:00
c8a092196f add auto ICMP ping check for all devices (#68)
## Summary

- Every device now automatically gets an ICMP ping check (`source_type: "auto_discovery"`) when created via `Monitoring.ensure_default_ping_check/1`
- When a device IP changes, the ping check host config is synced automatically
- Backfill migration inserts ICMP Ping checks for all existing devices that don't have one
- Fixes checkbox alignment in the Add Service Check modal (verify SSL / follow redirects)

## Test plan

- [x] 4 new tests for `ensure_default_ping_check/1` (create, idempotent, IP update, no-op)
- [x] 3 new tests for device lifecycle (auto-create on device create, sync on IP change, no-op when IP unchanged)
- [x] 13 existing tests updated to account for auto-created ping check
- [x] Full test suite passes (`mix precommit`)
- [ ] Verify backfill migration on staging
- [ ] Create device in UI, confirm ping check appears in service checks list
- [ ] Verify checkbox alignment in Add Service Check modal

Reviewed-on: graham/towerops-web#68
2026-03-17 17:55:23 -05:00
76b3dd6a93 add ssl/tls certificate expiration check type (#64)
add ssl check executor that connects via TLS, reads peer certificate
expiry, and returns OK/WARNING/CRITICAL based on configurable
warning_days threshold. includes protobuf config, liveview form fields,
agent channel encoding, oban worker dispatch, and unit tests.

Reviewed-on: graham/towerops-web#64
2026-03-17 16:14:22 -05:00
150ffb421d delegate service checks to agents and fix ui issues (#63)
- Skip HTTP/TCP/DNS checks in Phoenix when device is assigned to agent
- Agents now execute service checks from the device's network location
- Fix cancel button in service check modal (text link instead of button)
- Fix status badge alignment in checks table

Reviewed-on: graham/towerops-web#63
2026-03-17 15:43:31 -05:00
cd19db0680 fix-sticky-header-overlap (#60)
Reviewed-on: graham/towerops-web#60
2026-03-17 13:21:58 -05:00
fa2e09d7cc skip discovery for devices with snmp disabled (#58)
add early bail in DiscoveryWorker when snmp_enabled is false.
mark snmp_not_enabled and device_unresponsive as non-retryable
errors to prevent wasted retries.

Reviewed-on: graham/towerops-web#58
2026-03-17 11:16:33 -05:00
0c8bb35ef7 add query_helpers tests and fix all credo issues (#56)
- add tests for QueryHelpers.sanitize_like/1 (%, _, \ escaping)
- fix nesting depth in store_check_result, handle_check_state_change,
  graphql check resolver, checks controller, and ping executor
- fix cyclomatic complexity in build_check_protobuf by extracting
  check_type_config/2 function clauses
- fix formatting in api_docs_html template

Reviewed-on: graham/towerops-web#56
2026-03-17 10:43:57 -05:00
28e97ff5f0 add service checks REST API, GraphQL API, and ping executor (#52)
- REST CRUD at /api/v1/checks for HTTP, TCP, DNS, ping checks
- GraphQL queries (checks, check) and mutations (create/update/delete)
- ping executor using system ping with macOS/Linux parsing
- check config validation for ping type (requires host)
- API documentation updated with checks resource section

Reviewed-on: graham/towerops-web#52
2026-03-16 18:40:18 -05:00
d0e2ead923 hide rf links from sidebar navigation (#51)
Reviewed-on: graham/towerops-web#51
2026-03-16 17:54:57 -05:00
c6913685ee wire up service checks (HTTP/TCP/DNS) to remote agents (#50)
send check_jobs protobuf to agents during job dispatch cycle, handle
check_result messages back with state transitions and alert
creation/resolution. auto-assign checks to device's effective agent
token on creation, broadcast check changes via PubSub so connected
agents get immediate updates.

add edit/delete support for service checks on device show page with
form pre-population from existing check config. auto-fill TCP host
from device IP address.

includes validate_check_result/1 for protobuf validation and
list_checks_for_agent/2 for querying agent-assigned service checks.

also fixes pre-existing broken device_type_icon reference in device
index template.

Reviewed-on: graham/towerops-web#50
2026-03-16 17:25:02 -05:00
06c5d14455 ui/remove-device-type-column (#49)
Co-authored-by: Forgejo Actions <actions@git.mcintire.me>
Reviewed-on: graham/towerops-web#49
2026-03-16 16:38:00 -05:00
c5481c2cd3 add mobile token auth to GraphQL endpoint (#48)
Extend the GraphQL API and socket to accept mobile session tokens
alongside existing API tokens. Add OrganizationScope middleware
that extracts organization_id from query args for mobile users.
Add my_organizations query for mobile app org listing.

Reviewed-on: graham/towerops-web#48
2026-03-16 15:23:07 -05:00
0fa1fa800b fix/suppress-health-check-logs (#47)
Reviewed-on: graham/towerops-web#47
2026-03-16 15:22:12 -05:00
6321a974a9 TOW-44: Hide RF Links UI elements from network map (#45)
## Summary

Hide RF link-related UI elements from the network map page while keeping all backend code intact.

### Changes
- Commented out "View RF Links" button in network map node detail panel
- Commented out RF Link health legend (Good signal / Degraded / Critical) from map legend
- Backend topology.ex code untouched — `compute_rf_link_stats/1` and `enrich_edges_with_rf/2` remain
- No RF tab exists on the device show page, so no changes needed there

### Verification
- `mix compile --warnings-as-errors` — zero warnings
- `mix test` — 8490 tests pass, 0 failures

Paperclip issue: TOW-44

Reviewed-on: graham/towerops-web#45
2026-03-16 15:12:09 -05:00
72ab9274c7 fix-mobile-qr-layout (#44)
Co-authored-by: Forgejo Actions <actions@git.mcintire.me>
Reviewed-on: graham/towerops-web#44
2026-03-16 14:24:51 -05:00
8abf01ccb1 fix-mobile-qr-device-info-width (#43)
Co-authored-by: Forgejo Actions <actions@git.mcintire.me>
Reviewed-on: graham/towerops-web#43
2026-03-16 14:02:31 -05:00
33daaf14f0 fix: return raw session token instead of hash in mobile QR login (#42)
the complete_qr_login endpoint was returning session.token (the SHA256
hash stored in the database) instead of session.raw_token (the plaintext).
this caused all subsequent API calls and WebSocket connections to fail
with 401/REFUSED since the server would hash the already-hashed token.

adds a regression test that verifies the returned token is usable for
authenticated API requests.

Reviewed-on: graham/towerops-web#42
2026-03-16 13:59:52 -05:00
fa0f68439b remove-add-device-from-sites (#37)
Reviewed-on: graham/towerops-web#37
2026-03-15 18:46:16 -05:00
316725acfd fix: reduce left margin on integrations page (#34)
Reduced left padding from lg:px-8 (32px) to lg:pl-4 (16px) to bring
page content closer to the sidebar navigation.

Reviewed-on: graham/towerops-web#34
2026-03-15 18:29:03 -05:00
7c8731f86a fix: make sidebar sticky with header and resolve all credo issues (#32)
- Changed sidebar from fixed to sticky positioning
- Wrapped sidebar and main content in flex container
- Sidebar now sticks to top along with header when scrolling
- Removed unused Ecto.Query import from gps_sync.ex

Credo improvements (17 → 0 remaining):
- Replaced all length/1 checks with empty list comparisons
- Extracted helper functions to reduce nesting depth
- Split complex functions into smaller, testable units
- Applied 'with' statements for clearer control flow
- Refactored high-complexity functions (cyclomatic complexity)
- Files improved: storm_detector, alert_notification_worker,
  alert_digest_worker, gps_sync, statistics_sync, device_sync,
  site_sync, site_correlation, reports_live, topology,
  pagerduty/client, cn_maestro/sync

Reviewed-on: graham/towerops-web#32
2026-03-15 18:19:09 -05:00
42bfc8b9d2 paperclip/TOW-21-uisp-integration-phase1 (#31)
Reviewed-on: graham/towerops-web#31
2026-03-15 17:57:36 -05:00
acf70b3bee
fix: remove duplicate StormDetector child spec
The StormDetector was being added twice:
1. In the main children list (line 127)
2. In background_workers() (line 176)

This caused the supervisor to fail with 'duplicate_child_name' error.

Removed the duplicate from main children list since it's correctly
placed in background_workers() which also handles test env properly.
2026-03-15 16:05:30 -05:00
44fc017653
test: improve test coverage and code quality
- Update pre-commit config
- Add test coverage for URL validation, security modules, and monitoring executors
- Improve network map implementation and tests
- Add coverage for API controllers and LiveView endpoints
- Refactor code to fix Credo issues (reduce nesting, extract helper functions)
2026-03-15 14:49:55 -05:00
90af7ac0d1 feat: alert storm correlation & suppression (TOW-13)
- StormDetector GenServer: sliding-window per org, >10 alerts/min triggers
  suppression mode with single summary notification
- Site correlation: >3 devices at same site down within 120s creates
  single 'Site Outage' alert instead of N individual alerts
- Notification rate limiter: max 5 per user per 15min window, excess
  batched into digest via AlertDigestWorker
- PagerDuty client: retry with exponential backoff on 429, respects
  Retry-After header
- Batch maintenance checks: new Maintenance.devices_in_maintenance/1
  takes list of device_ids, returns MapSet (single query vs N queries)
- Migration: alert_storms, site_outages, notification_digests tables
  plus site_outage_id/storm_suppressed on alerts
- DeviceMonitorWorker integrates storm detection and site correlation
  before creating alerts
- Tests for StormDetector, SiteCorrelation, NotificationRateLimiter,
  and batch maintenance checks
2026-03-15 14:29:12 -05:00
b134a2e56a perf: LiveView event batching & PubSub optimization (TOW-26)
- Add 1.5s debounce to dashboard LiveView for alert events, preventing
  excessive re-renders during mass outages where hundreds of alerts fire
  simultaneously
- Replace global 'device:events' PubSub topic with org-scoped
  'device_events:org:{id}' topics across all broadcasters (device_poller_worker,
  sensor_change_detector, discovery) so LiveView processes only receive
  events for their tenant
- Update EventLogger GenServer to subscribe to per-org topics dynamically,
  with runtime subscription support for newly created orgs
- Add Organizations.list_organization_ids/0 for EventLogger bootstrap
- Increase default DB pool_size from 20 to 25 for production concurrent users

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-15 14:08:27 -05:00
9e35e1eef2 feat: WISP-tuned network map enhancements (TOW-16)
- Tower/device icons: role-based shapes (triangle for APs/wireless,
  diamond for switches, round-rect for routers, hexagon for firewalls)
- RF link overlays: edges colored by signal health (green/yellow/red)
  with bandwidth-proportional line thickness
- Enhanced hover tooltips: device-to-device name, SNR dB, bandwidth,
  signal health indicator
- Wireless client count badge on AP nodes
- Filter bar: All Links | Degraded Only | Sites with Alerts | Search
- Search with zoom-to-match and highlight
- Geographic layout toggle (when sites have lat/lng)
- Enhanced detail panel: RF stats (client count, signal health, SNR),
  View RF Links button for wireless devices
- Backend: compute_wireless_stats/compute_rf_link_stats enrich topology
- Site nodes include lat/lng for geographic layout
- Edge enrichment with RF signal health from SNR sensors
- Fix pre-existing compile error in notification_rate_limiter.ex
2026-03-15 14:06:06 -05:00
f13ff205ce
feat: add zoom in/out/fit controls to network map 2026-03-14 18:31:35 -05:00
a1c5d593dc
fix: network map label sizes, site layout, and missing links
- Increase node font from 11px to 13px, compound labels to 14px
- Replace cose physics layout with deterministic grid preset layout to eliminate site group overlap
- Fix nil source_interface_id crash in topology.ex find_existing_link/1 (was using == nil instead of is_nil/1)
- Fix nil device label in device_to_node/1 fallback to IP or "Unknown"
2026-03-14 18:27:54 -05:00
a7f3133a5b fix: scope agent token deletion to organization (IDOR) 2026-03-14 14:57:40 -05:00
1590f78bdc fix: input validation, SSRF, API hardening, and cookie security
- LIKE wildcard injection: sanitize %, _ in search queries (devices, sites, gaiia)
- Jason.decode! → Jason.decode with error handling for untrusted input
- inspect() leak: replace with generic error messages, log details server-side
- SSRF protection: URL validator blocks private IPs, localhost, non-HTTP schemes
- SSRF validation added to HTTP monitoring executor and integration credentials
- GraphQL complexity limits: always applied, not just in prod
- GraphQL introspection: also check GET query params, not just body
- Stripe webhook: explicit nil/empty checks for signature and body
- Cookie security: secure flag for session (prod), http_only+secure for remember_me
- Honeybadger API key: read from env var with fallback
- String.to_integer → Integer.parse with fallback for URL params
- String.to_atom → whitelist map for HTTP methods
- Gaiia webhook: remove secret_len and expected signature from log
- Admin API: add rate limiting pipeline
- to_atom_keys: per-key fallback instead of all-or-nothing rescue
2026-03-14 14:48:59 -05:00
7251930404
add mobile app websocket channel and improve QR linking UX
- add MobileSocket for token-authenticated mobile WebSocket connections
- add MobileChannel with org-level and device-level real-time events
- add /mobile/socket endpoint for iOS app connections
- add "Link Mobile App" to user dropdown menu for discoverability
- improve QR code page with numbered steps and clearer instructions
- add socket/channel tests (17 total)
2026-03-13 17:28:36 -05:00
326932b68e
Move "Choose one" label outside billing box
The exclusive billing provider indicator now appears above the bordered
selection box rather than inside it.
2026-03-13 16:52:39 -05:00
cc6dfab889
Move theme toggle from header to user settings
Remove the theme toggle dropdown from the nav bar and its component
definition. Theme selection already exists in Account Settings → Personal.
2026-03-13 16:51:33 -05:00
9104ad2948
fix: onboarding checklist alerting step guides to escalation policy
When alert routing is set to "builtin" (TowerOps) but no escalation
policy exists, the checklist item now reads "Create an escalation
policy" and links directly to the new escalation policy page instead
of looping back to general settings where the routing already looks
configured.
2026-03-13 16:41:23 -05:00
83e85952f1
fix: remove redundant sync schedule box from integrations cards
The sync schedule was shown twice on non-exclusive integration cards
(inline status row + separate box). Remove the box to match the
uniform inline display used by the billing card.
2026-03-13 16:33:46 -05:00
529a6138d1
fix: align sidebar bottom border with footer border
Adjust sidebar bottom section padding from py-3 to py-3.5 to align
the horizontal border with the footer border. Also remove unused
private functions from settings_live and fix credo warnings.
2026-03-13 16:29:44 -05:00
88323a2290 Deduplicate integrations: settings tab links to standalone page
- Replace inline integrations tab with link to /settings/integrations
- Remove ~1500 lines of duplicated integration UI from settings
- Remove duplicated integration event handlers from settings_live
- Standalone IntegrationsLive page remains the single source of truth
- Settings page down from ~2400 to ~900 lines
2026-03-13 16:16:35 -05:00
3a10682d8c Add dashboard setup checklist + onboarding progress
- Setup Progress bar on dashboard when devices exist but setup incomplete
- Tracks: devices added, billing connected, alert routing configured,
  on-call schedule created
- Each step links to the relevant setup page
- Dismissible with X button
- Progress bar shows visual completion percentage
- Optional steps labeled (billing, on-call)
2026-03-13 15:37:48 -05:00
9bae7a886a Add page descriptions + rename Schedules to On-Call
- Alerts: add description subtitle
- On-Call (was Schedules): renamed in sidebar + page title, add description
- Maintenance: add description subtitle
- Trace: add title + description (was missing both)
- Sidebar nav updated: Schedules → On-Call
- All pages now have contextual help text explaining their purpose
2026-03-13 15:30:39 -05:00
37314bd154 Add alert routing setting (builtin/pagerduty/both)
- New 'alert_routing' field on organizations (default: builtin)
- AlertNotificationWorker respects the setting:
  - builtin: only built-in escalation policies
  - pagerduty: only PagerDuty events
  - both: fire both (useful during migration)
- Radio button UI in Settings → General with visual cards
- Warning shown when PagerDuty selected but not configured
- Migration, schema validation, 6 tests
2026-03-13 15:26:25 -05:00
8e25034e69 Move sidebar-collapsed class to <html> element
LiveView DOM patching was stripping the client-side sidebar-collapsed
class from #sidebar-wrapper on every navigation. Fix: put the class
on document.documentElement which LiveView never touches.

- Sync script in <head> applies state before first paint (no flash)
- Hook click handler toggles on <html> + saves to localStorage
- CSS selectors updated to html.sidebar-collapsed
- Removed phx-click JS.toggle_class (hook handles toggle directly)
2026-03-13 14:28:38 -05:00