Commit graph

2275 commits

Author SHA1 Message Date
FluxCD
08cdbe355b chore: update towerops image to git.mcintire.me/graham/towerops-web:main-1777398683-e7bca61 [skip ci] 2026-04-28 17:53:44 +00: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
FluxCD
2e6729777b chore: update towerops image to git.mcintire.me/graham/towerops-web:main-1777396805-e822bc9 [skip ci] 2026-04-28 17:23:42 +00:00
e822bc9986 chore: rate_limit rewrite + on-call redesign plan + handoff_notifications_mode 2026-04-28 12:17:29 -05:00
FluxCD
135d9b2720 chore: update towerops image to git.mcintire.me/graham/towerops-web:main-1777312851-3dce118 [skip ci] 2026-04-27 18:04:14 +00:00
3dce118427 flux refactor 2026-04-27 12:57:59 -05:00
FluxCD
bbf8cfacf2 chore: update towerops image to git.mcintire.me/graham/towerops-web:main-1777043250-85dd821 [skip ci] 2026-04-24 15:13:53 +00: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
FluxCD
36e58d4d9f chore: update towerops image to git.mcintire.me/graham/towerops-web:main-1776794780-0751069 [skip ci] 2026-04-21 18:08:21 +00: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
FluxCD
0e0cf7163e chore: update towerops image to git.mcintire.me/graham/towerops-web:main-1776787421-9cb4c59 [skip ci] 2026-04-21 17:55:18 +00: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
FluxCD
093c24a783 chore: update towerops image to git.mcintire.me/graham/towerops-web:main-1776785720-0350ced [skip ci] 2026-04-21 15:37:36 +00: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
FluxCD
8a3d024453 chore: update towerops image to git.mcintire.me/graham/towerops-web:main-1776783978-91e3181 [skip ci] 2026-04-21 15:08:30 +00: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
FluxCD
2d6c7d28e2 chore: update towerops image to git.mcintire.me/graham/towerops-web:main-1776782753-04e7d5a [skip ci] 2026-04-21 14:47:58 +00: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
FluxCD
eeacfa4e70 chore: update towerops image to git.mcintire.me/graham/towerops-web:main-1776782194-2653e25 [skip ci] 2026-04-21 14:40:03 +00: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
fc51974d36 chore: update dependencies
Bumps hammer_backend_redis 7.1.0 → 7.1.1
2026-04-21 08:30:52 -05:00
f648b6cb9b fix(ci): modify existing sources.list to add non-free instead of creating new file 2026-04-17 17:42:34 -05:00
bc7651cec4 fix(ci): detect OS before enabling non-free — Debian needs it, Ubuntu multiverse already enabled 2026-04-17 17:38:06 -05:00
a878fb7abd fix(ci): remove bad Debian sources entry — runner is Ubuntu Noble with multiverse already enabled 2026-04-17 17:32:32 -05:00
dd028f6157 fix(ci): enable Debian non-free repo for snmp-mibs-downloader; loosen flaky timing assertion to 500ms 2026-04-17 17:24:15 -05:00
84f38b6491 fix(ci): remove sudo from apt-get — Forgejo runner runs as root 2026-04-17 17:16:18 -05:00
e64df28906 fix: correct login URL path in markdown negotiation content 2026-04-17 15:50:31 -05:00
12aa724a86 feat: add WebMCP tool definitions for AI agent browser interaction 2026-04-17 14:17:28 -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
20df4df566 test: improve sitemap controller test coverage
Assign response body once to fix the double call, rename the test,
and assert all 6 public URLs plus the exact <loc> count.
2026-04-17 14:00:57 -05:00
3f82c1c110 feat: add /sitemap.xml and update robots.txt for agent discovery 2026-04-17 13:58:19 -05:00
FluxCD
cc2cad10a7 chore: update towerops image to git.mcintire.me/graham/towerops-web:main-1776278121-ecb30b2 [skip ci] 2026-04-15 18:45:21 +00:00
ecb30b2785 fix(flux): add imagepolicy setter markers to deployment (#238)
Without these comments the ImageUpdateAutomation with strategy=Setters
has nothing to rewrite, so new image tags detected by ImagePolicy never
get committed back to main and the cluster never rolls out new builds.

Reviewed-on: graham/towerops-web#238
2026-04-15 13:33:08 -05:00
445cafa88b fix(ci): install docker cli before building image in production workflow (#237)
Runner doesn't have docker pre-installed; explicitly install docker.io
matching the pattern used in microwaveprop.

Reviewed-on: graham/towerops-web#237
2026-04-15 13:20:51 -05:00
f8b8082ee5 chore/update-deps (#236)
Reviewed-on: graham/towerops-web#236
2026-04-15 13:13:36 -05:00
2f3824cecc chore(deps): update all dependencies (#235)
Upgraded Elixir deps:
- absinthe 1.9.1 => 1.10.0
- credo 1.7.17 => 1.7.18
- fine 0.1.4 => 0.1.6
- honeybadger 0.25.0 => 0.26.0
- lazy_html 0.1.10 => 0.1.11
- mimerl 1.4.0 => 1.5.0
- phoenix_pubsub_redis 3.1.0 => 3.1.1
- swoosh 1.24.0 => 1.25.0

Updated npm deps in e2e:
- @playwright/test 1.58.2 => 1.59.1
- @types/node 25.4.0 => 25.6.0
- dotenv 17.3.1 => 17.4.2
- otplib 13.3.0 => 13.4.0

No security vulnerabilities found.

Reviewed-on: graham/towerops-web#235
2026-04-15 13:03:25 -05:00
978a72ad41 db-performance-migrations (#234)
Reviewed-on: graham/towerops-web#234
2026-04-04 14:22:25 -05:00
70e07d6033 Merge pull request 'chore(deps): update dependency @playwright/test to ^1.59.0' (#228) from renovate/playwright-monorepo into main 2026-04-01 09:01:23 -05:00
Renovate Bot
5bedfd3d16 chore(deps): update dependency @playwright/test to ^1.59.0 2026-04-01 06:02:44 +00:00
8811a10b4d use-existing-docker-daemon (#227)
Reviewed-on: graham/towerops-web#227
2026-03-29 13:45:09 -05:00
344c6658e8 fix: use existing Docker daemon instead of starting new one (#226)
The CI runner already has a working Docker daemon and the runner user
is in the docker group. All previous failures were caused by the workflow
trying to start a NEW dockerd instance with vfs storage driver.

Solution: Remove all daemon startup code and just use the existing Docker
daemon directly. The runner user already has access via group membership.

Verified on ci host:
- Docker service running: overlayfs storage driver
- runner user in docker group: uid=1001(runner) groups=1001(runner),989(docker)
- docker info works as runner user

This is the simplest solution and should have been tried first.

Reviewed-on: graham/towerops-web#226
2026-03-29 13:28:37 -05:00
a0b916cc9a fix: use img for completely unprivileged container builds (#225)
Podman fails even with chroot isolation on this runner. The environment
is too restricted for standard container tools (Docker, Buildx, Podman).

Solution: Use 'img' by Jess Frazelle, designed specifically for truly
unprivileged environments:
- No root/sudo required for builds
- No user namespaces required
- No special capabilities required
- Pure userspace implementation
- Built for restricted CI environments

This is the last resort tool for environments where Docker/Podman
cannot run at all.

Reviewed-on: graham/towerops-web#225
2026-03-29 13:14:53 -05:00
26befb9f4f fix: use chroot isolation for Podman builds on unprivileged runner (#224)
Podman fails with 'cannot clone: Operation not permitted' on this runner
because it cannot create namespaces. The runner environment lacks the
capabilities for namespace-based container isolation.

Solution: Add --isolation=chroot flag to use chroot instead of namespaces.
This works in restricted environments without CAP_SYS_ADMIN or user
namespace support.

Trade-off: Chroot isolation is slower but functional on unprivileged
runners.

Reviewed-on: graham/towerops-web#224
2026-03-29 13:09:44 -05:00