Adds an optional plain-language summary and recommended action to every
active insight. A new Oban cron worker runs every 5 minutes, picks up
unenriched insights, and asks the configured LLM to restate the finding
and suggest one specific action grounded in the structured metadata.
- Migration: adds llm_summary, recommended_action, llm_model,
llm_enriched_at columns to preseem_insights
- Towerops.LLM context with swappable behaviour; real client uses Req
with the standard Req.Test plug for test isolation
- MockClient + InsightPrompt module (builds chat messages, parses JSON
with code-fence stripping and a raw-text fallback)
- Worker logs and skips on rate limits / missing key / parse errors so
insights still render without an AI summary when the LLM is unavailable
- Optional towerops-llm k8s secret with example template; deployment.yaml
references it as optional in both init and main containers
- UI renders summary + recommended action callout on /insights and on
the org-level insights page
- Add Codeberg Ansible collection link to API/GraphQL docs and Terraform guide
- Un-skip the previously-disabled DevicePollerWorker tests that still pass
- Expand MibController, CnMaestro.Sync, CheckWorker, ActivityFeed,
MobileController, JobCleanupTask tests
- New UploadMibs Mix task test covering arg validation and upload paths
- Set :mib_dir in test config so MibController upload/delete flows can
exercise the real on-disk paths against a writable tmp directory
cnHeat-2.0-style automation hooks. Coverage prediction is now a
first-class API resource that Terraform / Ansible / cron can drive.
REST API (under /api/v1, Bearer-token auth scoped to one org)
- GET /coverages list
- POST /coverages create + auto-queue compute
- GET /coverages/:id read (includes status / progress_pct
for polling-based providers)
- PATCH /coverages/:id update editable fields
- DELETE /coverages/:id delete + cleanup
- POST /coverages/:id/recompute requeue, returns 202
- GET /coverages/:id/kmz Google Earth bundle (KML + PNG)
Each response includes the full resource — Terraform read-after-write
reconciles cleanly; status polling hits a stable JSON shape.
Email-on-complete (Towerops.Coverages.Notifier)
- Worker fires deliver_compute_complete on both :ready and :failed
paths. Sends a short text email to every member of the owning
organisation with a link to the show page (or the failure
reason). Mirrors cnHeat 2.0's per-project email alerts.
- Failures are non-fatal: a stuck SMTP relay never blocks the
underlying coverage record from transitioning state.
KMZ export
- Builds a flat KMZ in-memory via :zip.create — doc.kml +
overlay.png, with the LatLonBox set from the coverage's bbox.
XML special chars are escaped. 409 if not yet ready.
Tests (11 new, all green)
- Bearer-token auth happy path + 401 missing
- index / show / create / update / delete + 404 cross-org
- create returns status:'queued' confirming the auto-enqueue
- recompute returns 202 with status:'queued'
Plus docs/terraform-coverages.md showing how to drive the new API
from a third-party restapi provider until the first-party
terraform-provider-towerops gets a coverage resource.
This commit addresses multiple CRITICAL, HIGH, and MEDIUM severity security vulnerabilities identified in the security audit:
CRITICAL FIXES:
- Fix weak RNG for recovery codes - replaced Enum.random() with :crypto.strong_rand_bytes/1 for cryptographically secure token generation
- Fix subscription limit race conditions - moved free org and device quota checks inside transactions with FOR UPDATE locks to prevent concurrent bypass
- Fix default organization race condition - moved is_default check inside transaction to prevent multiple defaults per user
HIGH SEVERITY FIXES:
- Fix agent token deletion race condition - moved PubSub broadcast inside transaction to ensure agents only receive notification after successful deletion
MEDIUM SEVERITY FIXES:
- Fix LIKE wildcard injection in search - applied sanitize_like() to all user-facing search queries in devices.ex, sites.ex, and gaiia.ex to prevent enumeration attacks
- Fix Jason.decode! DoS - replaced with safe Jason.decode/1 with error handling in device_live/index.ex
- Fix SSRF vulnerability - added URL validation in HTTP executor to block requests to private/internal IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16)
- Fix error information leakage - replaced inspect() in API responses with generic error messages, logging details server-side only
- Fix atom table pollution - HTTP method normalization now uses whitelist mapping instead of String.to_atom()
SECURITY IMPROVEMENTS:
- All quota checks now use pessimistic locking (SELECT FOR UPDATE) to prevent TOCTOU race conditions
- Private IP validation prevents cloud metadata service access (169.254.169.254)
- DNS resolution performed before HTTP requests to detect IP spoofing
- Error details logged server-side but not exposed to clients
Files changed:
- lib/towerops/accounts/user_recovery_code.ex
- lib/towerops/organizations.ex
- lib/towerops/devices.ex
- lib/towerops/sites.ex
- lib/towerops/gaiia.ex
- lib/towerops/agents.ex
- lib/towerops/monitoring/executors/http_executor.ex
- lib/towerops_web/live/device_live/index.ex
- lib/towerops_web/controllers/api/v1/mib_controller.ex
- lib/towerops_web/controllers/api/v1/agent_release_webhook_controller.ex
- lib/towerops_web/controllers/api/v1/geoip_controller.ex
Reviewed-on: graham/towerops-web#108
- 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
- nix.md: TimescaleDB now enabled for both dev and test databases
- nix.md: added troubleshooting section for TimescaleDB reinitialization
- New: docs/features/organization-settings.md (tabbed interface, all tabs documented)
- New: docs/features/device-monitoring.md (schema field reference, activity feed fields, SNMP socket management)
Build a rich network topology from SNMP polling data using evidence-based
confidence scoring. LLDP/CDP neighbors, MAC address tables, and ARP data
are combined to infer device links with weighted confidence merging.
- Add DeviceLink and DeviceLinkEvidence schemas for persistent topology
- Implement evidence collectors: LLDP (0.95), CDP (0.95), MAC (0.7), ARP (0.6)
- Add device role inference from sysObjectID/sysDescr patterns
- Hook topology inference into DevicePollerWorker pipeline
- Add stale link cleanup (24h mark stale, 72h delete) via NeighborCleanupWorker
- Update NetworkMapLive with "Added" vs "All Devices" tabs
- Add connected devices section to device detail page
- Add device role selector to device edit form
- Update Cytoscape.js with role-based node shapes/colors and confidence edges
Add comprehensive documentation for idiomatic Phoenix LiveView URL state
management to ensure browser back/forward buttons work correctly.
Changes:
- Add design document: docs/plans/2026-02-12-browser-navigation-fix-design.md
- Documents the problem (18 LiveViews missing handle_params/3)
- Provides idiomatic Phoenix patterns using push_patch/2
- Includes migration strategy for updating all LiveViews
- Contains testing protocols and success criteria
- Update CLAUDE.md: Add "Browser Navigation and URL State Management" section
- Documents the three core patterns: tabs, modals, filters
- Establishes critical rules for all future LiveView development
- Provides working code examples for each pattern
- Links to official Phoenix documentation
Key Principles:
- URL is single source of truth for visible state
- Use push_patch/2 in event handlers, never assign for URL-backed state
- Always implement handle_params/3 to validate and process URL params
- Use Phoenix built-ins (no custom helpers needed)
This ensures future LiveView development follows the correct pattern
and browser navigation works as users expect.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implemented all critical and nice-to-have Arista sensor improvements identified
in LibreNMS parity audit. Upgrades Arista EOS support from 85% to 100% parity.
Files Changed:
- lib/towerops/snmp/profiles/vendors/arista.ex (enhanced)
Added 4 major enhancements:
1. DOM power conversion (watts→dBm): Detects optical power sensors via regex,
converts using formula dBm = 10*log10(watts*1000), preserves original value
in metadata
2. Arista threshold discovery: Walks ARISTA-ENTITY-SENSOR-MIB threshold table
(OID 1.3.6.1.4.1.30065.3.3.1.1.1), applies 4 threshold types (low_critical,
low_warn, high_warn, high_critical), converts thresholds to dBm for optical
sensors
3. Smart grouping: Organizes sensors by SFPs, PSUs, Platform (chipsets), Power
Connectors, System for better UX
4. Description cleanup: Removes redundant "sensor" text, simplifies PSU naming,
cleans whitespace
- lib/towerops/snmp/profiles/dynamic.ex (integration)
Added apply_vendor_post_processing/3 to call Arista enhancements after sensor
discovery. Applies to both arista_eos and arista-mos profiles.
- test/towerops/snmp/profiles/vendors/arista_test.exs (comprehensive tests)
Added 23 new tests covering:
- DOM conversion (6 tests): Rx/Tx power, Xcvr power, non-DOM sensors, edge cases
- Threshold discovery (4 tests): Basic thresholds, dBm conversion, no thresholds,
SNMP failures
- Smart grouping (6 tests): SFPs, Xcvr, PSUs, power connectors, platform, system
- Description cleanup (5 tests): Trailing sensor, duplicate Sensor strings, PSU
naming, hotspot, whitespace
- End-to-end post-processing (1 test): All enhancements in correct order
All tests passing (30/30 in arista_test.exs, 6367/6367 total).
Result: Arista EOS support upgraded from 85% to 100% LibreNMS parity. All critical
gaps closed: optical power now displayed correctly in dBm, alerting enabled via
thresholds, sensors organized for better UX, descriptions cleaned up.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace Docker-based builds with Nix flakes for reproducible builds.
Changes:
- Use nixos/nix:latest image with `nix` tag requirement
- Build Docker image with `nix build .#dockerImage`
- Add optional Cachix integration for binary caching
- Load image with `docker load < result`
- Keep existing deploy stage unchanged
Benefits:
- Reproducible builds across all environments
- Binary caching via Cachix (faster subsequent builds)
- All dependencies pinned in flake.lock
- Image size similar to Docker builds (~507 MB compressed)
Requirements:
- GitLab Runner with Nix installed and `nix` tag
- Docker socket mounted for loading/pushing images
- Optional: CACHIX_AUTH_TOKEN for binary caching
See docs/gitlab-runner-nix-setup.md for runner setup instructions.
The old Docker-based config is available in git history if rollback needed.
Comprehensive documentation covering architecture, usage, technical
details, and future enhancements for the job monitoring dashboard.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add UserSudoController with GET and POST /users/sudo/verify routes
- Create verify.html.heex template for TOTP verification form
- Only accept TOTP codes (6 numeric digits), reject recovery codes
- Update grant_sudo_mode to set authenticated_at virtual field
- Exclude /users/sudo paths from return_to overwriting
- Add comprehensive controller tests (12 test cases)
- Verify redirect behavior, error handling, and session management
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The plug was incorrectly placed in the endpoint, causing it to run on
all requests including APIs. Moved to :browser pipeline where it belongs,
and removed redundant fetch_session call since the pipeline handles it.
Documents the root cause analysis and solution for the Valkey pod
restart issue. The problem was a race condition during node restarts,
not a Flannel failure.
Resolution:
- Added system-cluster-critical priority class to Valkey
- Application-level Redis health checks provide additional resilience
- Monitoring ongoing to verify stability over 24-48 hours
🤖 Generated with Claude Code