Commit graph

2620 commits

Author SHA1 Message Date
00b0ed2f5d Fix ProfileWatcherTest: handle FileSystem not loaded in test env 2026-06-16 16:33:26 -05:00
bdf22cd411 Fix flaky deferred_discovery timeout test — use receive instead of Process.sleep for deterministic blocking 2026-06-16 16:26:11 -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
d1403c8069 Fix failing tests and clean up code
- Fix doctests for Accounts, Agents.Stats, Snmp to match actual behavior
- Fix dynamic_extra_test vendor post-processing tests to seed sensor data
- Fix activity_controller_test to seed devices for feed data
- Fix session_manager_test to create browser session for test
- Fix topology_test link creation for connection test
- Fix device_monitor/driver_worker tests for unique job constraints
- Fix accounts_test expired_tokens assertion (magic link token is expired)
- Fix happy_path_test and show_events_test to seed monitor data
- Fix admin user_live_test user.name -> user.email (no name field)
- Fix schema_test to seed activity data
- Fix mobile_qr_live_test to match actual template text
- Fix SnmpKit.MIB doctests and tests for enriched return values
- Fix onboarding_live, mobile_controller, mib_test weak assertions
- Remove dead code and fix credo warnings
2026-06-16 14:54:34 -05:00
13f5e8a487 fix settings doctests with undefined function refs 2026-06-16 11:13:15 -05:00
3c2e22fe0d fix broken doctests and test var refs 2026-06-16 10:52:50 -05:00
58daf45b60 fix oban pro worker warnings 2026-06-16 10:47:34 -05:00
78c013e8b7 credo fixes 2026-06-16 09:56:51 -05:00
d85d156bc5 credo fixes 2026-06-15 11:15:46 -05:00
5096ea2669 credo fixes 2026-06-14 16:38:03 -05:00
5e2a3c229e credo fixes 2026-06-14 08:56:02 -05:00
d0160f7d8a credo fixes 2026-06-14 08:27:57 -05:00
9c3319050f credo fixes 2026-06-13 18:24:06 -05:00
0abe522582 credo fixes 2026-06-13 18:20:20 -05:00
5bb55180a7 Fix all mix credo --strict and compiler warnings 2026-06-13 15:45:09 -05:00
bc1a8bd4bd credo fixes 2026-06-13 14:52:18 -05:00
454d9821ae credo fixes 2026-06-13 14:25:19 -05:00
7a3950d1ad chore: add jump_credo_checks ~> 0.4 with all 19 checks enabled
All checks set to exit_status: 0 initially to avoid blocking on
pre-existing violations. Remove exit_status overrides as each
check category is cleaned up incrementally.

Dependency added to dev/test only.
2026-06-12 13:45:59 -05:00
b96b1fabcb chore: add jump_credo_checks ~> 0.4 with all 19 checks enabled
All checks set to exit_status: 0 initially to avoid blocking on
pre-existing violations. Remove exit_status overrides as each
check category is cleaned up incrementally.
2026-06-12 13:44:31 -05:00
080e5111e4 fix: resolve all dialyzer type warnings across codebase
- Narrow @spec for NetworkInsightPrompt.build/1 and NetworkSnapshot.build/1
- Match/discard unmatched returns in monitoring.ex, alerts.ex, and
  cloud_latency_probe_worker.ex
- Remove unreachable :rate_limited pattern in mikrotik_webhook_worker.ex
  (resolve_error returns {:rate_limited, pos_integer()}, never bare atom)
- Fix gettext pluralisation in insights_live/index.ex: dngettext was
  receiving a string where it expected a non_neg_integer count
2026-06-08 16:17:10 -05:00
b5e3c1f977 fix: reduce health-check probe pressure to stop readiness-flapping downtime
Three changes targeting the root cause of production up/down alerting:

1. Skip BruteForceProtection for /health and /health/live paths
   (removes unnecessary Repo.get_by(IpBlock) DB query from every
   k8s probe, matching the existing /socket/agent exemption).

2. Relax k8s readiness probe: periodSeconds 5→10, timeoutSeconds
   2→5, failureThreshold 2→3, successThreshold 3→2 so transient
   DB/Redis blips don't cascade into full service unavailability.

3. Use the persistent Towerops.Redix connection for health checks
   instead of opening a brand-new TCP connection (handshake→AUTH→
   PING→close) on every probe. Towerops.Redix.Fake gains a
   set_ping_response/2 toggle for testing failure paths.
