Commit graph

256 commits

Author SHA1 Message Date
8e1703cf28
higher snmp timeout 2026-01-18 13:25:15 -06:00
55fa754a25
add force rediscovery per site 2026-01-18 13:22:17 -06:00
b325b253f1
add texas footer 2026-01-18 13:16:20 -06:00
6706613f24
add texas footer 2026-01-18 13:15:27 -06:00
212744365f
fix: add timezone attribute to form layouts 2026-01-18 13:07:07 -06:00
9c2f08317f
feat: remove organization slug from sites and devices URLs
- Created new on_mount hook :load_default_organization that automatically loads user's first organization
- Moved sites and devices routes to root path (/ sites, /devices) instead of /orgs/:org_slug
- Updated all navigation links and route references throughout the app
- Dashboard, alerts, and agents still use org-specific routes (/orgs/:org_slug)
- Users can still switch organizations via org selector for other pages
2026-01-18 13:06:23 -06:00
592b99d739
fix: use correct device_id for neighbor discovery
The neighbor discovery was using snmp_device.id instead of the actual
Equipment device.id when creating neighbor records. This caused all
neighbor inserts to fail because the foreign key constraint expected
the Equipment ID, not the SNMP Device ID.

Fixed in two places:
- Discovery: Use snmp_device.device_id (FK to Equipment)
- PollerWorker: Use device.id (Equipment ID) instead of snmp_device.id

Neighbors should now be properly discovered and saved during both
initial discovery and ongoing polling.
2026-01-18 13:00:31 -06:00
dc9400e6ae
fix: adjust graph x-axis range based on selected time period
- Pass range parameter to SensorChart hook via data-range attribute
- Calculate x-axis min/max dynamically based on selected range (1h, 6h, 12h, 24h, 7d, 30d)
- Show date + time on x-axis labels for ranges > 24 hours
- Show time only for ranges <= 24 hours
- Fixes issue where 7d and 30d graphs showed incorrect time range
2026-01-18 12:56:44 -06:00
1c400259fc
fix: use microsecond precision for ICMP latency measurements
- Changed ping module to use microsecond timer resolution instead of millisecond
- Prevents 0ms readings for very fast pings (localhost, local network)
- Updated response_time_ms to float type to store decimal precision
- Migration converts existing integer values to float
2026-01-18 12:52:29 -06:00
16bfd7667d
fix: pass timezone attribute to all layout components 2026-01-18 12:49:01 -06:00
9d667ac162
fix: use Map.get for timezone in layouts to handle missing assigns 2026-01-18 12:46:27 -06:00
d8ee0320dc
fix: use @timezone instead of assigns[:timezone] in footer calls 2026-01-18 12:43:35 -06:00
442a79baa7
fix: use proper function head for default values with multiple clauses 2026-01-18 12:41:39 -06:00
139eb3b147
fix: display deploy timestamp in user's timezone in footer 2026-01-18 12:24:52 -06:00
3dc22b118d
fix: display deploy timestamp in user's timezone in footer 2026-01-18 12:20:59 -06:00
d2e38e351e
feat: display all timestamps in user's timezone
- Add timezone to socket assigns in mount_current_scope
- Detect user timezone from browser using Intl.DateTimeFormat
- Auto-save detected timezone to user profile on first connection
- Update all templates to use TimeHelpers with timezone parameter
- Update agent helper functions to accept timezone parameter
- Templates updated:
  - Alert list (triggered, acknowledged, resolved)
  - Device list (last checked)
  - Agent list and show (created, last seen, updated)
  - Admin dashboard (impersonation logs)
  - Admin org/user lists (created/joined dates)
  - Main dashboard (alert times)
2026-01-18 12:19:17 -06:00
35f0ac495b
fix: always measure actual ICMP latency instead of returning 0ms for SNMP devices 2026-01-18 12:13:11 -06:00
617e8c5414
reduce snmp poller concurrency 2026-01-18 12:11:08 -06:00
712aa63b04
fix: merge Redis config directly into PubSub opts instead of nesting under redis_config key 2026-01-18 12:10:31 -06:00
5952f32d6b
profile import now upserts 2026-01-18 11:56:41 -06:00
e4b3778da4
exq improvements 2026-01-18 11:50:19 -06:00
5b1b9a08c7
improve profile import 2026-01-18 11:46:56 -06:00
dab3e29025
fix(telemetry): add comprehensive error handling for Exq API calls
Wrapped all Exq.Api calls in individual try/rescue/catch blocks to handle
GenServer exits and exceptions when Exq hasn't connected to Redis yet.

