- Bump elixir requirement from ~> 1.19 to ~> 1.20 for gradual typing
- Extract SNMP polling functions from graph_live/show.ex (1492→1118 lines)
into new GraphLive.Polling module (363 lines) — 374 lines net reduction
- Defer subscriber impact API call via start_async/handle_async in
device_live/show.ex, avoiding synchronous Gaiia API call in handle_params
- Add terminate/2 to graph_live/show.ex for live_poll timer cleanup
- Fix .tool-versions to match installed Erlang 27.3.4.2
- Relax AGENTS.md dialyzer policy: allow ignore file for deps/vendored code
The prod build runs MIX_ENV=prod without make/gcc/net-snmp. The NIF
is pre-built by the towerops-nif Nix package and injected via
build.nix preBuild. Skip the mix compiler task in prod to avoid
'could not be found' CI errors.
- DeepSeek.complete: use Keyword.get/3 instead of || so passing api_key: nil
is respected even when DEEPSEEK_API_KEY env var is set (nil is explicit
'no key', not 'use default'). Also fixed base_url and model for consistency.
- test alias: add ecto.migrate after ecto.load so Oban migrations are applied,
fixing 'Found version 12, but version 14 is required' on oban 2.23
- Register :towerops_nif compiler in mix.exs so mix compile/test builds the C NIF
- Add 'make -C c_src' to nix devShell shellHook so NIF is pre-built
- Extract init_mib_resolution/0 from Application.start/2 (reduces CC)
- Wrap NIF calls in try/rescue ErlangError for graceful fallback
- Wrap test_helper.exs NIF calls in try/rescue ErlangError
- Add .nix-postgres-version to .gitignore
- Override mint to 1.9.3 to fix HTTP/1 chunk-size parser CVE-2026-59249
- Remove plug_cowboy dependency by switching PromEx standalone metrics server
to PromEx.Plug in the main endpoint (Bandit), eliminating cowlib CVEs
- Update castore transitive dep 1.0.19→1.0.20
- accounts.ex: convert get_user_by_email_and_password if→with-chain
with explicit User struct match and else→nil. Flatten delete_account
nested if (password check + sole-owner check) into single with-chain
with named else clauses, eliminating do_delete_account indirection.
- organizations.ex: flatten update_member_role nested if/case into
with-chain — parse_role guard → membership fetch → owner check,
each with explicit else branch. Prevents variable shadowing by using
current_role for the membership pattern match.
- capacity.ex: extract calculate_throughput_from_stats inner if into
multi-clause compute_throughput_from_pairs/1 — empty list clause
returns zero_throughput(), non-empty clause handles the computation.
- 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
- 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
- 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
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.
- 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
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.
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.
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
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>
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>
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.
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.
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).
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.
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)
- 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)
- 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
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.