Commit graph

141 commits

Author SHA1 Message Date
ca992f1fd1 fix: add unique form IDs for LiveView 1.2 compatibility, add type attr to button component 2026-07-08 11:10:41 -05:00
749932082f chore: update phoenix_live_view 1.1.32→1.2.6, relax constraint to ~> 1.2 2026-07-08 10:57:31 -05:00
4bd444f147 revert: remove custom color palette, restore standard Tailwind v4 colors 2026-06-23 12:29:46 -05:00
253657280b Revert "refactor: apply custom color palette across entire codebase"
This reverts commit 5a7cdc7138.
2026-06-23 12:26:57 -05:00
5a7cdc7138 refactor: apply custom color palette across entire codebase
- Replace gray->cool-steel, blue/indigo->cerulean, red->sweet-salmon, yellow/amber->wheat
- Dark sidebar: sidebar/footer use cool-steel-800 bg, text lightened for contrast
- ~11,600 color replacements across ~99 files
- Update tests for new color class names
2026-06-23 10:58:48 -05:00
ebcd54be5b refactor: remove daisyUI, use straight Tailwind v4 with custom color palette
- Remove daisyUI plugins and theme from app.css
- Define 5 custom color palettes: sweet-salmon, desert-sand, wheat, cool-steel, cerulean
- Add @utility card, badge, btn classes to replace daisyUI components
- Replace all daisyUI classes across 16+ template files
- Update tests for new class return values
- Set light mode as default theme
2026-06-23 10:44:20 -05:00
3a408a8dc1 Security hardening + performance fixes across codebase
CRITICAL:
- Membership: remove :role/:org_id/:user_id from mass-assignment cast; use explicit create_changeset/4 and role_update_changeset/2
- GraphQL member resolver: add authorize_invite/3 checking admin/owner role and role hierarchy
- REST invitations controller: add auth check for invite creation

HIGH:
- ApiToken: remove :organization_id/:user_id from cast; use explicit create_changeset/4

MEDIUM:
- Move 8 LiveView Ecto queries into context modules (Admin, Alerts, Coverages, OnCall, Snmp)
- Replace Process.put/Process.get with socket assigns for unresolved_alert_count (user_auth + layouts + 50 templates)
- Add batch get_utilization_for_interfaces/1 to eliminate N+1 capacity queries in device show
- Replace Process.sleep with Process.monitor/assert_receive or Process.send_after in 5 test files

LOW:
- Add handle_params/3 to UserResetPasswordLive, UserRegistrationLive, StatusPageLive
- Remove redundant Repo.preload calls; add preloads to list_site_devices/1
- Fix @impl annotations and credo nesting warnings
2026-06-21 17:40:50 -05:00
a4e08b2aca Major codebase cleanup: DRY schemas, remove dead config, consolidate modules
- Created Towerops.Schema base macro (95+ schemas updated to use it)
- Created Towerops.Snmp.Reading macro (7 reading schemas consolidated)
- Created Towerops.Gaiia.BaseSchema macro (4 Gaiia schemas consolidated)
- Created Towerops.SyncLog shared module (UISP + Preseem merge)
- Created Towerops.LogFilters (3 log filter modules merged into 1)
- Created Towerops.OnCall.ChangesetHelpers (9 on-call schemas simplified)
- Created API v1 ResourceController shared helpers (7 controllers)
- Extracted ConnectionHelpers.format_connection_result (2 LiveViews)
- Removed 5 dead config keys (scopes, mib_dirs, stripe_meter_id, etc.)
- Fixed 2 pre-existing broken tests
- All 13,219 tests pass, Credo clean (0 issues)
2026-06-16 15:29:22 -05:00
58daf45b60 fix oban pro worker warnings 2026-06-16 10:47:34 -05:00
d0160f7d8a credo fixes 2026-06-14 08:27:57 -05:00
4421727e61 fix: resolve all Elixir 1.20 type system warnings across project
Removes unreachable catch-all clauses, drops unused `require Logger`
lines, adds pin operators to bitstring size patterns, reorders function
heads for correct dispatch, and replaces deprecated LoggerBackends calls.
2026-05-28 14:52:30 -05:00
2ff6a61bd1 fix(gaiia): add ghost devices tab, manual link, and refresh to reconciliation
Fix auto_match return tuple order (mac/ip/site were swapped). Add ghost
devices tab for cleaning up stale mappings to deleted devices. Add "Link"
button on untracked devices to manually link to existing Gaiia items.
Add manual refresh button for on-demand reconciliation.
2026-05-11 18:35:34 -05:00
97910aa178 fix(gaiia): correct mac_address to if_phys_address in inventory_creator
Also guard against empty model_id form submission.
2026-05-11 16:42:18 -05:00
7067bc2fa9 feat(gaiia): hyperlink IP addresses in untracked devices table 2026-05-11 16:39:53 -05:00
fb2d4b9b1a fix(gaiia): correct Interface MAC field name, relabel auto-match button
- Interface MAC field is `if_phys_address`, not `mac_address`
- Button now says "Auto-Match All" since it matches by MAC, IP, and site
2026-05-11 16:20:35 -05:00
3483d9d5f0 feat(gaiia): MAC-based auto-matching (phase 1, highest confidence)
Adds MAC address matching as the primary matching phase:
- Normalizes MACs (strips colons/dots/dashes) from SNMP interface data
- Matches against gaiia_inventory_items.mac_address (synced from Gaiia)
- Phase 1: MAC → Phase 2: IP → Phase 3: Site

