Commit graph

1223 commits

Author SHA1 Message Date
247bbe58da refactor: address audit recommendations from results.md
Applies the 11 recommendations from the codebase optimization review.

Module decomposition:
- Extract Towerops.Topology.InferenceEngine: pulls device-role inference
  (capability rules, vendor matching, evidence merging) out of the topology
  context. Topology now delegates merge_confidence/2,
  infer_role_from_capabilities/1, and infer_device_role/1.
- Extract Towerops.Accounts.TOTP: TOTP secret/QR generation, multi-device
  verification, recovery code lifecycle, and sudo MFA logic. Accounts
  delegates the public TOTP API for backwards compatibility.
- Extract Towerops.Accounts.Sessions: browser session create/list/touch/
  revoke/anonymize/expire. Accounts delegates the public session API.
- Extract Towerops.Snmp.Queries: time-series read paths
  (sensor readings, interface stats, latest-per-id batches).
- Extract Towerops.Snmp.Monitoring: time-series write paths
  (sensor/interface/processor reading inserts, batch inserts via
  Repo.insert_all). Snmp.ex shrinks from 2985 to 2742 lines.

DRY refactoring:
- Add per-schema Query modules (Devices.DeviceQuery, Alerts.AlertQuery)
  with composable for_organization/for_site/with_status/etc filters;
  refactor list_site_devices, count_organization_devices,
  count_site_devices, count_site_devices_down, count_organization_alerts
  to compose them.
- Introduce create_discovered_checks/6 helper in Snmp; collapses 4 nearly
  identical Enum.reduce blocks for sensors/interfaces/processors/storage.
- Unify MAC and ARP evidence collectors behind collect_lookup_evidence/3
  (lldp/cdp were already factored).

