Commit graph

674 commits

Author SHA1 Message Date
4b4bb2668f
feat: add billing tab to organization settings
Implements Phase 6 of Stripe billing integration:
- Add Billing tab to organization settings page
- Display subscription status, device usage, and estimated costs
- "Upgrade to Paid Plan" button for free tier orgs
- "Manage Billing" button to access Stripe Customer Portal
- Billing information panel showing subscription details
- Support for dark mode and responsive design

Also fixes organization name truncation in top navigation:
- Long organization names now show with ellipsis instead of being cut off
- Added max-w-xs constraint and truncate class to org switcher button
2026-03-06 11:08:05 -06:00
b3cff9afbb
stripe and email after signup 2026-03-06 10:07:27 -06:00
07911dfa3e
perf(tests): make AgentChannel debounce delay configurable for faster tests
- Add :agent_channel_debounce_ms config (500ms prod, 50ms test)
- Reduce test timeouts from 700-1000ms to 150ms
- Performance improvements:
  * agent receives updated job list: 1567ms → 664ms (57% faster)
  * agent result for deleted device: 1059ms → 620ms (41% faster)
  * rapid assignment changes: 744ms → <500ms
  * debounces rapid changes: 613ms → <200ms
- Total time saved: ~2 seconds across affected tests
- No production behavior changed
2026-03-06 07:21:52 -06:00
b0fcfb800f
fix(logs): add Plug.Telemetry metadata filter to suppress health check logs
The FilterNoisyLogs plug wasn't completely preventing logs in Phoenix 1.8.
Added metadata_filter to Plug.Telemetry to explicitly skip logging for:
- GET /health
- GET /health/time
- HEAD /

This works at the Telemetry level before logs are emitted.
2026-03-05 19:23:23 -06:00
192cf5b064
fix(alerts): resolve stuck alerts on every successful poll, not just status change
Previous fix only resolved alerts when device status changed from down to up.
This didn't help devices already marked as 'up' with stuck unresolved alerts.

Now:
- Always check for stuck device_down alerts after successful SNMP poll
- Also check for stuck device_up alerts (should always be auto-resolved)
- Logs when stuck alerts are found and resolved
- Works for existing stuck alerts without waiting for status to change

This fixes the 8 stuck alerts in production - they'll auto-resolve on next poll.
2026-03-05 16:54:00 -06:00
3f1d97218d
fix(alerts): auto-resolve device_down alerts when agent polls device successfully
When agents poll devices via SNMP and get successful results, the code was
only processing sensor/interface data without updating device status or
resolving alerts. This caused device_down alerts to persist even after
devices came back online.

This fix:
- Updates device status to 'up' when SNMP polling succeeds
- Auto-resolves any active device_down alerts
- Creates device_up alert (matching DeviceMonitorWorker behavior)
- Broadcasts device_status_changed via PubSub

Also fixes device_up alert creation to include resolved_at timestamp
(matching DeviceMonitorWorker behavior).

Test coverage: Added test verifying auto-resolution when device recovers.
2026-03-05 16:47:48 -06:00
ab24a3100e
fix: suppress health check logs and downgrade CI artifacts to v3
Health Check Logging:
- Update TelemetryFilter to catch both request and response logs
- Filter /health and /health/time endpoints completely
- Check phoenix_log metadata to suppress response logs
- Prevents log flooding from Kubernetes probes (every 5s)
- Refactor to reduce cyclomatic complexity (Credo compliant)

CI Artifact Actions:
- Downgrade upload-artifact from v4 to v3
- Downgrade download-artifact from v4 to v3
- v4 is not supported on Forgejo/GHES

Fixes:
- Production logs no longer flooded with health check entries
- CI pipeline works on self-hosted Forgejo instance
2026-03-05 15:54:47 -06:00
9a12de7526
fix: remove duplicate dashboard alert tests and add SQL Sandbox plug
- Remove 2 duplicate tests that were covered by existing alert feed test
- Add Phoenix.Ecto.SQL.Sandbox plug to endpoint for test mode
- Enables LiveView processes to access SQL Sandbox connections in tests
- All 7,422 tests now passing (100% pass rate)
2026-03-05 14:19:36 -06:00
8f52d87854
feat: add rate limiting to admin endpoints (100 req/min)
- Add :admin type to RateLimit plug with 100 requests per minute per IP
- Apply rate limiting to all /admin routes (LiveView and controller actions)
- Protects superuser endpoints from abuse and brute force attempts
- Aligns with existing auth (10 req/min) and API (1000 req/min) limits
2026-03-05 14:11:19 -06:00
d3d5d1e706
feat: add TowerOps suffix to all page titles
Add '| TowerOps' suffix to page titles so users can easily identify
TowerOps tabs when multiple browser tabs are open.