2026-06-08 16:08:31 -05:00
26ac82653e chore: update elixir 1.20.0 / erlang 29.0.1 / debian trixie, add libsctp1 2026-06-04 15:41:19 -05:00
a797bc7cef upgrade: re-vendor oban_pro 1.7.5, oban_web 2.12.5, oban_met 1.2.0; fix Oban unique states warnings; update deps and elixir to 1.19 2026-06-03 14:38:34 -05:00
cad00fee06 ci: install hex/rebar before deps.get to fix OTP 28 compat
The previous commit bumped CI from OTP 27.2/Elixir 1.18.1 to
OTP 28.5/Elixir 1.19.5. The broad mix cache restore-keys was
restoring a stale ~/.mix cache with hex compiled for the old
OTP, causing beam_load errors on new bs_add instructions.

Adding explicit mix local.hex --force ensures the correct hex
version for the running OTP is installed before deps.get.
2026-06-02 17:20:06 -05:00
f58312ca82 bump elixir/erlang version in ci 2026-06-02 17:06:06 -05:00
d85b42c3ee fix: replace remaining Process.sleep calls with receive/timeout patterns
Production code (lib/): all Process.sleep calls replaced with receive
after timeout - retry backoff, exponential backoff, batch delays,
poll intervals, and settle waits.

Test code: poll_until helpers across 4 agent channel test files now
use receive after instead of Process.sleep. Various standalone
Process.sleep(N) calls replaced with :sys.get_state sync barriers
or receive after. deferred_discovery test simulation sleeps reduced
500ms -> 30ms.
2026-06-02 15:27:23 -05:00
cd19e6626c fix(tests): replace Process.sleep with event-driven sync patterns
Replaced unbounded Process.sleep calls across the test suite with
event-driven alternatives: :sys.get_state sync barriers, assert_receive
with bounded timeouts, and Process.send_after + assert_receive.

Largest improvements:
- storm_detector_test: 120-150ms sleeps removed, uses assert_receive
- deferred_discovery_test: 500ms -> 50ms (timeout test simulation)
- event_logger_test: polling loop replaced with :sys.get_state barrier
- tcp_executor_test: 2000ms sleep replaced with bounded recv timeout
- rate_limit_test: sleeps replaced with send_after + assert_receive
- Various 5-50ms sleeps removed from live_view tests
- Unused require Logger removed from 2 test files
2026-06-02 15:16:53 -05:00
075e779b19 fix(snmpkit): increase timeout on charlist parse test from 1s to 30s
Test takes ~16s on this hardware to resolve/error on invalid
charlists. 1000ms timeout was causing CI pipeline failure, preventing
image build and deployment.
2026-06-02 14:51:22 -05:00
8f9dc201af feat(monitoring): alert when backhaul radios lose connected clients
Backhaul radios (ePMP, AirOS, PMP, Cambium, MikroTik, RouterOS) now
monitor their connected client count with a low threshold of 1. When
the count drops to 0 the SnmpSensorExecutor marks it CRITICAL, which
triggers an alert through the existing check → alert pipeline.

- Adds  field to sensor_def type in Vendor behaviour
- Sets  on all clients-type wireless sensors in
  backhaul-capable vendor profiles
- Adds default executor handling: any clients sensor at 0 → status 2
- 3 new tests for clients sensor behavior
2026-06-02 12:27:44 -05:00
b1cbdc6c28 fix: serve logo as WebP with PNG fallback via picture element
Converts the 120KB PNG logo to 17KB WebP (86% smaller). Both logo instances in the marketing layout use <picture> with WebP source and PNG fallback for maximum browser compatibility.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-31 09:53:54 -05:00
56843b8e4e fix: add aria-label to section landmarks on home page
Adds descriptive aria-label attributes to 5 <section> elements that were missing accessible landmark labels: 'Pain points', 'Use cases', 'Pricing', 'Technical details', 'Get started'.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-31 09:53:50 -05:00
1ce1738fc5 fix: set explicit robots meta, Twitter Card tags, and WebP favicon
Adds <meta name='robots'>, twitter:card/title/description meta tags, and WebP favicon reference to the root layout. Fixes title duplication (TowerOps | TowerOps -> Home | TowerOps) by passing page_title from the page controller.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-31 09:53:37 -05:00
62af9991af Fix L2, L3: remove blocking Process.sleep calls
L2: Remove 2-second Process.sleep from post_startup — the try/catch
already handles transient noproc errors from the TaskSupervisor.

L3: Remove Process.sleep from delete_device — in-flight jobs already
handle the race condition via verify_polling_assignment_unchanged.
The blocking sleep was unnecessary and delayed web requests by 500ms.
2026-05-29 16:00:07 -05:00
b07d42bcba Remove incorrect H1 finding from findings.md
The :env config key IS properly set in all environments:
- config/test.exs:112 sets :test
- config/dev.exs:216 sets :dev
- config/runtime.exs:461 sets :prod