The "GenServer Exq terminating" errors in logs are from Exq itself crashing
when it receives calls before it's ready, not from our code. These will still
appear in logs as Exq restarts, but our telemetry code won't crash.

Changes:
- Added try/rescue/catch around each queue_size call in the loop
- Added try/rescue/catch around processes/busy calls
- Kept outer try/rescue/catch as a final safety net
- rescue blocks must come before catch blocks in Elixir
2026-01-18 11:32:58 -06:00
2ecc2082e6
fix(imports): skip sensors without OIDs and truncate long MIB values
Fixed two profile import issues:

1. Skip sensors without OIDs:
   - Some profiles have sensors with null/missing OID fields
   - These can't be used for SNMP polling anyway
   - Added check to skip importing sensors without oid or num_oid
   - Prevents Ecto.Changeset validation errors

2. Truncate MIB values to 255 characters:
   - ERROR 22001 (string_data_right_truncation)
   - MIB field is varchar(255) but some values exceed this
   - Added truncation to first 255 chars before saving
   - Location: lib/towerops/device_profiles/importer.ex:229-236

Both errors were causing profile imports to fail with crashes.
2026-01-18 11:30:27 -06:00
eb9ba57f2e
fix(telemetry): handle Exq/Redis connection errors gracefully
Changed pattern matching to proper error handling in telemetry functions
to prevent crashes when Exq or Redis connections aren't available.

Issues fixed:
1. Exq.Api.queue_size/2 calls were using pattern matching {:ok, size} =
   which crashed with FunctionClauseError when Exq wasn't connected to Redis

2. Redix.start_link/1 and Redix.command/2 calls were using pattern matching
   which crashed when Redis wasn't available

Changes:
- publish_exq_stats: Use case statements for queue_size calls
- publish_exq_stats: Use with statement for processes/busy calls
- publish_redis_stats: Use with statement for all Redis operations

This allows the app to start and run even when Redis/Exq connections
aren't immediately available, which is especially important during
container startup and rolling deployments.
2026-01-18 11:25:26 -06:00
04daf191f7
fix(imports): handle DateTime microseconds and integer string floats
Fixed two import-related errors:

1. DateTime microseconds error when updating API token last_used_at:
   - Error: :utc_datetime expects microseconds to be empty
   - Fix: Use DateTime.truncate(:second) to remove microseconds
   - Location: lib/towerops/api_tokens.ex:179

2. Float parsing error for integer strings like "0":
   - Error: :erlang.binary_to_float("0") - not a textual representation of float
   - Fix: Use Float.parse/1 instead of String.to_float/1
   - Float.parse/1 handles both "0" and "0.0" correctly
   - Returns {float, remainder} on success, :error on failure
   - Location: lib/towerops/device_profiles/importer.ex:380

Both errors were causing profile imports to fail with crashes.
2026-01-18 11:23:43 -06:00
a6a2c1cbde
fix(telemetry): use is_nil instead of not for Process.whereis check
The `not` operator in Elixir only works with booleans, not nil values.
When Exq process doesn't exist, Process.whereis(Exq) returns nil, causing
`:badarg` error when used with `not`.

Error:
  Class=:error
  Reason=:badarg
  {:erlang, :not, [nil], [error_info: %{module: :erl_erts_errors}]}

Changed from:
  not Process.whereis(Exq)

To:
  is_nil(Process.whereis(Exq))

This properly checks if the process exists without causing runtime errors.
2026-01-18 11:14:15 -06:00
dcb875ea8c
fix(ui): fix API token success modal z-index issue
The token success modal was missing z-index classes that caused the modal
content to appear behind the grey backdrop. Users could only see the
checkmark icon and couldn't interact with the modal properly.

Changes:
- Added z-0 class to backdrop div
- Added relative z-10 classes to span and modal content div
- Added unique ID to modal (token-success-modal)
- Added unique ID to modal title (token-success-modal-title)

This matches the z-index structure used in the "add token" modal.
2026-01-18 11:10:29 -06:00
8e38e19510
fix duplicate modal-title IDs in user settings 2026-01-18 11:01:05 -06:00
5f8041dcc3
fix API token creation and add raw ICMP ping to Rust agent
API token creation fix:
- Handle both nested (%{"token" => %{...}}) and flat params
- Support URL-encoded form data from LiveView
- Add validation for required name and organization_id fields