Examples:
- Dashboard → 'Dashboard | TowerOps'
- Equipment Details → 'Router-1 | TowerOps'
- No page_title → 'TowerOps'

Uses Phoenix LiveView's suffix parameter on live_title component.
2026-03-05 13:51:28 -06:00
b56cdf4e9f
feat: dynamic favicon reflects real-time system health status
Favicon changes to green/yellow/red based on device and alert status.
LiveView computes the status server-side and passes it to a minimal
JS hook that generates PNG favicons via canvas.
2026-03-05 13:47:54 -06:00
ade7e2fe15
fix: correct mail adapter message interpolation in login page
Fixes fragmented translation that displayed raw '%{link}' placeholder text
instead of properly interpolated link in development mail adapter notice.

Changes:
- Consolidate separate translation calls into single interpolated string
- Use raw() helper to safely inject HTML link into translated text
- Update Spanish translation to include %{link} placeholder
- Extract updated translations with mix gettext.extract

Before: "To see sent emails, visit %{link}. the mailbox page."
After: "To see sent emails, visit the mailbox page." (with proper link)
2026-03-05 13:32:34 -06:00
4ce1155548
fix: suppress noisy health check logs in production
Add logger filter to drop Kubernetes health probe logs that were flooding
production logs every few seconds.

Implementation:
- Create ToweropsWeb.TelemetryFilter module with filter_health_checks/2
- Configure as logger :default_handler filter in prod.exs
- Filters based on request_path metadata and message content
- Drops logs for GET /health and HEAD / requests

Impact:
- Eliminates ~120 log entries per minute per pod from K8s probes
- Keeps application logs focused on actual user activity and errors
- No impact on health check functionality - only suppresses logging

Files:
- lib/towerops_web/telemetry_filter.ex (new)
- lib/towerops/application.ex (attach filter on startup)
- config/prod.exs (add filter to logger config)
- CHANGELOG.txt
2026-03-05 13:13:19 -06:00
1d928d4356
security: implement comprehensive security audit fixes
Critical Fixes:
- Remove /health/time endpoint exposing system time information
  * Prevents attackers from detecting time sync issues for TOTP attacks
  * Removed route and controller function, updated tests
- Add email confirmation check to account data export
  * GDPR export now requires confirmed email address
  * Prevents unconfirmed accounts from accessing data export
- Add path traversal validation for MIB archive uploads
  * Extract to temp directory, validate all paths, then copy if safe
  * Prevents malicious tar/zip files from writing outside target directory
  * Added validate_extracted_paths/1 helper function

High Priority Fixes:
- Add comprehensive input validation for mobile auth
  * Length limits: device_name (255), device_os (100), app_version (50), push_token (512)
  * Prevents database corruption and storage exhaustion
- Add heartbeat rate limiting to agent channel
  * Limit database updates to once per 30 seconds (max ~2/min per agent)
  * Prevents malicious agents from exhausting database connections
- Sanitize 500 error responses
  * Return generic error messages to clients
  * Log full details server-side with request_id for support
  * Prevents leaking stack traces and module names
- Add message size limits to agent channel
  * 10MB maximum for all protobuf messages (result, heartbeat, error)
  * Prevents DoS attacks via oversized payloads

Medium Priority Fixes:
- Add GraphQL query depth limits (max_depth: 10)
  * Prevents DoS from deeply nested queries
  * Complements existing complexity limits

Code Quality:
- Refactor agent channel handlers to reduce nesting depth
  * Extract message processing into separate private functions
  * Fixes Credo warnings about excessive nesting
  * Improves code readability and maintainability

Files changed:
- lib/towerops_web/controllers/health_controller.ex
- lib/towerops_web/controllers/api/account_data_controller.ex
- lib/towerops_web/controllers/api/v1/mib_controller.ex
- lib/towerops/mobile_sessions/mobile_session.ex
- lib/towerops_web/channels/agent_channel.ex
- lib/towerops_web/controllers/error_json.ex
- lib/towerops_web/router.ex
- CHANGELOG.txt
- priv/static/changelog.txt
- test/towerops_web/controllers/health_controller_test.exs
- test/towerops_web/controllers/error_json_test.exs

All 7,424 tests passing.
2026-03-05 13:08:10 -06:00
e0757fcd6a
feat: add LLDP topology discovery support from agent
Integrates LLDP neighbor discovery from towerops-agent into Phoenix:

