fix(coverage): production show-page crash and stale device on PubSub refresh

- show.html.heex: `@coverage.device_id and ...` raised BadBooleanError
  in production because device_id is a UUID string, not a boolean. Use
  is_nil/1 checks instead.
- show.ex: handle_info/2 was reloading the coverage without preloading
  :device, so the device row vanished from the UI on every status
  broadcast. Preload :device alongside the reload.
- coverage_worker: thread the precomputed tx_height through the pixel
  loop instead of re-deriving `coverage.height_agl_m + ...` per pixel
  with inconsistent nil-safety vs. compute_pixels/6.
This commit is contained in:
Graham McIntire 2026-05-06 15:19:32 -05:00
parent 3273312f6a
commit ef4a4c99b5
3 changed files with 15 additions and 20 deletions

View file

@ -140,18 +140,11 @@ defmodule Towerops.Workers.CoverageWorker do
fn r ->
row_lat = max_lat - (r + 0.5) * cell_lat
tx = {{lat, lon}, tx_z, tx_height}
for c <- 0..(ncols - 1) do
pixel_lon = min_lon + (c + 0.5) * cell_lon
compute_pixel(
coverage,
antenna,
{lat, lon},
tx_z,
{row_lat, pixel_lon},
grid,
eirp
)
compute_pixel(coverage, antenna, tx, {row_lat, pixel_lon}, grid, eirp)
end
end,
ordered: true,
@ -173,7 +166,7 @@ defmodule Towerops.Workers.CoverageWorker do
}}
end
defp compute_pixel(coverage, antenna, tx_latlon, tx_z, rx_latlon, grid, eirp) do
defp compute_pixel(coverage, antenna, {tx_latlon, _tx_z, _tx_height} = tx, rx_latlon, grid, eirp) do
distance_m = Profile.great_circle_distance_m(tx_latlon, rx_latlon)
cond do
@ -184,15 +177,14 @@ defmodule Towerops.Workers.CoverageWorker do
:nan
true ->
do_compute_pixel(coverage, antenna, tx_latlon, tx_z, rx_latlon, grid, eirp, distance_m)
do_compute_pixel(coverage, antenna, tx, rx_latlon, grid, eirp, distance_m)
end
end
defp do_compute_pixel(
coverage,
antenna,
{tx_lat, tx_lon} = tx_latlon,
tx_z,
{{tx_lat, tx_lon} = tx_latlon, tx_z, tx_height},
{rx_lat, rx_lon} = rx_latlon,
grid,
eirp,
@ -202,10 +194,10 @@ defmodule Towerops.Workers.CoverageWorker do
samples = max(3, min(@profile_samples, ceil(distance_m / coverage.cell_size_m) + 2))
profile = Profile.sample(grid, tx_latlon, rx_latlon, samples)
# Replace TX endpoint with the absolute Z (so diffraction sees the correct
# antenna height). The receiver height is added inside Propagation.
# Replace TX endpoint with the ground elevation under the antenna (so the
# path loss model sees the correct antenna height above terrain).
[_old_first | rest] = profile
profile = [tx_z - (coverage.height_agl_m + (coverage.height_above_rooftop_m || 0.0)) | rest]
profile = [tx_z - tx_height | rest]
# 2. Antenna pattern attenuation at this bearing/elevation.
bearing_offset = bearing_offset_deg(tx_lat, tx_lon, rx_lat, rx_lon, coverage.azimuth_deg)
@ -218,7 +210,7 @@ defmodule Towerops.Workers.CoverageWorker do
distance_m,
coverage.frequency_mhz * 1.0,
profile,
coverage.height_agl_m + (coverage.height_above_rooftop_m || 0.0),
tx_height,
coverage.receiver_height_m || 3.0
)

View file

@ -61,7 +61,9 @@ defmodule ToweropsWeb.CoverageLive.Show do
@impl true
def handle_info({:coverage_status, _status, _extra}, socket) do
coverage =
Coverages.get_coverage!(socket.assigns.organization.id, socket.assigns.coverage.id)
socket.assigns.organization.id
|> Coverages.get_coverage!(socket.assigns.coverage.id)
|> Towerops.Repo.preload(:device)
eirp =
Coverage.eirp_dbm(

View file

@ -199,7 +199,8 @@
/>
<.param_row
:if={
@coverage.device_id and Ecto.assoc_loaded?(@coverage.device) and @coverage.device
not is_nil(@coverage.device_id) and Ecto.assoc_loaded?(@coverage.device) and
not is_nil(@coverage.device)
}
label={t("Device")}
value={@coverage.device.name || @coverage.device.ip_address}