Commit graph

557 commits

Author SHA1 Message Date
012a8ece95 Migrate images from codeberg.org to git.mcintire.me
Some checks failed
Production Deployment / Run ExUnit Tests (push) Failing after 59s
Production Deployment / Build and Push Docker Image (push) Has been skipped
2026-07-24 14:38:50 -05:00
Graham McInitre
e5a6dd173c update imagens 2026-07-16 07:54:28 -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
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
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
ca7bb75472 fix: 11 critical security/correctness bugs from code audit
- C1: Replace innerHTML template literals with DOM textContent in sites_map.ts
- C2: Derive user_id from current_scope instead of client params in policy consent
- C3: Fix broken halt() in GDPR data export (orphaned _ = ... halt())
- C4: Add owner/admin authorization check to MembersController update/delete
- C5: Require HMAC signature on agent release webhook (was optional)
- C6: Fix Repo.all_by/2 (not a real Ecto function) → Repo.all with query
- C7: Move auth exemption to before_send callback in BruteForceProtection
- C8: Block </style>/<script injection in status page custom_css validation
- C9: Create secrets.example.yaml with placeholders; secrets.yaml already gitignored
- C10: Load Stripe key from STRIPE_SECRET_KEY env var instead of hardcoded value
- C13: Remove credentials from PubSub backup broadcast; channel resolves them

C11 (no force_ssl): by design — Cloudflared terminates TLS
C12 (device quota race): false positive — FOR UPDATE serializes correctly
2026-05-12 10:20:52 -05:00
6a8ee862bd chore(k8s): align secrets template with prod secret schema
The towerops-db secret in prod has both DATABASE_URL and individual
POSTGRES_* fields; towerops-redis uses HOST/PORT/PASSWORD instead of
a single REDIS_URL. Update the committed template to match so a fresh
copy of k8s/secrets.example.yaml mirrors what's actually in the
cluster.
2026-05-09 18:08:52 -05:00
32c9575952 chore(k8s): unify secrets template into one k8s/secrets.example.yaml
Replace the single-secret towerops-llm template with one
multi-document YAML covering every Secret the Deployment references:
towerops-secrets, towerops-db, towerops-aws, towerops-redis,
towerops-billing, towerops-llm.

Workflow: copy k8s/secrets.example.yaml to k8s/secrets.yaml (which is
gitignored), fill in real values, kubectl apply. Real values never land
in git.
2026-05-09 18:01:09 -05:00
91f23416e4 feat(insights): SectorOverload + CpeRealign recommendation rules
Two new multi-source rules using data we already collect, both wired
into the existing hourly RecommendationsRunWorker.

SectorOverload — fires when a Preseem-monitored AP has <25% free
airtime AND active subscribers. Critical urgency when free airtime
drops below 15%, OR when free airtime <25% AND QoE <50. Reuses the
airtime/subscriber/QoE data already pulled by PreseemSyncWorker so
no new collection is needed. UI: orange evidence card with free
airtime, subscriber count, QoE score, and AP model.

CpeRealign — fires when a wireless client has BOTH signal_strength
<=-78 dBm AND SNR <=18 dB seen in the last 2 hours. This is the
classic alignment / obstruction signature, distinct from the
existing single-metric wireless_signal_weak / wireless_snr_low
alerts produced by WirelessInsightWorker. One insight per (AP, CPE)
pair via a metadata.dedup_key. Critical when signal <=-88 OR SNR
<=10. UI: rose evidence card with signal, SNR, TX/RX rate, distance,
hostname.

Insights.insert_insight_if_new/1 dedup logic now prefers a
metadata.dedup_key over device_id when explicitly set, allowing
per-CPE insights without collapsing multiple CPEs on the same AP
into a single insight. Existing rules without dedup_key are
unaffected.

Insight.@valid_types extended with sector_overload and cpe_realign.

LLM enrichment automatically applies to both new types via the
existing Phase 1 worker — no extra wiring.

Also adds k8s/secrets.yaml to .gitignore so operators can drop a
local Secret manifest with real values, kubectl apply manually, and
never accidentally commit it. Documented in k8s/README.md.
2026-05-09 17:07:15 -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
50324c61ce feat(metrics): expose Prometheus /metrics via PromEx on :9568
Add PromEx with Application/BEAM/Phoenix/LiveView/Ecto/Oban plugins plus
a custom plugin that bridges existing Towerops Oban/Redis telemetry events.
The metrics HTTP server runs isolated on port 9568 so scrape traffic never
traverses the public Traefik IngressRoute.