Flash now shows breakdown: "Auto-matched 47 devices (12 by MAC, 34 by
IP, 1 by site). 17 remaining."
2026-05-11 16:09:41 -05:00
40dadededd fix(gaiia): wrap manufacturer select in form for phx-change to pick up name attr
A bare <select phx-change="..."> sends %{"value" => ...}, not the name
attribute. Wrapping in <form phx-change="..."> fixes this so the handler
receives %{"manufacturer_id" => value}.
2026-05-11 15:58:51 -05:00
8db9fb4fcd fix(gaiia): add error handling and logging for model fetch
- Handle empty manufacturer_id selection (select placeholder)
- Log and flash when model fetch fails or returns empty
- Add require Logger to reconciliation live view
2026-05-11 15:25:34 -05:00
2695e8a0eb refactor(gaiia): manufacturer/model dropdowns for inventory creation
Replaces auto-matching with explicit two-step selection:
1. Select manufacturer from Gaiia's inventory model library (dropdown)
2. Select model for that manufacturer (dropdown, filtered live)
3. Click Create

Manufacturers and models are fetched from Gaiia on demand and cached
in socket assigns. Only one device form is open at a time.
2026-05-11 15:12:52 -05:00
31231d9fad fix(insights): correct ngettext usage in auto-match flash
ngettext is a macro that already calls dngettext internally. The
previous code was calling dngettext a second time on the result string,
passing nil as the count — causing a FunctionClauseError.
2026-05-11 15:07:39 -05:00
3267bb134f feat(gaiia): create inventory items for unmatched devices
Adds `Towerops.Gaiia.InventoryCreator` which:
- Matches a device's SNMP-discovered manufacturer/model against Gaiia's
  inventory model library to find the right modelId
- Calls `startAddInventoryItemsJob` to create the item, assigned to the
  correct Gaiia network site with MAC address from SNMP interfaces

A "Create in Gaiia" button now appears next to each untracked device on
the reconciliation page.
2026-05-11 14:58:37 -05:00
485748d253 feat(gaiia): site-based auto-matching + automatic site mapping during sync
Two improvements to Gaiia reconciliation:

1. Auto-match now uses two-phase matching:
   - Phase 1: IP match (high confidence, same as before)
   - Phase 2: Site match — if a device is at a Towerops site linked to a
     Gaiia network site, match it to unmapped inventory assigned to that site

2. Gaiia sync now automatically maps network sites to Towerops sites by
   name during sync. When a Gaiia network site name matches a Towerops
   site name, `site_id` is set on the `gaiia_network_sites` row. This
   enables the site-based matching in phase 2.

Flash message now shows breakdown: "Auto-matched 47 devices (34 by IP,
13 by site). 17 remaining."
2026-05-11 14:17:41 -05:00
80837b8b26 feat(gaiia): one-click auto-match untracked devices by IP
Adds `Gaiia.auto_match_untracked/1` which finds unmapped Gaiia inventory
items whose IP matches an untracked Towerops device and links them.

