- 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
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.
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.
- 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
- 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
- Replace filter tabs with pill buttons (All/Unmatched/Matched)
- Add IP-based match suggestions when AP IP matches a TowerOps device
- Show "Match found" badge on rows with suggestions; sort suggestions first
- Show suggestion chips in inline linking row for one-click linking
- Make unlinked AP rows clickable to open linking panel
- Make IP addresses clickable http links
- Improve linking row layout and descriptive copy
- Replace bare dash with "Not linked" pill in Linked Device column
The linking row was rendered in a separate loop after all device rows,
causing it to always appear at the bottom of the table. Changed to use
a single for loop with an inline if check so the linking row appears
immediately after the corresponding device row.
Update Gaiia integration links on settings and integrations pages to match
the names already used on the mapping page itself: "Device & Site Linking"
and "Comparison Report".
When saving new or updated API credentials, reset last_sync_status to
"never" and clear last_sync_message and last_synced_at so stale error
messages from previous syncs don't persist in the UI.
All non-NetBox provider forms (Sonar, Splynx, Gaiia, Preseem, PagerDuty,
VISP) had the same bug as NetBox: credential inputs used value= bindings
reading from saved DB state. Any phx-click event (Test Connection, etc)
triggering a re-render would wipe unsaved typed values.
Fix by adding @integration_credentials assign, initialized from saved
integration when opening config and updated from form params on every
phx-change via validate_integration.
Sync direction button clicks (phx-click) triggered a LiveView re-render
that reset the url and api_token inputs to their saved DB values, wiping
any unsaved typed text. Fix by tracking url and api_token in @netbox_config
so they survive click-triggered re-renders.
- Replace "Back to X" links with proper breadcrumb trails on settings,
integrations, Gaiia, and Preseem pages
- Fix active_page on config-timeline routes so Devices nav highlights
- Add title tooltip to truncated alert messages (hover to see full text)
- Add tooltips to QoE column header explaining the metric
- Add tooltips to Subs/MRR labels on dashboard status bar
- Make insight messages more descriptive (explain what untracked/ghost/mismatch means)
- Clarify action button labels on insight cards
- Redesign Gaiia mapping page to be TowerOps-centric: devices/sites as primary rows, search Gaiia for what to link them to
- Add IP-based match suggestions and smart sorting (suggestions first, then unlinked, then linked)
- Add search_inventory_items/2 and search_network_sites/2 to Gaiia context
- Make IP addresses clickable links (http://<ip>)
- Add optional Gaiia App URL setting to enable deep links to inventory items and network sites
When a billing provider is active, all other billing providers are
hidden from the main list and collected into a compact <details>
disclosure element showing provider names and descriptions.
When a billing provider is active (enabled), the other billing providers
are dimmed and collapsed to a single row showing a lock icon and which
provider is active. This reinforces the mutually exclusive constraint.
Also fixes status emoji accumulating in page title (JS regex needed
unicode flag) and adds missing page_title to 8 LiveViews.
Fix JS regex in StatusTitle hook to use unicode flag so emoji are
properly matched and replaced instead of accumulating. Add page_title
to 8 LiveViews that were missing it, which caused "TowerOps | TowerOps"
duplication from the live_title suffix.
Add setup_help info boxes to each integration provider showing where
to create API keys, step-by-step instructions, required permissions,
and example URL patterns.
- Show sender's full name in invitation emails
- Use relative time for "Sent" column (more readable)
- Use date format for "Expires" column (shorter)
- Make invitation table horizontally scrollable
Fixes display cropping issue where long timestamps caused
the pending invitations table to overflow its container.
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
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
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.
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 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.
- 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.
- Fix CLOAK_KEY placeholder in nix shell (invalid base64)
- Fix error HTML test assertion to match actual template
- Fix Gaiia sync: read 'subnet' field instead of 'block' for IP blocks
- Fix Gaiia sync: extract nested status name from subscription objects
- Fix security headers tests for environment detection
- Fix mobile auth tests to use raw_token
- Fix LiveView test assertions to match actual template content
- Fix SNMP config test expectations for test environment
- Refactor: resolve all Credo complexity/nesting issues
- Extract helper functions in NetBox sync, VISP sync, Sonar sync
- Flatten nested conditionals in settings, integrations, alerts
- Use with/validate_present pattern for connection testing