The pod template carries prometheus.io annotations (scrape, port, path, job)
so the external Prometheus on 10.0.15.31 discovers each pod through the
existing kubernetes-pods scrape job (apiserver-proxy).
2026-05-08 10:25:25 -05:00
31cbd18128 ci: cache slow runtime apt deps in a prebuilt base image
Splits the runtime apt installs (gdal-bin, snmp, libsnmp40, locales,
BEAM runtime libs) into k8s/Dockerfile.base, hosted at
codeberg.org/gmcintire/towerops-base:latest. The app Dockerfile now
does FROM that base instead of re-installing gdal (~500 MB) on every
push.

The new build-base workflow rebuilds the base image only when
k8s/Dockerfile.base or the workflow itself changes, weekly via cron
(Sundays 06:00 UTC, with CACHE_BUST=<ISO week> to force apt-get update
on a week boundary), or via workflow_dispatch.

Production workflow now uses buildx + does docker login before the
build so it can pull the private base image.
2026-05-05 11:19:49 -05:00
222253b7c8 ci: move container image to codeberg.org/gmcintire/towerops
The repo got renamed towerops-web → towerops on the Codeberg side,
so the published image follows: image is now
codeberg.org/gmcintire/towerops with the same main-<ts>-<sha> tag
pattern. Login URL is the hardcoded env.REGISTRY rather than
secrets.REGISTRY_URL so a stale URL secret can't push to the wrong
registry.
2026-05-05 10:57:24 -05:00
158ee9d323 feat: add Texas LIDAR DEM catalog with on-demand elevation reads
Catalog-only system for LIDAR-derived 1m DEMs covering Texas. We do not
mirror raster data — the DB stores tile metadata (URL + footprint
geometry) and elevation values are streamed on-demand from public USGS
3DEP / TNRIS Cloud-Optimized GeoTIFFs via GDAL /vsicurl/ HTTP byte-range
reads. Used for future tower coverage line-of-sight calculations.