Rust agent raw ICMP ping implementation:
- New ping module with async ICMP echo request/reply
- Build ICMP packets manually with proper checksum calculation
- Use socket2 for raw socket creation (requires CAP_NET_RAW)
- Parse both raw ICMP and IP-wrapped responses
- Include unit tests for checksum and packet building
- Dependencies: socket2 v0.5, rand v0.8
- Compiles successfully with release profile optimizations
2026-01-18 10:58:46 -06:00
f530c1b9a6
fix AutoDismissFlash hook order and implement raw ICMP ping
AutoDismissFlash hook fix:
- Move hook definitions before LiveSocket initialization
- Ensures hooks are defined when referenced in LiveSocket constructor
- Remove duplicate Hooks object

Raw ICMP ping implementation:
- Replace system ping command with pure Elixir implementation
- Use raw ICMP sockets for echo request/reply
- Build ICMP packets manually with proper checksum calculation
- Parse IP addresses using :inet.parse_address/1
- Handle both raw ICMP and IP-wrapped responses
- Requires CAP_NET_RAW capability in production containers
- Suitable for containerized environments without ping command
2026-01-18 10:56:04 -06:00
1b4b7f1773
fix user deletion bug and implement timezone support
User deletion bug:
- Store client IP in socket assigns during mount
- Access stored IP during delete event instead of connect_info
- Fixes RuntimeError when attempting to delete users

Timezone support:
- Add timezone offset maps for common timezones (no external deps)
- Implement shift_timezone/2 helper using DateTime.add/3
- Add timezone-aware format_datetime/2, format_date/2, format_iso8601/2
- Keep backwards-compatible 1-arity versions defaulting to UTC
- Add comprehensive test coverage for timezone conversions
- All tests passing (23 tests, 0 failures)

Note: Timezone offsets are standard time only (no DST handling)
Common timezones supported: America/New_York, America/Los_Angeles,
Europe/London, Europe/Paris, Asia/Tokyo, Asia/Shanghai, Australia/Sydney
2026-01-18 10:51:11 -06:00
b781246abd
convert API token modal to LiveView and add timezone support
- Replace JavaScript modal handlers with LiveView event bindings
- Add phx-click events for show_add_token_modal and cancel_add_token
- Convert plain HTML form to LiveView form with phx-submit
- Remove outdated window.liveSocket.execJS JavaScript code
- Add timezone parameter support to TimeHelpers functions:
  - format_datetime/2 now accepts timezone and shifts DateTime
  - format_date/2 now accepts timezone and shifts DateTime
  - format_iso8601/2 now accepts timezone and shifts DateTime
- All helper functions fallback to UTC if timezone is invalid or nil
2026-01-18 10:46:43 -06:00
243c687d35
fix all credo issues and add flash auto-dismiss
- Replace TODO comment with "Future enhancement" note
- Alias nested DeviceProfiles.Importer module
- Replace expensive length/1 check with empty list comparison
- Extract nested logic into helper functions to reduce complexity:
  - import_sensor_with_states for sensor import
  - walk_sensor_oid and walk_processor_oid for SNMP walks
  - apply_processor_precision and determine_processor_status
  - check_device_health for device monitoring
- Add auto-dismiss functionality to flash messages (30 second timeout)
- Implement AutoDismissFlash LiveView hook in TypeScript
2026-01-18 10:43:23 -06:00
90ae39091c
refactor user settings to use tab navigation
- Add tab navigation UI with Personal, Account, Security, API, Notifications tabs
- Add active_tab state management in LiveView
- Add switch_tab event handler
- Show only active tab content (like Tailwind Plus template)
- Remove section prefixes since each is now on separate tab
- Default to Personal tab on mount
2026-01-18 10:35:29 -06:00
f941e13f0d
add user profile settings with name, avatar, and timezone
- Add migration for name, avatar_url, timezone fields
- Update User schema with profile_changeset
- Add Accounts context functions for profile updates
- Reorganize settings page into sections:
  * Personal Information (name, avatar, timezone)
  * Account (email, password)
  * API (tokens)
  * Notifications (mobile devices)
  * Security (passkeys)
- Avatar URL accepts external URLs (Gravatar, etc)
- Timezone selector with common zones
2026-01-18 10:32:23 -06:00
712dbf25dd
increase sudo mode timeout to 12 hours 2026-01-18 10:25:17 -06:00
3cc69c9250
feat: add Horde for cluster-aware distributed supervision
Prevents duplicate polling in multi-node production environment:
- Replace DynamicSupervisor with Horde.DynamicSupervisor (distributed)
- Replace Registry with Horde.Registry (cluster-aware)
- Each device monitor runs on exactly ONE node across the cluster
- Each SNMP poller runs on exactly ONE node across the cluster
- Automatic failover when nodes go down
- Automatic rebalancing when nodes join/leave