Button on the reconciliation page's Untracked tab runs the match
inline and shows a flash with matched/remaining counts.
2026-05-11 13:51:13 -05:00
03f364a956 feat(insights): LLM-powered insight enrichment
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
2026-05-09 16:41:48 -05:00
aa7fc830e5 test: refactor LiveView tests to use HTML-aware element/form helpers
Apply patterns from testingliveview.com — replace render_click/submit/change
event-name calls with element/form-targeted equivalents that validate the
actual DOM wiring, not just handler logic. Adds stable test selectors
(id="...") to interactive elements across 10 LiveView templates.

Surfaces (and fixes) several real issues that the bypass-style tests masked:
- Removes dead regenerate_token handler in agent_live/index.ex (no UI ever
  triggered it) and its corresponding test.
- Removes dead refresh_topology test in network_map_live_test (event was
  referenced only in the test — no handler, no button, no JS anywhere).
- Corrects "shows notification tab with add device modal" — modal lives on
  the security tab; original test mounted ?tab=notifications and only
  worked because direct event calls bypassed tab gating.
- Corrects form input shape mismatches in agent_live/index_test that the
  handler's defensive case clauses had been masking.
- Expands setup data for org/settings_live_test (snmp_community,
  default_agent_token_id, multi-org membership) so the gated buttons
  actually render.

11 direct render_click/submit/change calls remain — all intentional,
documented server-side guard tests (tampered POSTs, race conditions,
defensive idempotent handlers).

Full LiveView test suite: 1300 tests, 0 failures.
2026-04-29 11:14:20 -05:00
3ca0834ef0 tests: raise coverage to 70% via helper promotion + new unit/property tests
Promoted pure presentation and utility helpers from `defp` to `def @doc false`
across ~20 LiveViews, Oban workers, and sync modules so they're reachable from
unit tests. Refactored several `cond` blocks into idiomatic function heads with
guards. Added ~250 new test cases in new files under test/towerops and
test/towerops_web, including DB-backed tests for CnMaestro.Sync and
AlertNotificationWorker, and removed dead LiveView tab components and
CapacityLive (no callers anywhere in lib/test).

Configured mix.exs test_coverage.ignore_modules to exclude vendored third-party
code (SnmpKit, protobuf-generated Towerops.Agent.*, Absinthe GraphQL types,
Phoenix HTML modules, Inspect protocol impls) from coverage calculations —
these are not our project code.

Coverage: 66.93% → 70.09%. Full suite: 10,127 tests, 0 failures.
2026-04-24 09:49:06 -05:00
0350ced8e1 dialyzer: fix remaining 88 warnings — clean dialyzer run
Categories addressed:
- pattern_match / pattern_match_cov (32): remove dead case/with clauses
  that dialyzer proved unreachable from the caller types.
- contract_supertype / extra_range / invalid_contract /
  contract_with_opaque (25): narrow @spec declarations to match actual
  success typings.
- call / call_without_opaque (18): fix bad calls, narrow User.t to
  allow nil for in-memory changeset structs, suppress Ecto.Multi
  opaque-type false positives with targeted @dialyzer directives.
- guard_fail / no_return / unused_fun / unknown_function (13): remove
  dead || fallbacks, simplify always-true params, cascade-resolve
  no_returns via the underlying pattern_match and call fixes.

Real production bug fixed: StormDetector.handle_cast/2 had swapped
`:queue.in` args (`queue |> :queue.in(ts)` which desugars to
`:queue.in(queue, ts)` — wrong argument order). Alert timestamps
were never being enqueued, so storm detection would fail at runtime.
Corrected to `ts |> :queue.in(queue)`.

.dialyzer_ignore.exs: suppress two genuine dep-PLT gaps
(:ranch.get_addr/1 false positive from Bandit's transitive ranch,
and the Cloak.Vault GenServer callback_info on the CI build path).

`mix dialyzer` now: Total errors: 114, Skipped: 114 — passes clean.
Warnings: 88 → 0.
2026-04-21 10:32:42 -05:00
91e3181bbc dialyzer: fix all unmatched_return warnings (154 → 0)
Prefix fire-and-forget calls (PubSub.broadcast, Oban enqueue, Logger,
update_*, etc.) with `_ =` so dialyzer knows the return value is
intentionally discarded. Wrap a few if/case expressions whose
branches produce mixed types the same way.

No behavior changes — only explicit acknowledgement of discarded
returns.