Application.get_env(:towerops, :env) works correctly at runtime.
2026-05-29 15:51:55 -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
196c9473a4 ops(k8s): point liveness/startup at /health/live, scale to 2 replicas
Now that the image serving /health/live is live, switch liveness and startup
probes to the shallow endpoint so transient db/redis blips can't kill or block
a healthy pod; readiness stays on the deep /health. Run 2 replicas so a single
pod restart no longer means full downtime.
2026-05-20 12:22:53 -05:00
9ffa49a8f2 fix(oban): use DynamicLifeline in prod; add /health/live + split k8s probes
Prod paired the Oban Pro Smart engine + unique workers with core
Oban.Plugins.Lifeline, whose rescue has no unique_violation handling. An
orphaned executing CheckExecutorWorker job rescued back to available collides
with its already-scheduled successor on oban_jobs_unique_index (23505),
crashing the plugin every 60s and leaving ~150 orphans stuck. Switch prod to
Oban.Pro.Plugins.DynamicLifeline, which repairs the conflict via
Smart.clear_uniq_violation. Dev keeps core Lifeline (Basic engine).

Add a shallow /health/live endpoint that does not touch db/redis. k8s liveness
and startup probes now target it so a transient dependency outage can't kill or
block boot of a healthy pod; readiness keeps the deep /health to gate the load
balancer. (deployment.yaml probe/replica change pushed separately, after the
image carrying /health/live is live.)

Also drop unused POSTGRES_* keys from the secrets example.
2026-05-20 12:07:29 -05:00
d9bf4f260b fix(monitoring): stop CheckExecutorWorker duplicate job buildup with unique constraint
CheckExecutorWorker had no Oban `unique` constraint, so every insert path
created a fresh job row: self-scheduling, JobHealthCheckWorker, the check
form/API/GraphQL, and — the main amplifier — Monitoring.schedule_check/1
called unconditionally by snmp.ex discovery after create_check/1, which is
an upsert that returns {:ok, check} even when the check already exists.
Every SNMP discovery pass re-enqueued a job for every existing check, so
the check_executors queue grew to ~70k jobs for ~5.7k checks and stopped
draining.

Add `unique: [keys: [:check_id], states: [:available, :scheduled,
:retryable], period: :infinity]`. `:executing` is excluded so perform/1's
self-reschedule does not conflict with its own running job.