- Updated protobuf definitions with LldpTopologyResult and LldpNeighbor messages
- Regenerated Elixir protobuf code (agent.pb.ex) with LLDP_TOPOLOGY job type
- Added validator for LLDP topology results with neighbor and management address validation
- Added AgentChannel handler for "lldp_topology_result" events
- Added Topology.upsert_neighbor/3 public API for storing agent-discovered neighbors
- Stores LLDP neighbors in device_neighbors table via validated protobuf messages

Agent changes were committed separately in towerops-agent repo (commit b6f60c8).

Files modified:
- priv/proto/agent.proto: Added LLDP message definitions
- lib/towerops/proto/agent.pb.ex: Regenerated from proto file
- lib/towerops/agent/validator.ex: Added validate_lldp_topology_result/1
- lib/towerops_web/channels/agent_channel.ex: Added lldp_topology_result handler
- lib/towerops/topology.ex: Added upsert_neighbor/3 public wrapper
2026-03-05 11:16:28 -06:00
117d13b44b
fix: update alert_type atom comparisons to strings in AlertLive
Changed all alert_type == :device_down to alert_type == "device_down"
in AlertLive.Index to match the string-based alert_type schema.
2026-03-05 09:49:47 -06:00
86a5dc728c
fix: comprehensive bug fixes from reliability audit
Critical Fixes (5):
- Fix Task.yield_many race condition causing data corruption in DevicePollerWorker
- Fix Enum.zip data corruption in SNMP Base Profile with length validation
- Fix missing Alert schema fields for check alerts (check_id, severity, etc.)
- Fix memory leaks from uncancelled LiveView timers in 4 components
- Fix PubSub subscription leak in device form credential testing

High Severity Fixes (3):
- Fix clock skew bug in needs_discovery? check with DateTime.diff clamping
- Fix nil crash in interface status display with proper nil handling
- Fix migration index names after equipment→devices table rename

Medium Severity Fixes (6):
- Fix race condition in device monitor worker (duplicate maintenance checks)
- Fix missing preload validation in devices.ex get_org_default_agent
- Fix broad rescue clause in alerts.ex with specific error handling
- Fix fire-and-forget notification tasks with try-catch error logging
- Fix LiveView state bleeding between tabs (assign_new → assign)
- Add catch-all handle_info callbacks to 3 LiveViews

Infrastructure:
- Silence health check endpoint logs (/health, /health/time)
- Add migration to fix equipment index names missed in rename

Files Changed: 16 files modified, 1 migration added
All changes compile successfully and are backward-compatible.
2026-03-05 08:53:30 -06:00
ec03a03cf5
fix: completely silence logs for noisy health check endpoints
Created FilterNoisyLogs plug that disables Phoenix logging for both
request and response logs by setting conn.private.phoenix_log to false.

This prevents both:
- Initial request log (Plug.Telemetry)
- Response log (Phoenix.Logger "Sent 200 in Xms")

Filters:
- GET /health (Kubernetes probes)
- HEAD / (external uptime monitors)
2026-03-05 08:23:12 -06:00
c56496f7cc
fix: org switching now correctly updates session
The select_org LiveView event was updating default_organization_id in
the database but not the session's current_organization_id. Since
load_default_organization prioritizes the session value, users would
always get redirected back to the stale org.

Replaced with a controller POST action that updates both the session
and DB default before redirecting.
2026-03-04 17:04:49 -06:00
c10394808a
chore: remove unused functions from disabled brute force protection
Removed all unused private functions and imports from BruteForceProtection
plug since the feature is currently disabled (Cloudflare handles security).

Functions can be restored from git history if feature is re-enabled.

Fixed compilation warnings:
- unused function track_404_if_needed/2
- unused function maybe_track_404/2
- unused function get_remote_ip/1
- unused function get_fallback_ip/1
- unused function format_remote_ip/1
- unused function check_and_block/2
- unused function block_request/2
- unused function agent_websocket?/1
- unused alias BruteForce
- unused import Plug.Conn
2026-03-04 16:57:48 -06:00
01ebb8968d
chore: filter out external uptime monitor HEAD requests from logs 2026-03-04 16:48:59 -06:00
8109bd71c8 fix: auto-dismiss poll gap insights when polling resumes
When a device resumes polling (last_snmp_poll_at updated), automatically
dismiss any active 'device_poll_gap' insights for that device.

This prevents stale insights from lingering after connectivity issues
are resolved (e.g., agent reconnection after being blocked).