Warnings: 242 → 88.
2026-04-21 10:03:55 -05:00
a9c2f8e5e5 fix/findings-medium-priority (#188)
Reviewed-on: graham/towerops-web#188
2026-03-27 12:31:53 -05:00
c7a236e504 reliability-audit-fixes (#181)
Reviewed-on: graham/towerops-web#181
2026-03-26 14:10:36 -05:00
7c8731f86a fix: make sidebar sticky with header and resolve all credo issues (#32)
- Changed sidebar from fixed to sticky positioning
- Wrapped sidebar and main content in flex container
- Sidebar now sticks to top along with header when scrolling
- Removed unused Ecto.Query import from gps_sync.ex

Credo improvements (17 → 0 remaining):
- Replaced all length/1 checks with empty list comparisons
- Extracted helper functions to reduce nesting depth
- Split complex functions into smaller, testable units
- Applied 'with' statements for clearer control flow
- Refactored high-complexity functions (cyclomatic complexity)
- Files improved: storm_detector, alert_notification_worker,
  alert_digest_worker, gps_sync, statistics_sync, device_sync,
  site_sync, site_correlation, reports_live, topology,
  pagerduty/client, cn_maestro/sync

Reviewed-on: graham/towerops-web#32
2026-03-15 18:19:09 -05:00
42bfc8b9d2 paperclip/TOW-21-uisp-integration-phase1 (#31)
Reviewed-on: graham/towerops-web#31
2026-03-15 17:57:36 -05:00
1590f78bdc fix: input validation, SSRF, API hardening, and cookie security
- 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
2026-03-14 14:48:59 -05:00
326932b68e
Move "Choose one" label outside billing box
The exclusive billing provider indicator now appears above the bordered
selection box rather than inside it.
2026-03-13 16:52:39 -05:00
83e85952f1
fix: remove redundant sync schedule box from integrations cards
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.
2026-03-13 16:33:46 -05:00
529a6138d1
fix: align sidebar bottom border with footer border
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.
2026-03-13 16:29:44 -05:00
88323a2290 Deduplicate integrations: settings tab links to standalone page
- 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
2026-03-13 16:16:35 -05:00
9bae7a886a Add page descriptions + rename Schedules to On-Call
- Alerts: add description subtitle
- On-Call (was Schedules): renamed in sidebar + page title, add description
- Maintenance: add description subtitle
- Trace: add title + description (was missing both)
- Sidebar nav updated: Schedules → On-Call
- All pages now have contextual help text explaining their purpose
2026-03-13 15:30:39 -05:00
37314bd154 Add alert routing setting (builtin/pagerduty/both)
- 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
2026-03-13 15:26:25 -05:00
412fc3c626
ux: overhaul Preseem devices page to match Gaiia mapping UX
- 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
2026-03-13 11:38:45 -05:00
88ee55d603
fix: show device link dropdown inline below clicked row in Preseem devices table
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.
2026-03-13 10:45:45 -05:00
0791089c18
ux: show search button in header on mobile/collapsed menu state
Change search trigger from hidden-on-mobile to always visible, hiding
only the text label and keyboard shortcut hint on small screens.
2026-03-12 15:42:01 -05:00
6565385759
ux: rename Entity Mapping and Reconciliation links to user-friendly labels
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".
2026-03-12 15:40:23 -05:00
2beadb0dc7
ux: show sync interval on each integration card
Display "Syncs every Xmin" (or "Xh") alongside the next sync countdown
so users can see the cadence at a glance without opening the configure
panel.
2026-03-12 15:02:30 -05:00
da5cd4054b
fix: clear sync status when integration credentials change
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.
2026-03-12 14:58:36 -05:00
bbd74e895f
fix: prevent credential inputs resetting on re-render for all integrations
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.
2026-03-12 14:31:31 -05:00
f980e9eb6d
fix: preserve NetBox url/api_token in netbox_config across re-renders
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.
2026-03-12 14:25:08 -05:00
ecc69b4e96
ux: improve navigation, tooltips, and discoverability across pages
- 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
2026-03-12 13:40:10 -05:00
58181923f8
improve Gaiia integration UX
- 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
2026-03-12 13:04:17 -05:00
41708651a0
refactor: collapse inactive billing providers into expandable disclosure
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.
2026-03-11 10:09:27 -05:00