Also add the `checks` and `check_executors` queues to config/dev.exs —
they were missing from dev entirely (prod's runtime.exs already had them).
2026-05-14 14:01:07 -05:00
6b82205a58 fix(sync): classify unexpected 5xx statuses as transient so vendor syncs retry
SyncErrors.transient?/1 only had explicit clauses for the {:http_error, ...}
shape, but vendor API clients return {:unexpected_status, status, body}. A 502
was retried only by accident via the catch-all, and a 4xx in that shape would
have been retried 20 times. Add explicit {:unexpected_status, ...} clauses: 5xx
is transient (Oban backs off and retries), 4xx is permanent.
2026-05-14 09:27:17 -05:00
8e3f20a589 cleanup 2026-05-13 12:50:53 -05:00
8682cdce55 fix: resolve session salts at runtime so prod release boots
The @session_options module attribute used Application.compile_env, which
baked compile-time placeholders into the endpoint while runtime.exs set
the real values from SESSION_SIGNING_SALT / SESSION_ENCRYPTION_SALT env
vars. Phoenix detected the mismatch and refused to start (failing migrate
Job in k8s).

- Remove hardcoded salts from config/config.exs (no compile-time binding)
- Add stable per-env salts in dev.exs / test.exs so local + CI don't need
  the env vars
- Split static cookie opts (@static_session_options) from runtime-resolved
  opts in endpoint.ex; expose session_options/0 as an MFA tuple in socket
  connect_info so LiveView decodes sessions with the same runtime salts
- New ToweropsWeb.Plugs.RuntimeSession wraps Plug.Session, fetches salts
  from app env on first request, and caches the initialized opts in
  :persistent_term (zero per-request overhead after warm-up)
2026-05-12 16:26:54 -05:00
7c1aea50ec fix: M19, L16 — protobuf validation on 6 decode paths, vault ETS docs
- M19: Add validate_mikrotik_device, validate_mikrotik_command, validate_check,
  validate_snmp_query, validate_sensor, and validate_interface functions with
  string length and list size validation, wired into all call sites
- L16: Document that Cloak Vault ETS table name collision risk is mitigated by
  ETS tables being node-local (only a concern on same-BEAM-node, not a supported
  production configuration)
2026-05-12 15:46:13 -05:00
f87657fbfb fix: M20, M21, M22, L14 — archive safety, ETS protection, impersonation expiry, check pagination
- M20: Add check_no_hardlinks and check_no_special_files to MIB upload
  archive extraction to prevent hard link, device node, and FIFO attacks
- M21: Change RateLimit ETS table from :public to :protected; route hit/get/reset
  through GenServer.call instead of direct ETS access
- M22: Add 8-hour impersonation timeout — store impersonated_at in session and
  auto-revoke impersonation when expired
- L14: Add default limit (500) and optional offset to list_checks for pagination
2026-05-12 14:22:20 -05:00
a9579b84f0 fix: H11 — load session salts from env vars instead of hardcoded 8-byte values
SESSION_SIGNING_SALT, SESSION_ENCRYPTION_SALT, and LIVE_VIEW_SIGNING_SALT
are now loaded from environment variables in production. Dev/test keep the
previous defaults via config.exs. k8s/secrets.yaml has placeholder entries;
the user fills in real values before applying.
2026-05-12 14:03:31 -05:00
095c5d3236 fix: M1 — prevent cross-org resource existence probing via scoped fetch
Use Repo.get_by(schema, id: id, organization_id: organization_id) instead of
Repo.get + pattern match so that resources from wrong orgs return :not_found
instead of :forbidden, preventing org membership discovery.
2026-05-12 13:44:19 -05:00
c473f472c7 fix: L3, L7, L12, L13 — IP whitelist normalization, org scoping, liveSocket guard, changeset safety
- L3: Normalize IPv4-mapped IPv6 addresses (::ffff:x.x.x.x) to IPv4 tuples
  when matching against IP whitelist entries
- L7: Verify interface belongs to current org before set/clear capacity
- L12: Guard window.liveSocket behind NODE_ENV check for production safety
- L13: Replace Alert.changeset with Ecto.Changeset.change for simple field
  updates (resolved_at, acknowledged_at, gaiia_impact) to prevent
  accidental overwrites from the 17-field cast
2026-05-12 13:18:06 -05:00
cec8eabcd8 fix: M13, M14, M15, M18 — download safety, queue blocking, PubSub chunking, protobuf validation
- M18: Add validate_snmp_device/1 with 255-byte limits on community,
  v3_auth_password, and v3_priv_password to prevent memory exhaustion
- M13: Add client-side mime_type allowlist to phx:download handler
- M14: Replace Process.sleep loop in WeatherSyncWorker with Task.async_stream
  to avoid blocking the Oban queue slot
- M15: Chunk device list in CloudLatencyProbeWorker PubSub broadcasts
  (100 devices per message) to prevent megabyte-sized messages
2026-05-12 12:54:48 -05:00
6fa0b791f2 fix: M3, M6, M7, M12 — open redirect, overfetch, missing constraint, admin defense-in-depth
- M3: Replace blacklist-based valid_return_path? with whitelist of known app
  route prefixes, plus URI decoding to prevent %2f encoding bypasses
- M6: Replace full-org device/link load in get_node_detail with targeted
  join query filtering by discovered node fields
- M7: Add partial unique index on (organization_id, email) for pending
  invitations (WHERE accepted_at IS NULL)
- M12: Add on_mount superuser verification to all 7 admin LiveViews for
  defense-in-depth
2026-05-12 12:46:16 -05:00
d456283844 fix: H22 — batch data retention deletes in 10k-row chunks
Replace 8 unbounded DELETE statements with a batched_delete loop to prevent
WAL amplification, autovacuum storms, and long row locks on large tables.
2026-05-12 12:26:50 -05:00
52cde2cbc5 fix: H21 — stagger per-integration sync jobs across maintenance queue
Sonar, Splynx, VISP, NetBox, and Gaiia sync workers all followed the same
"cron runs, then Enum.map syncs every integration in the same job" shape.
A single slow tenant — anything from an upstream rate limit to a stuck
GraphQL call — would hold the maintenance queue slot for ten minutes
while every other integration waited in line.

Each worker now uses the dispatcher/per-integration pattern that
UispSyncWorker already had:

- the cron `perform(%Oban.Job{args: %{}})` fans out one job per eligible
  integration via `PollingOffset.calculate_offset/2`, staggered across a
  5- to 10-minute window with `unique: [period: window_seconds]`
- `perform(%Oban.Job{args: %{"integration_id" => id}})` does the actual
  sync, classifying failures via `Towerops.Workers.SyncErrors.transient?/1`
  so permanent errors (auth, 4xx) don't get retried forever

Worker-specific defaults preserved:
- gaiia / netbox: 30/600s window, longer interval
- sonar / splynx / visp: 10/300s window

Tests updated to match the two-stage dispatcher pattern — `perform_job/2`
with empty args now asserts the per-integration jobs got enqueued and
the actual sync is invoked via a separate `perform_job/2` call with the
integration_id arg.
2026-05-12 12:12:11 -05:00