Architecture:
- Horde.Registry for process naming (distributed lookup)
- Horde.DynamicSupervisor for DeviceMonitor processes
- Horde.DynamicSupervisor for PollerWorker processes
- Task.Supervisor remains local per-node for parallel operations
- members: :auto enables automatic cluster discovery via libcluster

Production impact:
- 2 replicas = devices split ~50/50 across both pods
- No duplicate SNMP polls to same device
- If pod dies, surviving pod takes over all devices
- When new pod starts, devices rebalance automatically

Dependencies added:
- horde ~> 0.9.0 (distributed supervisor/registry)
- delta_crdt (Horde dependency for CRDT-based state)
- libring (Horde dependency for consistent hashing)
2026-01-18 10:14:50 -06:00
fed1b3ecfc
feat: parallelize SNMP polling with Task.Supervisor and async streams
Architecture improvements:
- Add Task.Supervisor (PollerTaskSupervisor) to supervision tree for parallel operations
- Parallelize device-level operations: sensors, interfaces, changes, neighbors run concurrently
- Parallelize sensor polling within device using Task.async_stream (max 10 concurrent)
- Parallelize interface polling using Task.async_stream (max 10 concurrent)
- Parallelize interface changes checking using Task.async_stream (max 10 concurrent)

Benefits:
- Device with 50 sensors: ~5x faster (10 parallel vs sequential)
- Device with 24 interfaces: ~2.4x faster (10 parallel vs sequential)
- 4 device operations (sensors/interfaces/changes/neighbors): run concurrently
- Proper supervision and timeout handling with Task.Supervisor.async_nolink
- Graceful failure handling with Task.yield_many

Example: Device with 50 sensors + 24 interfaces:
- Before: ~74 seconds (50 + 24 sequential)
- After: ~7.4 seconds (5 + 2.4 parallel + concurrent operations)
2026-01-18 10:09:52 -06:00
0816459335
fix: use SNMP poll status for device health checks and improve Exq startup
- Use SNMP poll success as health indicator for SNMP-enabled devices
- Fall back to ICMP ping only for non-SNMP devices or stale SNMP data
- Add connection retry logic to Exq with backoff
- Add better error logging for ping permission issues
- Resolves issue where devices showed as down despite successful SNMP polling

Root cause: Container runs as non-root (UID 65534) without ICMP permissions,
so ping fails even though SNMP (UDP) works. Now using SNMP poll timestamps
to determine device status when SNMP is enabled.
2026-01-18 10:05:12 -06:00
a85085c3ed
fix: prevent telemetry errors when Exq is not running
- Check if Exq process is running before attempting to collect metrics
- Change rescue to catch to handle process exits (:noproc)
- Prevents error logs in environments where Exq is not configured
2026-01-18 10:00:14 -06:00
9b613f7a40
feat: update UI to Tailwind Plus design system and fix auth redirects
- Update user settings page to use Tailwind Plus 3-column grid layout
- Add API token management section to user settings
- Implement organization name display in navigation menu
- Update table component to use Tailwind Plus styling across all tables
- Update flash notifications with Tailwind Plus design
- Update dropdown menus and select inputs with custom chevron styling
- Fix authentication redirect to return to original page after login
- Preserve user_return_to session key across session renewal
- Add store_return_to_for_liveview plug to browser pipeline
- Add profiles.json to gitignore
2026-01-18 09:56:49 -06:00
56093bb493
refactor: use API token auth for profile imports instead of session cookies
Changes profile import endpoint to use standard API token authentication:

API Token Changes:
- Add user_id to api_tokens table (tracks who created the token)
- Update ApiTokens.verify_token/1 to return user along with org_id
- Update ApiAuth plug to assign current_user from token

Profile Import Changes:
- Move endpoint from /api/v1/admin/profiles/import to /api/v1/profiles/import
- Check user.is_superuser in controller instead of using RequireSuperuser plug
- Use api_v1 pipeline (Bearer token auth) instead of browser session
- Update documentation to show API token usage

Security:
- Only API tokens created by superusers can import profiles
- Returns 403 Forbidden if token user is not a superuser
- Logs import attempts with user email for audit trail

This provides a consistent API experience using Bearer tokens
instead of requiring browser session cookies.
2026-01-18 09:30:21 -06:00
e0bcd4feda
feat: add profile export/import workflow via API
Add export and import functionality for device SNMP profiles:

