- Add Codeberg Ansible collection link to API/GraphQL docs and Terraform guide
- Un-skip the previously-disabled DevicePollerWorker tests that still pass
- Expand MibController, CnMaestro.Sync, CheckWorker, ActivityFeed,
MobileController, JobCleanupTask tests
- New UploadMibs Mix task test covering arg validation and upload paths
- Set :mib_dir in test config so MibController upload/delete flows can
exercise the real on-disk paths against a writable tmp directory
The current ApiDocs/GraphQLDocs templates don't surface the sample_token
back into the rendered HTML, so the previous assertions were checking
for strings that don't exist. Drop them down to a 'page renders' check.
The controller branches that compute sample_token still get exercised.
Add focused tests across many modules to push overall coverage from
75.45% to ~77%.
New test files:
- test/towerops/snmp/topology_test.exs
- test/towerops/vault_test.exs
- test/towerops/workers/check_worker_test.exs
- test/towerops_web/graphql/resolvers/happy_path_test.exs
- test/towerops_web/graphql/schema_test.exs
- test/towerops_web/live/reports_live_test.exs
Expanded existing tests for: Airos vendor, ProfileWatcher, StormDetector,
LLDP, GpsSync, MikrotikBackupWorker, AdminController, MibController,
MobileController, GeoipController, OnboardingLive, UserResetPasswordLive,
SessionManager, Telemetry, CoverageLive.Show.
Also fix a pre-existing dead test in ApplicationSettingTest where the
schema default for value_type made the 'invalid without value_type' test
unreachable.
Apply Testing LiveView course patterns (live/2, has_element?,
render_click, form/3). Add tests for MobileQRLive, WeathermapLive,
StatusPageLive, controllers, plugs, and pure function modules.
Expand proto decode coverage to 85.24%.
Dialyzer:
* Add Lidar.Tile @type, narrow several @specs to match success-typing,
prefix discarded function returns with `_ =`, fix the broken
File.stream! call in the ms-buildings worker (it was causing dialyzer
to mark every private fun unreachable), broaden Antenna.spec key
types from `float()` to `number()` to match catalog int literals.
* Add :geo, :geo_postgis, :db_connection to plt_add_apps so the dep
PLT actually contains the modules we use.
* Result: 0 unsuppressed dialyzer warnings (was 50+ in project code).
Tree canopy:
* Replace the dead landfire.gov URL with the ETH Zurich Global
Canopy Height 10 m product. ETH publishes proper COGs with
overviews on libdrive.ethz.ch — confirmed working with
/vsicurl/ byte-range reads. The module now picks the 3°×3° tile
containing the bbox centroid, with both a `:metres` decoder
(ETH Byte raster, NoData=255) and a `:landfire_evh` decoder for
operators self-hosting LANDFIRE EVH COGs.
* Re-enable canopy by default now that there's a working source.
Tests (+105 tests, 0 failures):
* CoverageLive Index/Show/Form/Map — empty/loaded/auth states,
LiveView event handlers, formatters.
* Coverages.Building changeset + helpers.
* Coverages.TreeCanopy provider tile-URL builder and decoders.
* Workers.MsBuildingsImportWorker (full import flow incl. blank
lines, MultiPolygon, idempotency, hash fallback) — also fixed
the partial-index ON CONFLICT bug that prevented imports from
ever working: the unique index has WHERE ms_footprint_id IS
NOT NULL, so the worker now uses an :unsafe_fragment conflict
target that includes the predicate.
* GraphQLDocsController smoke test (was 0%).
Coverage rose from 73.19% → 74% with the major project modules I
touched all moved from 0% / <50% to >75%.
Updates all docs, UI copy, release checker, proto go_package,
container image refs, and tests to point at
codeberg.org/towerops-agent/towerops-agent. Also deletes stray
empty package.json/package-lock.json (Phoenix uses esbuild — no
npm) and an unused test_encode_decode.exs scratch file.
cnHeat-2.0-style automation hooks. Coverage prediction is now a
first-class API resource that Terraform / Ansible / cron can drive.
REST API (under /api/v1, Bearer-token auth scoped to one org)
- GET /coverages list
- POST /coverages create + auto-queue compute
- GET /coverages/:id read (includes status / progress_pct
for polling-based providers)
- PATCH /coverages/:id update editable fields
- DELETE /coverages/:id delete + cleanup
- POST /coverages/:id/recompute requeue, returns 202
- GET /coverages/:id/kmz Google Earth bundle (KML + PNG)
Each response includes the full resource — Terraform read-after-write
reconciles cleanly; status polling hits a stable JSON shape.
Email-on-complete (Towerops.Coverages.Notifier)
- Worker fires deliver_compute_complete on both :ready and :failed
paths. Sends a short text email to every member of the owning
organisation with a link to the show page (or the failure
reason). Mirrors cnHeat 2.0's per-project email alerts.
- Failures are non-fatal: a stuck SMTP relay never blocks the
underlying coverage record from transitioning state.
KMZ export
- Builds a flat KMZ in-memory via :zip.create — doc.kml +
overlay.png, with the LatLonBox set from the coverage's bbox.
XML special chars are escaped. 409 if not yet ready.
Tests (11 new, all green)
- Bearer-token auth happy path + 401 missing
- index / show / create / update / delete + 404 cross-org
- create returns status:'queued' confirming the auto-enqueue
- recompute returns 202 with status:'queued'
Plus docs/terraform-coverages.md showing how to drive the new API
from a third-party restapi provider until the first-party
terraform-provider-towerops gets a coverage resource.
Closes the last clutter source. NLOS heatmaps now see trees as
obstacles in addition to buildings:
- New Towerops.Coverages.TreeCanopy module fetches an AAIGrid of
canopy heights for the coverage bbox via the same /vsicurl/
+ Lidar.Reader pipeline used for terrain. Source defaults to
the public LANDFIRE 2022 EVH COG (~30 m, all CONUS) and is
override-able via :tree_canopy_url.
- EVH cells are integer codes; decode_evh maps the canopy range
(101..199 → height-100 m), the herbaceous range (11..29 →
(code-10) * 0.1 m), and zeroes everything else.
- Profile.sample/5 takes a :canopy grid alongside :clutter and
uses max(building_height, canopy_height) so a tree poking above
a one-storey roof still shadows.
- CoverageWorker fetches the canopy grid once per job (best
effort — failures log + continue with nil) and threads it
through the env map. LOS keeps clutter empty + canopy nil
(height-above-clutter view); NLOS gets both.
- Tests: tree_canopy_test for the disabled / override paths;
test_helper disables the LANDFIRE fetch by default so the
rest of the suite doesn't stream remote bytes.
Earlier commit a9aa8bf9 had the message but only the test tweak made
it through pre-commit stashing. This is the implementation:
- lib/towerops/lidar/sources/usgs_ned.ex
- lib/towerops/lidar.ex (mosaic_from_ned + get_elevation_from_ned
fallback paths)
- lib/towerops/coverages/building.ex + migration scaffolding
- lib/towerops/workers/coverage_worker.ex error message
- test_helper.exs and the lidar test changes
The 3DEP+TNRIS catalog was Texas-only; coverage compute crashed for
any site outside the catalog with 'No LIDAR terrain data available
for this site'. Production hit this for at least one user today.
This commit lays in the rest of the cnHeat-style backend foundation:
- Towerops.Lidar.Sources.UsgsNed: synthesizes Tile structs for the
USGS National Elevation Dataset (1/3 arc-second / ~10 m), keyed
off the predictable n{lat:02d}w{lon:03d} 1° tile naming on the
prd-tnm S3 bucket. Covers all of CONUS, AK, HI, and PR/USVI.
- Towerops.Lidar: when the curated catalog has no tile, falls back
to the synthetic NED tiles via the same Reader / GDAL /vsicurl/
pipeline. New :lidar_ned_fallback config flag (default on);
test_helper.exs disables it so tests can still assert :no_tile
without spinning up GDAL stubs.
- coverage_worker error message reflects the new geographic scope.
- coverage_buildings table + Towerops.Coverages.Building schema:
scaffolding for the next sprint where Microsoft Global ML
Building Footprints get imported and woven into the path
profile as DSM clutter.
- Tests: usgs_ned_test for tile_for_point/2 (CONUS, HI, AK, PR,
out-of-extent); grid_test fallback path verifying the synthetic
URL is hit and an AAIGrid response composes correctly.
Adds a "Radio location" section to the coverage form with an embedded
Leaflet map. The marker starts at the parent site's coordinates and
the user can drag it to the actual radio location. A "Use site
location" button clears the override.
- New CoverageLocationPicker hook in assets/js/app.ts: creates the
map, draggable marker, and pushes "location_picked" {lat, lon} on
dragend. Re-creates the marker when the user picks a site that
initially had no coords.
- form.html.heex: location section with map div (phx-hook), readout
("lat, lon - override" if set), and reset button. Hidden inputs for
latitude_override / longitude_override so the form's params always
carry the chosen coordinates.
- form.ex: handle_event "location_picked" updates the override fields
in the changeset; "use_site_location" clears them. Helper functions
radio_marker_lat/lon walk the form values, falling back to the
selected site.
CI test gate fix:
- Worker test now requires GDAL CLI tools to write GeoTIFF + PNG.
test_helper.exs detects whether gdal_translate / gdaldem are on
PATH; when missing (CI's lean test image), :requires_gdal tagged
tests are excluded. Locally with brew install gdal they still run.
Form simplification per user feedback. Form now shows only the
essentials (Name, Site, Antenna, Frequency, TX power, Range, Height,
Azimuth, Tilt, Foliage tuning) with imperial units throughout (ft, mi,
GHz). Defaults: TX power 18 dBm, height 100 ft, azimuth 0°, frequency
5.8 GHz, range 4 mi.
EIRP is calculated, not entered: shown as a live-updating badge in
the form header as `TX power + antenna.gain`.
Removed from the form (still on the schema with sensible defaults so
existing callers and the worker keep working): cell_size_m (auto-
computed from radius / 200, clamped 5–50 m), cable_loss_db, sm_gain_dbi,
tx_clearance_m, height_above_rooftop_m, receiver_height_m,
rx_threshold_dbm, latitude/longitude_override.
Schema:
- Six virtual imperial fields (height_agl_ft, height_above_rooftop_ft,
receiver_height_ft, tx_clearance_ft, radius_mi, frequency_ghz). The
changeset converts to SI before validation. Tests/workers/API keep
sending SI directly.
- ensure_cell_size_m/1: defaults the cell size when callers omit it.
Show + index pages now display GHz / ft / mi.
Propagation accuracy upgrade:
- Earth-curvature correction (4/3-Earth model, ITU-R P.453) applied
per profile sample. Important for paths over ~5 km.
- Replaced Bullington single-knife-edge with Deygout three-edge
multi-obstacle method (ITU-R P.526 §4.5): catches multi-ridge
terrain shadowing that a single dominant obstacle would miss.
Tests: 67 coverage tests pass (added Earth-curvature and Deygout
multi-obstacle reference checks). Full suite: 10817 tests.
The compute pipeline now actually runs: per-pixel path loss over LIDAR
terrain, antenna pattern lookup, real GeoTIFF + colored PNG output,
Leaflet imageOverlay rendering with opacity slider and dBm legend.
Propagation:
- Towerops.Coverages.Propagation: Friis FSPL + Bullington single-knife-edge
diffraction (ITU-R P.526). Pure Elixir, closed-form, no NIF dependency.
Clean signature so a future ITM/Longley-Rice backend can drop in.
- Towerops.Coverages.Profile: AAIGrid sampler with elevation_at, sample
along great circle, haversine distance.
- Towerops.Coverages.Raster: Float32 GeoTIFF via gdal_translate (with VRT
wrapper) and colored PNG via gdaldem color-relief. Outputs to
priv/static/coverage/<org>/<id>/, served by existing Plug.Static.
Worker:
- Towerops.Workers.CoverageWorker now does the full pipeline: bbox compute
-> Towerops.Lidar.get_elevation_grid -> per-pixel Task.async_stream
(parallel = schedulers) running antenna pattern lookup + propagation +
RSSI threshold check, write rasters. Configurable terrain source via
:coverage_terrain_module application env so tests can stub. Friendly
failure messages for :no_tile, :grid_too_large, :nodata,
:unknown_antenna, :missing_location.
Bundled antenna catalog (~95 entries):
- AntennaCatalog with compact specs for every antenna in the request list:
Cambium, ALPHA, Antel, ITELITE, KP Performance, L-com, MARS, MikroTik,
Mimosa, MTI, RADWIN, RF Elements, Ruckus, Tarana, Simulate, Ubiquiti.
- Antenna.from_spec/1 synthesizes 360+360 attenuation patterns from
gain+beamwidth (cosine-squared main lobe, smooth transition,
front-to-back floor with mild ripple). Real .ant files in priv/antennas/
override the catalog by slug. Test fixtures moved to test/support.
Schema (migration add_radio_fields_to_coverages):
- tx_power_dbm replaces eirp_dbm; EIRP is now derived as
tx_power + antenna.gain - cable_loss in Coverage.eirp_dbm/2 and shown
live in the form header.
- cable_loss_db, sm_gain_dbi, latitude_override, longitude_override,
height_above_rooftop_m, tx_clearance_m, foliage_tuning (0-100 slider).
- Coverage.location/1 returns {lat, lon} from override or parent site.
UI:
- form.html.heex: cnHeat-style grouped form (Identity / Location /
Mounting / RF / Coverage extent), foliage tuning range slider,
computed EIRP badge in the header.
- show.html.heex: Leaflet map area with L.imageOverlay heatmap,
opacity slider, dBm color legend, antenna marker with directional
wedge for azimuth.
- assets/js/app.ts: CoverageMap hook registered.
Tests: 24 new (propagation reference values, profile sampling,
worker end-to-end with stubbed terrain that writes real GeoTIFF + PNG,
org-mismatch job-cancel guard). Full suite: 10815 tests.
End-to-end CRUD scaffold for per-site, per-antenna RF coverage
prediction. Schema, context, antenna registry, Oban worker stub, and
three LiveViews are wired up; the actual ITM + LIDAR + buildings
compute pipeline lands in a follow-up.
- Migration + Coverage schema with full validation (RF params, pixel-
budget cap, antenna_slug existence). organization_id excluded from
cast (programmatic-only).
- Coverages context: org-scoped CRUD, validate_site_in_organization
rejects cross-tenant site_id refs at insert/update, queue_compute
with PubSub broadcast on per-coverage and per-org topics.
- MSI Planet .ant parser + persistent_term registry loaded at boot
from priv/antennas/. Two synthetic .ant fixtures (omni + 90deg
sector) bundled.
- CoverageWorker on new :coverage Oban queue (concurrency 2). Job
args carry organization_id; worker refuses to run on org mismatch.
Stub flips status to "failed" with "compute not yet implemented"
so the UI flow is exercisable.
- LiveViews: index (org-wide, live status updates), form (antenna
picker grouped by manufacturer), show (status banners, params
panel, map placeholder). Sidebar nav link added.
- Routes /coverage, /coverage/new, /coverage/:id, /coverage/:id/edit
under :require_authenticated_user_with_default_org.
- 41 new tests covering schema validation, .ant parsing, context
CRUD with cross-org guards, Oban job enqueue.
Each timeout test was sleeping 55ms vs a 50ms timeout — flaky on slow
CI runners where the sleep occasionally finishes before the timer
fires. Bumps every flaky timing pair to 500ms sleep / 25ms timeout so
the timeout always wins.
CI: switch postgres service image to timescale/timescaledb-ha:pg17-all
which bundles PostGIS, fixing the lidar migration failure in PR and
production workflows.
Tests: ~30 new test files / extensions, ~600+ new tests, taking total
coverage from 70.42% to 73.4%. Targets included GraphQL resolver
unauthenticated paths, SNMP context queries (mempools, processors,
ip_addresses, neighbors, wireless_clients, arp_entries), worker
routing/lifecycle (alert notification, alert digest, weather sync,
job cleanup, cn_maestro, uisp, mikrotik backup, report worker),
SNMP monitoring executors (storage, interface, processor) end-to-end
via stub adapter, NetBox + Sonar sync integration paths, ConfigChanges
listing and correlator, organizations membership query, and live-view
smoke tests for org-scoped settings, integrations, maintenance, agent,
device deep paths, schedules. Property tests used for query
composition and pure helpers where natural.
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
device_down alerts and active incidents represent the same outage.
Filter device_down alerts from the alerts section for devices already
shown as incidents, so a down device shows as 1 issue not 2.
The 1.7 upgrade migration added the new oban_workflows tracking but left
behind legacy state from earlier versions that the upgrade guide
strongly recommends removing
(https://oban.pro/docs/pro/v1-7.html):
- The uniq_key / partition_key GENERATED ALWAYS columns. The Smart
engine no longer reads them; uniqueness and partition lookups now
use expression indexes on meta. Keeping the columns imposes write
overhead on every oban_jobs insert/update and was contributing to
intermittent transaction-rollback errors.
- The oban_jobs_*_old workflow/chain indexes. v1.7 renamed the
originals and built expression-based replacements; the _old indexes
are dead weight that still slow down writes.
Also extend ErrorTrackerIgnorer to drop transient
DBConnection.ConnectionError noise ("connection is closed",
"transaction rolling back"). These come from the Repo's intentional
queue-target / disconnect-on-error cycling that recovers from stale SSL
connections — Ecto retries automatically, so logging adds noise without
value.
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.
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.