Changes:
- Auto-dismiss device_poll_gap insights in update_snmp_poll_time()
- Log when insights are auto-dismissed for visibility
- Fix brute force protection compilation error
2026-03-04 16:19:28 -06:00
74173dad0b fix: temporarily disable brute force IP protection
Disable all IP blocking and 404 tracking to allow unrestricted access
while we review and tune ban thresholds and exemptions.

This is a temporary measure to prevent legitimate traffic from being
blocked. Will re-enable after proper configuration.
2026-03-04 14:05:39 -06:00
94bc755dd8
fix: add dark mode support to marketing site
Add dark: variants throughout the marketing page and layout so text,
backgrounds, cards, badges, and borders render properly when the
system or user preference is set to dark mode. Use invert + screen
blend mode on the logo PNG to eliminate its white background.
2026-03-04 13:57:12 -06:00
dce9253afc
fix: exempt agent WebSocket connections from brute force IP bans
Agent connections authenticate at the channel join level with token
validation, so they should not be subject to IP-based brute force
protection. This prevents legitimate agents from being banned when
connecting to the server.

Changes:
- Exempt /socket/agent/websocket from all IP ban checks
- Update documentation to reflect agent exemption
- Agents still authenticate securely via token during channel join
2026-03-04 13:53:13 -06:00
998bdf39b8
update 2026-02-18 10:19:56 -06:00
7081e5daed
fix: correct subscriber counts by filtering CGNAT IPs and excluding routers
Router devices were inflating per-site subscriber counts because:
1. CGNAT IPs (100.64.0.0/10) in router ARP tables caused false matches
2. Routers see all traffic and shouldn't have subscribers attributed
3. Site totals were computed from raw links without filtering

Changes:
- Filter CGNAT (RFC 6598) IPs from device subscriber links
- Deduplicate accounts across devices by match method priority
- Detect routers via SNMP sys_descr and zero out per-device counts
- Derive site totals from filtered per-device impacts
- Add per-device Subs/MRR columns to site device tables
- Add device link fallback for sites without IP blocks
- Fix Gaiia sync case mismatch and execution order
- Improve device list toolbar button icons and tooltips
2026-02-18 09:15:10 -06:00
7668518114
fix: handle missing value attr in input component when using name= directly
When calling <.input name="..." ...> without a field or value attr, @value
was absent from assigns causing a KeyError. Added default: nil to the attr
and use __given__ to detect whether value was explicitly passed so the field
clause still reads value from the FormField correctly.
2026-02-17 16:13:46 -06:00
edeeacfefb
Add Exceptions link to admin nav bar 2026-02-17 10:44:03 -06:00
b7e2fb497c
Add Error Tracker link to admin dashboard 2026-02-17 10:37:44 -06:00
c92dfff6c9
Add ErrorTracker, remove Honeybadger email notice filter
Replace the HoneybadgerNoticeFilter that emailed raw stacktraces on
every exception with ErrorTracker for self-hosted error tracking.
Honeybadger itself is retained. ErrorTracker dashboard is mounted at
/admin/errors behind the superuser auth wall.
2026-02-17 08:13:34 -06:00
1712e7ac9d
Skip SNMP check execution in CheckExecutorWorker for agent-polled devices
CheckExecutorWorker was missing the phoenix_snmp_disabled and
should_phoenix_poll_device guards that DevicePollerWorker and
DeviceMonitorWorker already have. This caused SNMP checks to execute
through Phoenix even when devices are assigned to agents, creating
an infinite loop of failing jobs that fill the Oban queue.
2026-02-17 07:47:15 -06:00
7160bf95d6 Add admin endpoint to flush Oban jobs by state 2026-02-16 16:50:23 -06:00
de986bddf6
Prevent Oban polling/monitoring job stacking per device
When the Oban queue backs up, the 60-second uniqueness window expires
and duplicate jobs stack up per device. Switch to period: :infinity so
only one poll/monitor job exists per device at any time. Add replace
option to supersede stale scheduled jobs with updated scheduled_at.
Remove :executing from unique states so self-scheduling works while
the current job runs. Set max_attempts: 1 since retrying stale polls
is pointless.

Also fix all credo --strict issues across the codebase.
2026-02-16 16:37:48 -06:00
e09b306f9c
Fix maintenance window form crash on mount
The form template iterated over changeset error tuples directly
(e.g. changeset.errors[:name] returns a tuple, not a list), causing
Protocol.UndefinedError for Enumerable on Tuple.