Export (local):
- New mix task: mix export_profiles --librenms-path ~/dev/librenms --output profiles.json
- Exports all profiles to JSON format
- Can filter specific profiles with --profiles flag

Import (production):
- New API endpoint: POST /api/v1/admin/profiles/import
- Requires superuser authentication via browser session
- Processes imports in background via Exq worker (maintenance queue)
- Returns job ID for tracking

New modules:
- Mix.Tasks.ExportProfiles - Export profiles to JSON
- ProfileImportWorker - Background job for importing profiles
- ProfilesController - API endpoint for bulk import
- RequireSuperuser plug - Restricts access to superusers
- Importer.import_profile_from_data/2 - Import from data structures

This enables bulk profile management without SSH access to production.
2026-01-18 09:23:38 -06:00
28e9be773d
fix: comprehensive ePMP/dynamic profile discovery improvements
- Fix divisor calculation in importer: use precision value directly instead of 10^precision
- Fix Dynamic profile to handle map format from Client.walk (was expecting list)
- Add processor discovery to Dynamic profile (converts CPU to percent sensors)
- Fix sensor index extraction to clean template variables
- Make sensor indices unique using descriptive names for scalar sensors
- Add status determination for CPU sensors (ok/warning/critical thresholds)

This enables proper discovery and monitoring of ePMP devices with:
- DFS count sensors
- GPS/DFS/antenna state sensors
- CPU usage monitoring

All sensors now discover correctly without unique constraint violations.
2026-01-18 09:05:03 -06:00
143d9dceb3
fix: remove unreachable error clauses causing compile warnings
Fixed three compile warnings by removing unreachable error handling:

1. Dynamic.discover_sensors/2 always returns {:ok, sensors}, never errors
   - Removed unreachable error clause in discovery.ex
   - Updated spec to reflect actual return type

2. parse_detection_rule/2 always returns {:ok, :processed}
   - Simplified create_detection_rules/2 to use Enum.each instead of reduce_while
   - Removed unreachable error handling in caller

3. import_all_from_directory/2 always returns {:ok, %{...}}
   - Removed unreachable error clause in Mix task
   - Simplified to direct pattern match

All code now compiles without warnings.
2026-01-17 18:34:19 -06:00
888a1e0dda
refactor: remove all references to external project names
Replace all references with generic terms:
- 'external YAML files' instead of specific project names
- 'device profiles' for database-stored definitions
- 'imported OID definitions' for sensor configurations

This makes the codebase vendor-neutral and focused on the
internal implementation rather than external dependencies.
2026-01-17 18:31:41 -06:00
26bbd8e68a
fix: strip LibreNMS template variables from sensor OIDs
LibreNMS sensor OIDs contain template variables like '.{{ $index }}'
which need to be removed before performing SNMP walks.

Added clean_oid_template/1 to strip these template variables:
- '.1.3.6.1.4.1.17713.21.2.1.36.{{ $index }}' -> '.1.3.6.1.4.1.17713.21.2.1.36'
- 'sysUptime.{{ $index }}' -> 'sysUptime'

The SNMP walk will now use the base OID and discover all instances
with their actual index values.
2026-01-17 18:28:50 -06:00
d783afa14a
fix: disable OS definition polling due to MIB name resolution
LibreNMS OS definitions use MIB symbolic names (e.g.,
CAMBIUM-PMP80211-MIB::cambiumCurrentuImageVersion.0) which require
MIB compilation to resolve to numeric OIDs.

The SNMP client (snmpkit) only supports numeric OIDs, not MIB names.

Disabled OS definition polling for now. The system will use the profile
OS name as firmware_version (e.g., 'epmp') until MIB resolution is
implemented.

Future improvements:
- Add MIB compiler integration (net-snmp, snmptranslate)
- Import numeric OIDs during profile import
- Add MIB -> numeric OID lookup table
2026-01-17 18:27:08 -06:00
b83484acf8
feat: poll actual firmware version and serial from SNMP
Enhance dynamic profile to poll OS definitions from SNMP:
- Add serial_number field to snmp_devices table
- Update Dynamic profile to query profile_os_definitions
- Poll SNMP OIDs for version and serial fields
- Display actual firmware version instead of profile name

For ePMP devices, this will now query:
- CAMBIUM-PMP80211-MIB::cambiumCurrentuImageVersion.0 for firmware
- CAMBIUM-PMP80211-MIB::cambiumEPMPMSN.0 for serial number

The Operating System field will now show actual version like
'Cambium 4.7.1' instead of 'Cambium epmp'.
2026-01-17 18:25:52 -06:00