- Enable PostGIS extension; tile footprints indexed via GiST
- Catalog sync from USGS 3DEP (primary) and TNRIS (fallback for gaps)
- Tile dedup by ST_Contains so TNRIS doesn't duplicate 3DEP coverage
- Single-point reads via gdallocationinfo with tile fallthrough on nodata
- Grid retrieval via gdal_translate AAIGrid with multi-tile mosaicing
- Monthly Oban cron worker keeps catalog fresh
- gdal-bin added to runtime image; geo_postgis dep added
2026-05-04 13:03:28 -05:00
bd4e29331f ops: mount HRRR data NFS share at /data
Adds the same NFS volume that microwaveprop uses (10.0.15.103:/data) so
towerops can read the shared HRRR grids and render maps from them.
2026-04-30 10:27:12 -05:00
95babfcc49 ops: tighten readiness gate to prevent downtime during rolling deploys
With 1 replica, the old pod was torn down too quickly after the new pod
passed its first health check. Increase successThreshold to 3 (15s of
consecutive checks) and minReadySeconds to 30 to ensure Traefik has fully
propagated the new endpoint before the old pod is terminated.
2026-04-29 13:49:42 -05:00
fdff67237e
Dockerfile: align Elixir/OTP/Debian versions with microwaveprop 2026-04-29 12:45:21 -05:00
17bb5a9117 ops: scale towerops deployment to 1 replica 2026-04-29 11:14:20 -05:00
9720223487
remove obsolete flux manifest and imagepolicy annotations 2026-04-29 11:00:13 -05:00
FluxCD
c76a354d89 chore: update towerops image to git.mcintire.me/graham/towerops-web:main-1777403311-23304e1 [skip ci] 2026-04-28 19:10:01 +00:00
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
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
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
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
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
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
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
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
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
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
44a80cd5eb remove-gleam (#218)
Reviewed-on: graham/towerops-web#218
2026-03-29 11:03:20 -05:00
c9cb95129c fix/production-workflow-conflict (#208)
Co-authored-by: Forgejo Actions <actions@git.mcintire.me>
Reviewed-on: graham/towerops-web#208
2026-03-28 12:42:23 -05:00
bd3f872776 fix/docker-build-permissions (#207)
Reviewed-on: graham/towerops-web#207
2026-03-28 12:31:22 -05:00
66bb12ada7 fix: remove remaining Gleam references from Dockerfile (#203)
Reviewed-on: graham/towerops-web#203
2026-03-28 12:02:15 -05:00
f703e61b12 fix: code quality improvements and test reliability (#139)
Various code quality improvements and fixes:

- Settings: return error instead of auto-creating unknown settings
- PagerDuty: proper backoff timing with to_timeout()
- Gaiia sync: remove unnecessary transaction wrapper
- HTTP/UISP: simplify test plug setup
- Agent channel: tagged tuple config for check types
- SNMP: fix Task.yield/shutdown pattern, remove stale preload
- Discovery: simplify error handling
- Tests: bump executor timeouts for CI reliability
- Dockerfile: remove unnecessary --force compile
- Renovate/CI: config cleanup

Split from #135.

Reviewed-on: graham/towerops-web#139
2026-03-24 09:12:59 -05:00
5d87459ecc fix: force recompile in Docker build to prevent stale BEAM files (#119)
Incremental compilation can miss schema field removals in dependent
modules, causing undefined_column errors in production.

Reviewed-on: graham/towerops-web#119
2026-03-22 16:47:35 -05:00
6859525a1b Integrate Gleam into the build pipeline and rewrite three pure-function modules (#98)
Set up mix_gleam archive, gleam_stdlib, and gleeunit deps. Add Gleam
compiler to all Dockerfiles, CI workflows, nix shell, and .tool-versions.

Rewrite Numeric, QueryHelpers, and URLValidator from Elixir to Gleam with
full ExUnit test coverage. Update all callers to use the Gleam modules
directly via :towerops@module syntax. Remove duplicate sanitize_like/1
private functions in snmp.ex and trace.ex.

Reviewed-on: graham/towerops-web#98
2026-03-21 07:30:36 -05:00
fc03ed7b0e Disable SSL for database connections on private network (#97)
Every timeout stack trace pointed at ssl_gen_statem.call/2 — the OTP 27
SSL gen_statem hanging during recv. PG is on private network (10.0.15.20)
and SSL verify was already disabled (verify_none), providing no security
benefit while adding CPU overhead and the gen_statem layer that caused
all connection timeouts.

Removed DATABASE_SSL and DATABASE_SSL_VERIFY env vars from both the
init container and main container. Also disabled SSL on the PG server
(ssl=off in postgresql.conf, hostssl→host in pg_hba.conf).

Reviewed-on: graham/towerops-web#97
2026-03-20 12:48:05 -05:00
8a54f9a866 Increase CPU limits to fix SSL connection timeouts (#96)
Pods were running at 500m/500m (100% of limit) with 44-47% of scheduling
periods being CPU-throttled. SSL decryption is CPU-intensive — when the
BEAM gets throttled mid ssl_gen_statem:call/2, the SSL recv stalls until
the cgroup allows CPU again, causing the 15s DBConnection timeout.

This was the actual root cause of "client timed out because it queued
and checked out the connection for longer than 15000ms" errors. Previous
fixes (TCP keepalive, pool tuning, oban pruner) were treating symptoms.

- CPU limit: 500m → 2000m (4 cores burst capacity)
- CPU request: 100m → 500m (guarantee adequate baseline)

Reviewed-on: graham/towerops-web#96
2026-03-20 12:24:06 -05:00
071729f65c Reduce pool size and deployment surge to prevent PG connection exhaustion (#94)
PG max_connections=100 (97 usable). With pool_size=15 and maxSurge=100%,
rolling deploys briefly run 4 pods × 15 = 60 pool connections plus Oban
notifiers, exceeding the limit and causing too_many_connections errors
that cascade into ssl recv: closed timeouts.

- pool_size: 15 → 10 (steady state: 2×10=20, deploy: 3×10=30)
- maxSurge: 100% → 1 (max 3 pods during rollout instead of 4)

Reviewed-on: graham/towerops-web#94
2026-03-20 10:39:52 -05:00
b03f280b65 fix-ci-docker (#59)
Co-authored-by: Forgejo Actions <actions@git.mcintire.me>
Reviewed-on: graham/towerops-web#59
2026-03-17 13:13:09 -05:00
06c5d14455 ui/remove-device-type-column (#49)
Co-authored-by: Forgejo Actions <actions@git.mcintire.me>
Reviewed-on: graham/towerops-web#49
2026-03-16 16:38:00 -05:00
8abf01ccb1 fix-mobile-qr-device-info-width (#43)
Co-authored-by: Forgejo Actions <actions@git.mcintire.me>
Reviewed-on: graham/towerops-web#43
2026-03-16 14:02:31 -05:00
Forgejo Actions
011d82ce26 chore: update production image to production-1773007128-e91d74d [skip ci] 2026-03-08 22:04:51 +00:00