Rewrote form to use to_form/2 and <.input> components per LiveView
conventions. Errors are now handled properly by the input component.
2026-02-16 16:04:59 -06:00
6e3db720fd Collapse device page tabs into header row — single line nav 2026-02-16 15:54:42 -06:00
261bfd4b95 Merge pull request 'Fix dark mode inconsistencies across all pages' (#4) from fix/dark-mode-consistency into main
Reviewed-on: graham/towerops-web#4
2026-02-16 08:41:46 -08:00
7ec2b2a00a Fix dark mode inconsistencies across all pages
Add missing dark: variant classes to:
- consent_prompt, check_live/form_component: modal overlays
- error pages (404, 500): body background
- marketing_layouts: page background
- home page: pricing card, CTA button, feature card
- totp_enrollment: QR container
- admin audit_live/monitoring_live: badge colors, event borders
- alert_live: acknowledged-by text
- dashboard_live: severity color fallback
- device_live (index, show, form): status text, spinners
- settings_live: checkbox borders
- trace_live: status colors, labels, table headers
2026-02-16 10:31:52 -06:00
0f9fac76d9 Add maintenance windows LiveView UI
- Index page with All/Active/Upcoming/Past filter tabs
- Show page with active banner, edit/delete actions
- Form page with scope picker (org-wide/site/device), datetime fields
- Nav link added to desktop and mobile nav
2026-02-16 10:26:10 -06:00
3f2b2a5d5e Merge pull request 'Add onboarding flow for new organizations' (#3) from feature/onboarding-flow into main
Reviewed-on: graham/towerops-web#3
2026-02-16 08:23:22 -08:00
2735c1a032 Add onboarding flow for new organizations 2026-02-16 10:14:45 -06:00
0cd62951ec Replace 'Other Integrations' with specific category headers on settings page
Each non-billing integration now shows under its proper category:
- Quality of Experience (Preseem)
- Incident Management (PagerDuty)
- Infrastructure & IPAM (NetBox)
2026-02-16 10:06:47 -06:00
541b418d57 Group billing providers on settings integrations tab
Add 'Billing & Subscriber Management' section header with 'Choose one'
badge and 'Other Integrations' divider on the settings page integrations
tab. Reorder providers: billing first (Gaiia, Sonar, Splynx, VISP),
then QoE/incidents/infrastructure.
2026-02-16 08:34:46 -06:00
91946946a6 Make billing integration exclusivity more obvious
Add 'Choose one' badge with indigo border to billing category,
clarify that only one billing platform can be active at a time.
2026-02-16 08:22:47 -06:00
47b364a112
Fix Gaiia inventory matching, mapping UX, and logo link
- Fetch MAC address from Gaiia custom fields during inventory sync
- Remove name-based device matching (model names caused false matches)
- Keep IP-only matching for device suggestions
- Make unmapped rows clickable in the Gaiia mapping table
- Point authenticated logo link to /dashboard instead of /orgs
- Fix flaky monitor test teardown race condition
2026-02-15 17:41:43 -06:00
b4c0407ee0 Add granular org roles: executive, technician
New role system:
- owner: full access + financials
- admin: full access + financials (except org deletion)
- executive: read-only + full financial visibility (MRR, revenue)
- technician: field ops (devices, sites, alerts) without financials
- member: legacy alias for technician (migrated)
- viewer: read-only, no financials

Financial data (MRR, revenue, billing) gated behind can_view_financials
assign in all LiveView templates: dashboard, alerts, trace, site show.

Includes migration to rename existing member roles to technician.
2026-02-15 17:40:53 -06:00
d32ba58b75 Hide config backup and config change features from UI
Temporarily hide config change tracking, config timeline links,
and config backup references across dashboard, site show, trace,
and home page. Code preserved with hidden class / HTML comments.
2026-02-15 17:26:10 -06:00
866715fdcc Hide MikroTik API/SSH features from UI
Temporarily disable all MikroTik-related UI elements using false guards.
Code is preserved and can be re-enabled by removing the guards.

Hidden: org settings tab/panel, device form config section,
device show backups/timeline tabs, site form config section,
help page nav item.
2026-02-15 17:18:55 -06:00
f7dc546bcf
Merge remote-tracking branch 'origin/main' into gaiia 2026-02-15 17:17:10 -06:00
de075ea8d9
Store billing subscriber/MRR data on integrations for dashboard
Each billing sync (Gaiia, Splynx, VISP, Sonar) already computes
subscriber counts and MRR but only stored them in sync messages.
Now persists them on the integrations table so the dashboard can
aggregate across all billing providers instead of only querying
Gaiia network sites (which were never populated).

Also fixes SnmpKit Config environment detection to use Mix.env()
as fallback when MIX_ENV env var is not set.
2026-02-15 16:51:33 -06:00