Pattern matching:
- Replace cond block in infer_role_from_capabilities/1 with a declarative
  @role_rules table consulted via Enum.find_value (Elixir 1.19 forbids `in`
  with runtime lists in guards, so multi-clause functions weren't an option).
- Replace inline `if since` in get_sensor_readings/2 with a maybe_filter_since
  pipe helper.

Performance:
- Replace Enum.each + Repo.insert per evidence row in upsert_link with a
  single Repo.insert_all batch (truncates :observed_at to seconds).

Human-centric:
- Decompose build_device_lookup/1 into fetch_lookup_devices, map_ips_to_ids,
  map_names_to_ids, map_macs_to_ids.
- Adopt Towerops.Result.map/2 in Snmp.Client.do_get_multiple_sequential
  (kept narrow; `with` is preferred over Result.and_then for chained
  operations and is already used widely).

CLAUDE.md updates:
- Replace stale main→staging / production-branch deployment notes with the
  current flow: PRs deploy to Dokku staging, push to main runs the
  ExUnit gate then builds an image; argocd-image-updater rolls production.
- Refresh data-model diagram and references from "Equipment" to "Device"
  (post equipment→devices rename migration).
- Correct stale architecture facts: Hammer → in-tree Towerops.RateLimit,
  Rust NIF → C NIF (libnetsnmp), gitlab-registry → forgejo-registry,
  expand the Oban queue and cron lists, list actual implemented Ecto types.

All 10,228 tests pass.
2026-04-30 12:33:41 -05:00
6c05d45dd6 deps: upgrade oban_pro to 1.7.0
Bumps vendored Oban Pro from 1.6.13 to 1.7.0, which transitively bumps
oban core to 2.22.1 (requires migrations v14) and db_connection to 2.10.0.

- mix.exs constraint: ~> 1.6 → ~> 1.7
- vendor/oban_pro/ replaced with v1.7.0 hex package
- migration 20260430142200: Oban core v12 → v14 (must run first)
- migration 20260430142241: Oban Pro 1.7.0 schema additions

Also corrects a pre-existing constraint name in AgentAssignment that was
left stale by the equipment→devices rename migration (20260117190134).
The unique index is named agent_assignments_device_id_index, but the
schema's unique_constraint/3 still pointed at the old equipment_id name,
causing the test suite to surface Ecto.ConstraintError instead of a
changeset error. Required to get the suite green for this upgrade.
2026-04-30 10:26:18 -05:00
0d961e887f fix: allow Plausible analytics domain in CSP
Add https://a.w5isp.com to script-src and connect-src so the analytics
script loads and event POSTs are not blocked.
2026-04-29 14:24:04 -05:00
df1d361249 feat: add Plausible analytics to all pages 2026-04-29 13:46:24 -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
701ce12f08 perf+refactor: codebase-wide query and antipattern audit
Performance:
- schedule_live: preload page once instead of get_schedule!/1 per row
- alert_live + alerts: DB-side status filter; Repo.aggregate counts replace length/Enum.count over 500-row fetches on every event
- dashboard_live: drop duplicate get_device_status_counts; cap active alerts to 20 + use count_active_alerts/1
- maintenance.active_windows_for_device: 3 round-trips collapsed into one query (per-site-id branch keeps Postgres parameter types unambiguous)
- sites.build_site_tree: O(N^2) -> O(N) via group_by(parent_site_id)
- accounts.sole_owner_organizations: single group_by + having instead of per-org Repo.aggregate loop
- agents + agent_live (org + admin): count_assigned_devices_batch/1 + count_agent_polling_targets/1 (no preloads)
- alert_digest_worker: list_alerts_by_ids/1 batches digest fetch
- gaiia: distinct: true at DB; limit 50 on bidirectional ilike

Indexes:
- maintenance_windows(organization_id, starts_at, ends_at) WHERE suppress_alerts = true
- alerts(check_id) WHERE resolved_at IS NULL

Antipatterns:
- agents.delete_agent_token: PubSub.broadcast moved outside Repo.transaction so a rollback no longer leaves subscribers acting on a non-existent deletion
- integrations_controller.to_atom_keys: replaced String.to_existing_atom on user-controlled JSON keys with explicit allowlist
- 9 Task.start callsites converted to Task.Supervisor.start_child(Towerops.TaskSupervisor, ...) so background DB writes survive shutdown (test-mode discovery shims left as-is for sandbox semantics)
2026-04-28 16:58:51 -05:00
434386816e feat(on_call): drag-drop reorder + restrictions editor + new-schedule polish
Three deferred followups from the chunk-4 plan, all together:

A. Restrictions editor
- Resolver: time_of_week support (weekday set + time-of-day window),
  in addition to the existing time_of_day.
- New OnCall.update_layer_restrictions/2 + clear_layer_restrictions/1.
- schedule_live/show: per-layer Restrict on-call shifts form
  (Always on call / Time of day / Time of week with Mon-Sun checkboxes),
  changes patched live and persisted via the resolver.

B. Drag-and-drop reorder
- New OnCall.reorder_layers/2 + reorder_escalation_rules/2 (atomic
  transaction; validates id count + membership).
- assets/js/sortable_list.ts: minimal HTML5 drag-and-drop hook (no
  external dep; mirrors the existing DeviceListReorder pattern),
  registered as SortableList in app.ts.
- schedule_live/show + escalation_policy_live/show: layer/level cards
  now carry data-id + draggable + a visible drag handle. Up/down arrow
  buttons remain as a fallback for users on assistive tech.

C. Unified new-schedule UX
- schedule_live/show pre-opens the Add Layer form when the schedule
  has zero layers, so the new-schedule -> add-layers flow lands ready
  to act on. Two existing tests updated to seed a layer first; two
  new tests verify the auto-open behaviour both ways.

Full suite 10,231 / 0 failures. Format/dialyzer clean.
2026-04-28 14:27:38 -05:00
a7c0d362e5 feat(on_call): schedules list search + pagination + e2e refresh (chunk 4)
ScheduleLive.Index
- Name search input above the date nav (phx-debounce 300ms).
- Standard <.pagination> footer (10 per page) under the gantt cards.
- Both reflected in URL params (?search= and ?page=) for shareable
  links; defaults are stripped from the URL.
- Refactored: load_schedules_with_timeline/3 -> hydrate_schedules/3
  so filter/paginate/hydrate are clearly separate steps.

Tests
- 2 new ExUnit integration tests (search filter narrows results +
  search input renders).
- e2e/tests/schedules.spec.ts: switched from table-row selectors to
  card-link selectors (schedules tab is now gantt cards, not a table),
  added smoke tests for date nav controls + search input + REPEAT
  footer + Services sidebar, renamed "Add Rule" -> "Add Level".

Full suite 10,216 / 0 failures. Format/dialyzer clean.
2026-04-28 14:12:57 -05:00
23304e108c feat(on_call): schedule editor layer reorder + unified resolver (chunk 3)
OnCall context
- move_layer/2: up/down position swap for schedule layers, mirror of
  move_escalation_rule/2 (tested for both directions + boundary no-op).

ScheduleLive.Show
- New move_layer event handler.
- Up/down arrow buttons on each layer card; the top arrow is disabled on
  the first layer, the bottom on the last (matches the escalation policy
  level reorder UX shipped in chunk 1).
- Replaced two per-hour walks (build_layer_spans + build_final_spans -
  ~672 resolve calls per render at 14 days) with Resolver.resolve_range/3
  + a small segments_to_spans/3 mapper. Per-layer view wraps each layer
  in a temporary single-layer %Schedule{} so the resolver code path is
  shared and overrides don't bleed into the per-layer strip.

Tests: 3 new context tests for move_layer, 1 new integration test for
the layer reorder UI. Full suite 10,214 / 0 failures. Format/dialyzer
clean.
2026-04-28 14:05:40 -05:00
e7bca6174c feat(on_call): schedules list with gantt timeline (chunk 2)
Resolver
- Resolver.resolve_range/3: returns the contiguous on-call segments for a
  schedule across [start_at, end_at). Walks layer handoffs + override
  boundaries, drops gap segments, and merges adjacent same-user segments.

UserColor module
- New Towerops.OnCall.UserColor with deterministic id -> color mapping
  (Tailwind class + matching #RRGGBB hex). Refactored ScheduleLive.Show
  to use it instead of its own per-page index palette so the same person
  gets the same color across the schedules list and detail pages.

ScheduleLive.Index
- Date navigation: Today / prev / next + 1/2/4-week range selector.
- URL-driven: ?start=YYYY-MM-DD&range=N for shareable views; falls back
  to defaults on malformed values.
- Per-row card: schedule name link, on-call-now avatar swatch, day-of-week
  header strip, gantt strip rendered as a CSS grid with one column per day,
  Today marker overlay.

Tests: 5 new resolver tests, 9 new UserColor tests, 5 new ScheduleLive
integration tests. Full suite: 10,210 / 0 failures. Format/dialyzer clean.

Also: cleaned up two stale inline comments in k8s/deployment.yaml that
disagreed with the values they sat next to (replicas/maxSurge).
2026-04-28 12:48:52 -05:00
e4686f31ce chore: pending worktree changes (k8s probe, profile loader, e2e Makefile)
- k8s/deployment.yaml: bump startup-probe failureThreshold from 12 to 24
  (70s -> 130s max startup time) to give the app more headroom on cold start.
- lib/towerops/profiles/yaml_profiles.ex: defer YAML profile loading from
  init/1 to a handle_info(:load_profiles, ...) so the supervisor (and Bandit)
  come up immediately and the profiles populate in the background within ~60s.
- e2e/Makefile: convenience target wrappers around the existing npm scripts.
2026-04-28 12:39:26 -05:00
a91c00f3cf feat(on_call): PagerDuty-style escalation policy redesign + dialyzer fix
Chunk 1 of the on-call/escalation redesign plan
(docs/plans/2026-04-28-on-call-pagerduty-style-redesign.md):

Schema
- on_call.ex: list_devices_using_policy/2 (direct + org-default inheritance)
- on_call.ex: move_escalation_rule/2 for up/down level reordering
- list_escalation_policies/1 now preloads :rules

Show page
- Levels rendered as numbered cards with up/down/delete controls
- "Notify the following users immediately and escalate after N minutes."
- REPEAT footer surfaces repeat_count inline (no longer a standalone column)
- Right-side Services sidebar lists devices using the policy
- Header line shows the handoff notifications mode (when in use by a service /
  always / never)

Edit form
- Removed standalone Repeat Count input
- Added handoff notifications mode select
- Added inline "If no one acknowledges, repeat this policy N time(s)" block

Index views (policies tab + standalone)
- Replaced Repeat Count column with a Levels (rule count) column

Other
- rate_limit.ex: bind unmatched :ets.new/2 + schedule_clean/1 returns to fix
  dialyzer :unmatched_returns warning under the project's strict flags.

All 10,191 tests pass; mix format clean; mix compile --warnings-as-errors clean;
mix dialyzer passes.
2026-04-28 12:36:41 -05:00
e822bc9986 chore: rate_limit rewrite + on-call redesign plan + handoff_notifications_mode 2026-04-28 12:17:29 -05:00
b6bbcc8348 tests: remove remaining 1s sleeps in slow tests
* UISP diff_snapshots test: backdate first snapshot via UPDATE instead of
  Process.sleep(1_100) to get distinct second-precision snapshot_at values.

* PagerDuty.Client.send_event_with_retry/2: check max_retries BEFORE sleeping
  on 429. Previously a 429 response always slept once (~1s) before checking
  the retry cap, so even with @max_retries = 0 in test env every 429 test
  paid that cost.

Suite time: 65.3s → 33.9s (-48%). No test is >400ms now.
2026-04-24 10:13:17 -05:00
85dd821400 tests: disable Req retries in test env to eliminate ~7s-per-test delays
Req's default `retry: :safe_transient` waits ~7s across exponential backoff
when the stubbed response is a 5xx or connection error. Tests that asserted
error-status behavior paid that price on every run.

Disable retries in test env for every direct Req caller:

* `Towerops.HTTP` (covers weather/netbox/geocoding/http_executor/pagerduty)
* `CnMaestro.Client`, `Gaiia.Client`, `Preseem.Client`, `Sonar.Client`,
  `Splynx.Client`, `Agents.ReleaseChecker`, `Billing.StripeClient`

Use `Keyword.put_new(:retry, false)` so any test explicitly opting into
retry behavior is respected.

Suite time: 82.8s → 65.3s. Slowest test dropped from 7050ms to 1447ms.
2026-04-24 10:04:52 -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
075106932c dialyzer: remove all @dialyzer suppressions, refactor Ecto.Multi → Repo.transaction
The remaining @dialyzer {:no_opaque, ...} / {:nowarn_function, ...}
directives in accounts.ex, devices.ex, organizations.ex all masked the
same upstream issue: Ecto.Multi.new/0 produces a struct containing a
literal %MapSet{map: %{}} whose concrete shape violates MapSet's
@opaque t at every subsequent Ecto.Multi.* call boundary.

Tried first: public helper function with @spec Ecto.Multi.t() — confirmed
dialyzer uses success typing of the body, not the spec. Doesn't help.

Real fix: drop Ecto.Multi entirely for these transactions. Each was
performing a small, sequential set of Repo operations that map cleanly
to a plain `Repo.transaction(fn -> ... end)` block with Repo.rollback
on errors. No Multi machinery is actually needed:

- accounts.ex:
  - register_user/1: insert + 0-2 consent grants
  - register_user_with_organization/1: insert + create_org + consents
  - confirm_user_transaction/1: update + delete_all tokens
- organizations.ex:
  - do_create_organization/4: lock user → limit check → insert org + membership
  - set_default_organization/2: two update_alls
  - accept_invitation/2: update invitation + insert membership
- devices.ex:
  - create_device/2: maybe lock org → maybe quota check → insert

Caught a real consent-failure rollback bug along the way: initial
refactor discarded the return of grant_consents_sequential/2, which
meant FK violations on consent insert would abort the transaction at
the Postgres level but the caller still saw {:ok, user} — later
operations in the transaction silently failed with 25P02. Fixed by
propagating the {:error, changeset} up so Repo.rollback is called.
Verified by the AccountsDefensiveCoverageTest regression tests.

Behavior is preserved — same transactional guarantees, same success
/ error tuple shapes. Net -86 LOC and no suppressions.

mix dialyzer: `done (passed successfully)` with zero @dialyzer
directives in lib/.
2026-04-21 13:04:03 -05:00
9cb4c59638 dialyzer: replace suppressions with real fixes where possible
Changes to eliminate @dialyzer suppressions by fixing underlying causes:

NIF stubs (towerops_native.ex, mib_translator.ex):
- Change stubs to :erlang.nif_error(:nif_not_loaded) (no_return type).
  Real NIF replaces stubs at load time; calls to unloaded stubs now fail
  loudly instead of returning fake data. Lets dialyzer trust @spec.
- Remove @dialyzer :nowarn_function on three NIFs and on translate/1.

Discovery sync_* functions (snmp/discovery.ex, channels/agent_channel.ex):
- agent_channel passes %{device_id: _, interfaces: _} and %{id: _} maps
  into Discovery.sync_ip_addresses/sync_processors/sync_storage, which
  @spec'd only %Device{}. Add narrow map-type unions (ip_sync_device,
  snmp_device_ref) reflecting what the functions actually access.
- Remove @dialyzer :nowarn_function on three agent_channel helpers.

remote_ip.ex — real bug caught and fixed:
- `:ranch.get_addr(socket.transport_pid)` was always raising since
  Bandit uses ThousandIsland, not Ranch; the rescue _ -> nil silently
  returned nil every time. Switched to Phoenix's documented
  :peer_data connect_info (already enabled in endpoint.ex) via
  socket.assigns; remote IP now actually works.
- Remove remote_ip.ex entry from .dialyzer_ignore.exs.

Accounts / Organizations (Ecto.Multi opacity):
- Add @specs to Multi-building helpers, refactor into pipe chains.
- 6 @dialyzer :nowarn_function → 0, but 7 :no_opaque remain. Root
  cause is upstream: Ecto.Multi.new/0 returns a struct with a literal
  %MapSet{} whose @opaque internal representation trips dialyzer on
  every subsequent Multi.* call. Unfixable without an Ecto patch or
  bypassing Multi entirely. Comments document the specific upstream
  issue rather than a vague "Ecto.Multi opacity" claim.

Devices.ex:
- Adding @specs made it worse (call_without_opaque → contract_with_
  opaque); inlining the Multi didn't help either — same MapSet root
  cause. Suppression kept with a sharper comment.
2026-04-21 11:01:30 -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
04e7d5ac23 dialyzer: fix unknown_type warnings (60 → 0)
- Add @type t :: %__MODULE__{} to schemas referenced by callers:
  ApiToken, Monitoring.Check, OnCall.Schedule, SiteOutage,
  NotificationDigest, Snmp.WirelessClient, Topology.DeviceLink,
  Agent.AgentJob, Agent.MikrotikResult.
- Proto.Types: qualify forward references to nested modules with
  Types. prefix, and fix Metric typo (should be union type metric()).
- data_loaders.ex / chart_builders.ex: Snmp.SNMPDevice.t was a ghost
  spec — module is Snmp.Device.

Warnings: 328 → 242.
2026-04-21 09:43:37 -05:00
2653e2516d dialyzer: expand PLT, drop blanket codebase suppression
Ignore file was silencing every warning under lib/towerops/**. Root
cause was plt_add_deps: :apps_direct missing Plug/Phoenix/Oban/Decimal
/Redix/ssl/public_key — hundreds of false `unknown_function` warnings
were being papered over.

Expand plt_add_apps to cover the common deps, narrow the ignore file
to real dep-PLT gaps only, and fix two concrete bugs uncovered by the
change:

- ScopedResource.fetch_preload/4: spec was atom() | [atom()] but
  callers pass keyword lists (e.g. [rules: :targets]).
- ToweropsNative: tag NIF stubs with @dialyzer :nowarn_function so
  dialyzer trusts the @spec instead of the fallback body.

Remaining 328 real warnings surface for follow-up.
2026-04-21 09:33:22 -05:00
e64df28906 fix: correct login URL path in markdown negotiation content 2026-04-17 15:50:31 -05:00
ca041fe5aa feat: add Accept: text/markdown content negotiation for agent compatibility
Intercepts requests with Accept: text/markdown at endpoint level (before
the router's :accepts plug rejects them) and returns hardcoded markdown
representations for /, /privacy, and /terms.
2026-04-17 14:14:31 -05:00
42480144da feat: add RFC 8288 Link response headers to homepage 2026-04-17 14:11:15 -05:00
e370924083 refactor: extract base_url helper and deduplicate OAuth metadata in WellKnownController 2026-04-17 14:09:02 -05:00
0bb76dbe68 feat: add /.well-known discovery endpoints for agent readiness
Adds WellKnownController with 6 public, no-auth endpoints:
api-catalog, openid-configuration, oauth-authorization-server,
oauth-protected-resource, mcp/server-card.json, and agent-skills/index.json.
2026-04-17 14:06:51 -05:00
3f82c1c110 feat: add /sitemap.xml and update robots.txt for agent discovery 2026-04-17 13:58:19 -05:00
f8b8082ee5 chore/update-deps (#236)
Reviewed-on: graham/towerops-web#236
2026-04-15 13:13:36 -05:00
978a72ad41 db-performance-migrations (#234)
Reviewed-on: graham/towerops-web#234
2026-04-04 14:22:25 -05:00
44a80cd5eb remove-gleam (#218)
Reviewed-on: graham/towerops-web#218
2026-03-29 11:03:20 -05:00
f326808d15 fix: increase timeout in flaky debounce test (#210)
The test was using 75ms timeout which was too tight:
- 5 broadcasts with 5ms delays = 25ms
- Debounce delay = 50ms
- Processing overhead = variable

Increased to 200ms to match other similar tests in the file and provide
adequate buffer for debouncing + processing.

Also reformatted proto erlang_compat files to fix line length issues.

Reviewed-on: graham/towerops-web#210
2026-03-28 15:57:19 -05:00
7232ae6306 refactor/dry-improvements (#202)
Reviewed-on: graham/towerops-web#202
2026-03-28 10:56:34 -05:00
efaf5558ff refactor: convert 6 Gleam modules to idiomatic Elixir with TDD (#196)
Phase 1: Foundation Types (100% complete)
- query_helpers: SQL LIKE sanitization with pipe operators
- numeric: Integer parsing with pattern matching guards
- result: Pure Elixir Result monad (map, and_then, unwrap_or)

Phase 2: Ecto Domain Types (100% complete)
- ip_address: IPv4/IPv6 validation using :inet directly
- mac_address: Multi-format MAC parsing (colon/hyphen/dot/compact)
- snmp_oid: OID parsing/manipulation with recursive pattern matching

All 198 tests passing across converted modules.
API changed from Gleam-style {:some/:none to idiomatic {:ok/:error.
Refactored parse_numeric_oid to use with statement, reducing nesting depth.

Reviewed-on: graham/towerops-web#196
2026-03-28 09:52:07 -05:00
ce7a8d196f fix: replace Repo.rollback with proper error handling for Oban compatibility (#201)
- Replace manual Repo.rollback/1 calls with raise/return patterns
- Fix typo: Repo.transact -> Repo.transaction
- Add unwrap_transaction_result/1 helper to reduce nesting depth
- Extract helper functions to satisfy Credo nesting depth requirements
- Fixes 'operation :rollback is rolling back unexpectedly' error

When Oban Pro's Smart engine uses Ecto.Multi for nested transactions,
manual Repo.rollback/1 calls break the transaction stack. This fix uses
proper error handling patterns:

1. Raise exceptions to trigger automatic rollback (admin.ex)
2. Return error tuples and unwrap with helper function (accounts.ex, mobile_sessions.ex)

Files changed:
- lib/towerops/admin.ex (3 functions: delete_user, delete_organization, update_billing_overrides)
- lib/towerops/accounts.ex (5 functions with helpers: update_user_email, reset_user_password, update_user_and_delete_all_tokens, do_update_user_and_delete_tokens, delete_account)
- lib/towerops/mobile_sessions.ex (2 functions with helper: complete_qr_login, do_complete_qr_login)

All tests passing (277 tests total).

Reviewed-on: graham/towerops-web#201
2026-03-28 09:11:21 -05:00
bf30d9ec7e fix: remove duplicate sync_connect from Phoenix.PubSub.Redis config (#198)
Phoenix.PubSub.Redis 3.1.0 automatically appends sync_connect: true to the
redis_opts before passing to Redix.PubSub.start_link. Our configuration was
also passing sync_connect: false, creating a duplicate key that NimbleOptions
rejected with a confusing error message.

The error showed:
  NimbleOptions.ValidationError: unknown options [:sync_connect],
  valid options are: [..., :sync_connect, ...]

This happened because sync_connect appeared twice in the keyword list,
and NimbleOptions rejects duplicate keys.

The fix removes our sync_connect option entirely, letting phoenix_pubsub_redis
control the sync_connect behavior.

This completes the phoenix_pubsub_redis 3.1.0 migration started in cd660387.

Fixes production pod crash on startup.

Reviewed-on: graham/towerops-web#198
2026-03-27 17:59:47 -05:00
cd6603873a fix: update Phoenix.PubSub.Redis configuration for 3.1.0 compatibility (#197)
The recent dependency update to phoenix_pubsub_redis 3.1.0 introduced
breaking changes in the configuration API. The new version no longer
accepts connection options at the top level - they must be nested in
:redis_opts.

Changes:
- Move Redix connection options into :redis_opts keyword list
- Keep only :node_name at top level per v3.1.0 requirements
- Maintains all resilience settings (timeouts, backoff, keepalive)

Before (invalid for v3.1.0):
  [node_name: node(), host: "...", port: 6379, timeout: 5000, ...]

After (valid for v3.1.0):
  [node_name: node(), redis_opts: [host: "...", port: 6379, timeout: 5000, ...]]

This fixes the production crash with error:
  NimbleOptions.ValidationError: unknown options [:timeout, :backoff_initial,
  :backoff_max, :exit_on_disconnection, :sync_connect]

Related: commit 0399b30d (Update Elixir dependencies)

Reviewed-on: graham/towerops-web#197
2026-03-27 17:31:25 -05:00
30248504e2 optimize-slow-tests (#193)
Reviewed-on: graham/towerops-web#193
2026-03-27 17:05:29 -05:00
c4e301a84c fix: address code review findings - memory leaks and redundant queries (#192)
Fixed 5 critical issues from code review:

1. **GlobalSearchTrigger hook listener leak**
   - Added destroyed() callback to remove click listener
   - Prevents memory leak on LiveView unmount

2. **NetworkMap/WeathermapViewer hook listener leaks**
   - Store zoom/fit button handlers as properties
   - Remove DOM event listeners in destroyed() callback
   - Fixes leak on every LiveView remount

3. **SitesMap uses undocumented window.liveSocket.execJS**
   - Changed to proper LiveView pattern using pushEvent
   - Attach listener via Leaflet's popupopen event

4. **handle_info(:refresh_data) double DB fetch**
   - Removed redundant Devices.get_device call
   - Reuse device from assign_base_data

5. **Redundant DB queries in DeviceLive.Show overview**
   - Changed load_sensor_chart_data to accept snmp_device
   - Changed load_overall_traffic_chart_data to accept snmp_device
   - Eliminated 4 redundant Snmp.get_device_with_associations calls

Also removed unused functions:
- load_equipment_data/2
- reload_active_tab_data/1
- assign_subscriber_impact/1

Note: DeviceLive.Show module size (2252 lines) is acknowledged but
deferred as it requires larger architectural refactoring.

Reviewed-on: graham/towerops-web#192
2026-03-27 16:34:42 -05:00
c7a95164e3 fix snmp handling to be more like librenms (#190)
Reviewed-on: graham/towerops-web#190
2026-03-27 13:37:27 -05:00
3f08fd42b6 fix: handle alert_changed message in DashboardLive (#189)
Fixes FunctionClauseError when DashboardLive receives {:alert_changed, org_id}
message broadcast by user_auth hooks.

The user_auth module subscribes all LiveViews to the
"organization:#{org_id}:alerts" topic and broadcasts {:alert_changed, org_id}
messages when alerts are created or resolved. DashboardLive was missing a
handle_info/2 clause to handle this message format, causing crashes in
production.

Solution:
- Added handle_info/2 clause for {:alert_changed, _org_id} message
- Uses existing debounced reload pattern to update dashboard
- Added test to verify message is handled without crashing
- Updated both technical and user-facing changelogs

All tests pass. The fix follows the same pattern used in AlertLive.Index
and DeviceLive.Show.

Reviewed-on: graham/towerops-web#189
2026-03-27 13:26:39 -05:00
a9c2f8e5e5 fix/findings-medium-priority (#188)
Reviewed-on: graham/towerops-web#188
2026-03-27 12:31:53 -05:00
0bc6407c3f feature/entity-physical-inventory (#187)
Reviewed-on: graham/towerops-web#187
2026-03-27 10:48:50 -05:00
8a58c7c238 feat: implement mempools (memory pools) feature for SNMP monitoring (#184)
Add complete memory pool monitoring using HOST-RESOURCES-MIB.
Tracks RAM/swap usage with historical data and real-time updates.

- Add Mempool and MempoolReading schemas
- Integrate discovery and polling with concurrent Task.async_stream
- Add 7 context API functions for querying mempools
- 24 new tests, all 8754 tests passing (0 failures)

Implements C1 from findings_librenms.md

Reviewed-on: graham/towerops-web#184
2026-03-26 17:07:43 -05:00
a8e43b38d6 feat: implement batched SNMP GET for multi-OID requests (#183)
Replace sequential multi-GET with true batched SNMP GET support.
Previously, Client.get_multiple/2 sent N individual SNMP GET PDUs
for N OIDs. Now sends a single PDU with multiple varbinds, reducing
network round-trips by ~80% for multi-OID operations.

Implementation:
- Add get_multiple/3 callback to SnmpBehaviour
- Implement batched GET in Manager using PDU.build_get_request_multi/2
- Update Client.get_multiple/2 to use batched GET with sequential fallback
- Return map format %{oid => {type, value}} for batched results
- Convert to ordered list for backward compatibility
- Handle SNMP exceptions (noSuchObject, noSuchInstance) in result map

Testing:
- Update all test mocks to use get_multiple expectations
- Add individual get stubs for categorize_device_speed and optional fields
- 2249/2251 tests passing (99.87%)

This addresses F1 from LibreNMS feature parity analysis:
"Replace sequential multi-get with batched GET support."

Impact:
- Discovery system info (6 OIDs): 1 SNMP request instead of 6
- Storage polling (3 OIDs/entry): 1 request per entry instead of 3
- Network round-trips reduced by ~80% for all get_multiple operations

Reviewed-on: graham/towerops-web#183
2026-03-26 16:37:34 -05:00
c7a236e504 reliability-audit-fixes (#181)
Reviewed-on: graham/towerops-web#181
2026-03-26 14:10:36 -05:00
0f3f16d443 fix: only send SNMP jobs to agent for SNMP-enabled devices (#180)
- Check device.snmp_enabled before creating discovery/polling jobs
- Also check snmp_enabled for MikroTik jobs
- Prevents agent from attempting SNMP operations on ping-only devices
- Fixes "Agent collected 0 OIDs" errors for devices without SNMP

Reviewed-on: graham/towerops-web#180
2026-03-26 11:10:27 -05:00
a9e82779e7 fix: make deduplicate_discovery_checks migration production-safe (#178)
- Add @disable_ddl_transaction and @disable_migration_lock for concurrent index
- Increase statement_timeout to 10min for DELETE operation on large tables
- Use concurrently: true on index creation to avoid table locks
- Refactor detect_airfiber_counter_overrides to fix credo nesting warning
- Prevents timeout errors in production deployment

Reviewed-on: graham/towerops-web#178
2026-03-26 10:39:09 -05:00
11f9c4450c done i think (#177)
Reviewed-on: graham/towerops-web#177
2026-03-26 10:23:26 -05:00
06ca0390f0 ui: hide weathermap navigation links (#174)
Commented out weathermap navigation in sidebar and mobile menu.
Feature not ready for production use yet.

Files: lib/towerops_web/components/layouts.ex
Changelog: CHANGELOG.txt

Reviewed-on: graham/towerops-web#174
2026-03-25 16:40:11 -05:00