- 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
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.
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)
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
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)
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.
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
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
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.
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.
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
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
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.
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.
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.
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.
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.
- 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
Each non-billing integration now shows under its proper category:
- Quality of Experience (Preseem)
- Incident Management (PagerDuty)
- Infrastructure & IPAM (NetBox)
- 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
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.
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.
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.
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.
- Use 2-column grid on mobile, flex-wrap on sm+
- Hide pipe separators on mobile (they break when items wrap)
- Stats now flow cleanly in a grid on small screens
- Hide Preseem ID, Status, IP, and Linked Device columns on mobile
- Show Preseem ID and status badge inline under name on small screens
- Add overflow-x-auto to table container
- Entity mapping table: hide Gaiia ID and Linked columns on mobile,
show Gaiia ID inline under name on small screens
- Allow text wrapping in name column for long names
- Add overflow-x-auto to table container for safety
- Redirect authenticated users from / to /dashboard instead of /orgs
- Splynx client: replace broken Basic Auth with proper token flow
(POST /api/2.0/admin/auth/tokens → Splynx-EA bearer header)
- Support both api_key and admin login auth types with fallback
- Add MRR calculation to Splynx sync using mrr_total field
- Integrations UI: per-provider credential fields (instance_url,
api_key, api_secret for Splynx; instance_url, api_token for Sonar)
- Wire up test_connection for Splynx, Sonar, and VISP providers
- Verified against live Splynx demo (demo.splynx.com):
auth, customers, routers, services all working
- All 7350 tests passing
Instead of showing all 4 billing providers as separate full-width cards,
the billing section now shows:
- When no billing connected: a compact 4-column grid picker to select
your billing platform (Gaiia, Sonar, Splynx, VISP)
- When one is active: a single card showing the connected provider with
status, sync info, and a note to disable before switching
This reduces visual clutter since users only ever use one billing system.
Non-exclusive categories (QoE, Incidents, Infrastructure) remain as
individual cards since multiple can be active simultaneously.