- Create wireless_client_readings hypertable with compression and retention
- Add WirelessClientReading schema and batch insert function
- Update DevicePollerWorker to save historical readings
- Add signal_badge and snr_badge components to CoreComponents
- Add error_tracker_notifier for email alerts on exceptions
* Sends alerts to graham@towerops.net
* Configured in production only (gracefully shuts down in dev/test)
* Includes throttling to prevent duplicate error spam
- Fix AlertNotificationWorker when PagerDuty not configured
* Worker now handles :not_configured gracefully instead of failing
* Prevents unnecessary retries and error noise
* Logs as debug when PagerDuty integration isn't set up
Files changed:
- mix.exs, mix.lock
- config/runtime.exs
- lib/towerops/application.ex
- lib/towerops/workers/alert_notification_worker.ex
- CHANGELOG.txt, priv/static/changelog.txt
After login, users without an organization are redirected to /orgs instead
of /dashboard. The e2e tests expect /dashboard, so create a test organization
for the user during seeding.
Also made the seed script idempotent by deleting existing test user before
creating a new one.
Tested locally and confirmed working.
Function signature is create_totp_device(user_id, device_name, secret) with
separate arguments, not create_totp_device(user, map).
Tested locally and confirmed working.
Accounts.confirm_user_email/1 doesn't exist - the module only has confirm_user/1
which requires a token. For seeding, we can directly set confirmed_at timestamp
using Ecto.Changeset.change/2.
Created seeds_e2e.exs to automatically create a test user with TOTP enabled
for Playwright e2e tests. This ensures tests can authenticate in CI without
requiring manual user setup.
Test credentials (for CI only):
- Email: test@example.com
- Password: TestPassword123!
- TOTP Secret: JBSWY3DPEHPK3PXP (fixed for reproducibility)
The workflow now:
1. Loads database structure
2. Seeds test user with TOTP
3. Passes credentials to e2e tests via environment variables
Replace standard favicon with stoplight design using red (#ef4444),
yellow (#eab308), and green (#22c55e) circles matching the app's
status indicator colors.
The dashboard status indicator favicon (colored circle) uses canvas
to generate a data URL that's set as the favicon. This was blocked
by CSP in staging/production because default-src didn't include data:.
Added data: to default-src directive to fix favicon not updating in
browser tabs when device status changes.
Allow superadmins to override free device limits and per-device pricing
on a per-organization basis. Overrides cascade through subscription
limits, billing calculations, and user-facing settings display.
- Add custom_free_device_limit and custom_price_per_device to organizations
- Add billing_override_changeset with validation
- Update SubscriptionLimits.effective_device_limit to respect overrides
- Update Billing to use effective free count and price per device
- Add Admin.update_billing_overrides with audit logging
- Add override editing UI to /admin/organizations
- Update org settings page to show effective limits/pricing
- 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
The test alias was prompting 'Are you sure you want to proceed?' when
the database structure was already loaded, requiring manual confirmation
on every test run.
Add --skip-if-loaded flag to ecto.load command so it silently skips
structure loading if schema_migrations table already exists.
Now 'mix test' runs without user interaction.
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)
Replace sequential migration execution (4m8s for 172 migrations) with
structure.sql loading (416ms), reducing CI database setup time by 99.7%.
Changes:
- Configure Ecto to dump/load schema from priv/repo/structure.sql
- Update test alias: ecto.migrate → ecto.load
- Remove redundant database setup step from CI workflow
- Commit structure.sql to repository for version control
Benefits:
- CI database setup: 4m8s → 416ms (248s → 0.4s)
- Consistent schema snapshots across environments
- Faster local test runs
Maintenance:
- Run 'mix ecto.dump' after adding new migrations
- Structure file auto-updates with schema changes
- Add idempotent migration to ensure severity, notification_sent, etc. columns exist
- Fixes production issue where migration was marked as run but columns weren't created
- Uses IF NOT EXISTS to safely add missing columns without errors
- Includes all columns and indexes from ExtendAlertsForChecks migration
Implements Phase 1 of network topology discovery using LLDP (Link Layer
Discovery Protocol) via SNMP, inspired by concepts from lldp2map.
New Features:
- LLDP-MIB walker for discovering network neighbors via SNMP
- Database schema for storing neighbor relationships
- Functions to discover, store, and query device neighbors
- Automatic device linking when neighbors are found in database
Components Added:
- Migration: device_neighbors table with unique constraint on
(device_id, local_port, neighbor_name)
- Schema: Towerops.Topology.DeviceNeighbor for neighbor relationships
- Module: Towerops.Topology.Lldp for SNMP LLDP-MIB walking
- Functions: discover_lldp_neighbors/1, list_lldp_neighbors/1,
remove_stale_lldp_neighbors/1 in Topology context
LLDP-MIB OIDs:
- 1.0.8802.1.1.2.1.3.3.0 (lldpLocSysName)
- 1.0.8802.1.1.2.1.3.7.1.4 (lldpLocPortDesc)
- 1.0.8802.1.1.2.1.4.1.1.7 (lldpRemPortId)
- 1.0.8802.1.1.2.1.4.1.1.8 (lldpRemPortDesc)
- 1.0.8802.1.1.2.1.4.1.1.9 (lldpRemSysName)
- 1.0.8802.1.1.2.1.4.2.1.3 (lldpRemManAddr)
CI Fix:
- Updated config/test.exs to use DATABASE_URL from environment in CI
- Fixes "connection refused to localhost:5432" errors in CI builds
- Falls back to localhost config for local development
Code Quality:
- Refactored LLDP module to reduce cyclomatic complexity
- Extracted helper functions to improve readability
- Used 'with' statement to reduce nesting in parse_management_address
- All Credo checks passing
Next Steps (Phase 2):
- Background worker for automated topology discovery
- Topology visualization in LiveView
- Recursive discovery from seed devices
- Added entries for organization_id alert optimization
- Added entries for cascade delete safety improvements
- Added entry for sensor state validation
- Updated user-facing changelog with performance improvements
- All 23 bugs from reliability audit now addressed
Changed foreign key constraints from CASCADE to RESTRICT for:
- agent_tokens.organization_id
- agent_assignments.agent_token_id
**Problem:**
Deleting an organization would cascade delete all agent tokens, which
would then cascade delete all agent assignments, causing silent data
loss without any audit trail.
**Solution:**
Use RESTRICT instead of CASCADE - this prevents deletion if there are
dependent records, forcing explicit cleanup and preventing accidental
data loss.
**Impact:**
- Organizations cannot be deleted if they have agent tokens
- Agent tokens cannot be deleted if they have active assignments
- User must explicitly clean up dependencies first
This provides better data integrity and prevents accidental data loss.
Fixes Medium Bug #15 from reliability audit.
Previous version used hardcoded 'towerops_dev' which doesn't exist in production.
Now uses current_database() to get the actual database name at runtime.
This fixes the crash: ERROR 3D000 (invalid_catalog_name) database "towerops_dev" does not exist
Fixes ERROR 53400 (configuration_limit_exceeded) during large interface
sync operations in device discovery.
The sync_interfaces operation was decompressing 146,603 tuples but the
default limit was 100,000. Setting to 0 (unlimited) removes this constraint.
Error occurred in: Towerops.Snmp.Discovery.sync_interfaces/2
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.
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.
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.