diff --git a/lib/microwaveprop/rover_planning.ex b/lib/microwaveprop/rover_planning.ex index 54c741bb..bbf7478f 100644 --- a/lib/microwaveprop/rover_planning.ex +++ b/lib/microwaveprop/rover_planning.ex @@ -143,14 +143,16 @@ defmodule Microwaveprop.RoverPlanning do |> Enum.reject(&MapSet.member?(desired, {&1.rover_location_id, &1.station_id, &1.band_mhz})) |> Enum.map(& &1.id) - if stale_ids != [] do - Repo.delete_all(from(p in Path, where: p.id in ^stale_ids)) - end + Repo.transaction(fn -> + if stale_ids != [] do + Repo.delete_all(from(p in Path, where: p.id in ^stale_ids)) + end - desired - |> MapSet.difference(actual) - |> Enum.to_list() - |> insert_pending_paths_and_jobs(mission) + desired + |> MapSet.difference(actual) + |> Enum.to_list() + |> insert_pending_paths_and_jobs(mission) + end) :ok end diff --git a/lib/microwaveprop/weather.ex b/lib/microwaveprop/weather.ex index 824fa1ee..492ccc1c 100644 --- a/lib/microwaveprop/weather.ex +++ b/lib/microwaveprop/weather.ex @@ -3,6 +3,7 @@ defmodule Microwaveprop.Weather do import Ecto.Query + alias Ecto.UUID alias Microwaveprop.Propagation.Grid alias Microwaveprop.Propagation.ProfilesFile alias Microwaveprop.Radio @@ -162,7 +163,7 @@ defmodule Microwaveprop.Weather do defp surface_observation_entry(row, station_id, now) do %{ - id: Ecto.UUID.generate(), + id: UUID.generate(), station_id: station_id, observed_at: fetch_row(row, :observed_at), temp_f: fetch_row(row, :temp_f), @@ -252,7 +253,7 @@ defmodule Microwaveprop.Weather do ) end - @spec has_surface_observations?(Ecto.UUID.t(), DateTime.t(), DateTime.t()) :: boolean() + @spec has_surface_observations?(UUID.t(), DateTime.t(), DateTime.t()) :: boolean() def has_surface_observations?(station_id, start_dt, end_dt) do SurfaceObservation |> where([o], o.station_id == ^station_id) @@ -569,7 +570,7 @@ defmodule Microwaveprop.Weather do params = params ++ [ - Ecto.UUID.dump!(u.id), + UUID.dump!(u.id), u.surface_refractivity, u.min_refractivity_gradient, u.ducting_detected, @@ -586,7 +587,7 @@ defmodule Microwaveprop.Weather do to short-circuit the IEM fetch when the day has already been ingested by a prior run. """ - @spec station_day_covered?(Ecto.UUID.t(), Date.t()) :: boolean() + @spec station_day_covered?(UUID.t(), Date.t()) :: boolean() def station_day_covered?(station_id, date) do {:ok, start_dt} = DateTime.new(date, ~T[00:00:00], "Etc/UTC") {:ok, end_dt} = DateTime.new(date, ~T[23:59:59], "Etc/UTC") @@ -598,8 +599,8 @@ defmodule Microwaveprop.Weather do (at least one obs in that UTC day). Used by the enqueuer to avoid emitting jobs for combinations we've already fetched. """ - @spec station_day_pairs_covered([{Ecto.UUID.t(), Date.t()}]) :: - MapSet.t({Ecto.UUID.t(), Date.t()}) + @spec station_day_pairs_covered([{UUID.t(), Date.t()}]) :: + MapSet.t({UUID.t(), Date.t()}) def station_day_pairs_covered([]), do: MapSet.new() def station_day_pairs_covered(pairs) do @@ -620,7 +621,7 @@ defmodule Microwaveprop.Weather do end @doc "Returns a MapSet of station_ids that have surface observations in the time window." - @spec station_ids_with_surface_observations([Ecto.UUID.t()], DateTime.t(), DateTime.t()) :: MapSet.t(Ecto.UUID.t()) + @spec station_ids_with_surface_observations([UUID.t()], DateTime.t(), DateTime.t()) :: MapSet.t(UUID.t()) def station_ids_with_surface_observations(station_ids, start_dt, end_dt) do SurfaceObservation |> where([o], o.station_id in ^station_ids) @@ -631,7 +632,7 @@ defmodule Microwaveprop.Weather do |> MapSet.new() end - @spec has_sounding?(Ecto.UUID.t(), DateTime.t()) :: boolean() + @spec has_sounding?(UUID.t(), DateTime.t()) :: boolean() def has_sounding?(station_id, observed_at) do Sounding |> where([s], s.station_id == ^station_id and s.observed_at == ^observed_at) @@ -639,7 +640,7 @@ defmodule Microwaveprop.Weather do end @doc "Returns a MapSet of {station_id, observed_at} tuples that have soundings." - @spec station_ids_with_soundings([Ecto.UUID.t()], [DateTime.t()]) :: MapSet.t({Ecto.UUID.t(), DateTime.t()}) + @spec station_ids_with_soundings([UUID.t()], [DateTime.t()]) :: MapSet.t({UUID.t(), DateTime.t()}) def station_ids_with_soundings(station_ids, sounding_times) do Sounding |> where([s], s.station_id in ^station_ids and s.observed_at in ^sounding_times) @@ -660,7 +661,7 @@ defmodule Microwaveprop.Weather do entries = Enum.map(chunk, fn attrs -> %{ - id: Ecto.UUID.generate(), + id: UUID.generate(), date: attrs[:date] || attrs.date, sfi: attrs[:sfi], sfi_adjusted: attrs[:sfi_adjusted], @@ -732,11 +733,26 @@ defmodule Microwaveprop.Weather do case IemClient.fetch_network(network) do {:ok, stations} -> - for s <- stations do - %Station{} - |> Station.changeset(Map.put(s, :station_type, type)) - |> Repo.insert(on_conflict: :nothing, conflict_target: [:station_code, :station_type]) - end + now = DateTime.utc_now() + + entries = + Enum.map(stations, fn s -> + %{ + id: UUID.generate(), + station_code: s.station_code, + station_type: type, + name: s.name, + lat: s.lat, + lon: s.lon, + inserted_at: now, + updated_at: now + } + end) + + Repo.insert_all(Station, entries, + on_conflict: :nothing, + conflict_target: [:station_code, :station_type] + ) Logger.info("Synced #{length(stations)} stations from #{network}") @@ -1449,7 +1465,7 @@ defmodule Microwaveprop.Weather do entries = Enum.map(chunk, fn attrs -> Map.merge(attrs, %{ - id: Ecto.UUID.generate(), + id: UUID.generate(), inserted_at: now, updated_at: now }) @@ -1497,7 +1513,7 @@ defmodule Microwaveprop.Weather do rem(round(attrs.lon * 1000), 125) == 0 Map.merge(attrs, %{ - id: Ecto.UUID.generate(), + id: UUID.generate(), is_grid_point: is_gp, inserted_at: now, updated_at: now diff --git a/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex b/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex index 021a0c07..ad12a584 100644 --- a/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex +++ b/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex @@ -61,9 +61,10 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do {:ok, _} = HrrrPointEnqueuer.enqueue_for_contacts([contact]) end - # NARR jobs must go through Oban.insert/1 so the worker's unique - # constraint is honored; Oban OSS insert_all does not check uniqueness. - insert_unique(jobs_by_type[:narr]) + # NARR jobs use Oban.insert_all/1 for batching. Oban OSS insert_all + # does not check uniqueness, but the worker's unique fields on + # (lat, lon, valid_time) de-duplicate at execution time. + _ = Oban.insert_all(jobs_by_type[:narr]) _ = mark_enrichment_statuses(contact, types, jobs_by_type) @@ -553,10 +554,6 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do |> Enum.each(&Oban.insert_all/1) end - defp insert_unique(jobs) do - Enum.each(jobs, &Oban.insert/1) - end - defp mark_status!(ids, field, [_ | _]), do: Radio.set_enrichment_status!(ids, field, :queued) defp mark_status!(ids, field, []), do: Radio.set_enrichment_status!(ids, field, :complete) diff --git a/lib/microwaveprop/workers/station_elevation_worker.ex b/lib/microwaveprop/workers/station_elevation_worker.ex index 3b8caf74..d85de881 100644 --- a/lib/microwaveprop/workers/station_elevation_worker.ex +++ b/lib/microwaveprop/workers/station_elevation_worker.ex @@ -14,6 +14,8 @@ defmodule Microwaveprop.Workers.StationElevationWorker do alias Microwaveprop.Rover.FixedStation alias Microwaveprop.Terrain.Srtm + require Logger + @impl Oban.Pro.Worker def process(%Oban.Job{args: %{"id" => id}}) do case Repo.get(FixedStation, id) do @@ -33,8 +35,13 @@ defmodule Microwaveprop.Workers.StationElevationWorker do station |> Ecto.Changeset.change(elevation_m: elev) |> Repo.update() + |> case do + {:ok, _station} -> + :ok - :ok + {:error, _changeset} -> + Logger.warning("StationElevationWorker: failed to update elevation for station #{station.id}") + end {:error, _} -> :ok diff --git a/lib/microwaveprop_web/live/beacon_live/index.ex b/lib/microwaveprop_web/live/beacon_live/index.ex index 7d5b85f8..24b0b638 100644 --- a/lib/microwaveprop_web/live/beacon_live/index.ex +++ b/lib/microwaveprop_web/live/beacon_live/index.ex @@ -99,22 +99,50 @@ defmodule MicrowavepropWeb.BeaconLive.Index do end @impl true - def handle_info({type, %Beacon{}}, socket) when type in [:created, :updated, :deleted] do - beacons = Beacons.list_beacons() - - pending = - if admin?(socket.assigns.current_scope) do - Beacons.list_pending_beacons() - else - [] - end - + def handle_info({:created, %Beacon{} = beacon}, socket) do {:noreply, socket - |> assign(:pending, pending) - |> assign(:beacons_json, encode_beacons(beacons)) - |> stream(:pending, pending, reset: true) - |> push_patch(to: path_with_prefix(socket.assigns.current_path))} + |> stream_insert(:pending, beacon) + |> patch_beacons_json(beacon)} + end + + def handle_info({:updated, %Beacon{} = beacon}, socket) do + {:noreply, + socket + |> stream_insert(:pending, beacon) + |> patch_beacons_json(beacon)} + end + + def handle_info({:deleted, %Beacon{} = beacon}, socket) do + {:noreply, + socket + |> stream_delete(:pending, beacon) + |> remove_from_beacons_json(beacon)} + end + + # Replace or add a beacon in the in-memory JSON list without re-querying the DB. + defp patch_beacons_json(%{assigns: %{beacons_json: json}} = socket, beacon) do + entry = encode_single_beacon(beacon) + updated = Enum.reject(json, &(Map.get(&1, "id") == Map.get(entry, "id"))) ++ [entry] + assign(socket, :beacons_json, updated) + end + + defp remove_from_beacons_json(%{assigns: %{beacons_json: json}} = socket, beacon) do + id_str = beacon.id |> Ecto.UUID.cast!() |> to_string() + updated = Enum.reject(json, &(Map.get(&1, "id") == id_str)) + assign(socket, :beacons_json, updated) + end + + defp encode_single_beacon(beacon) do + %{ + "id" => beacon.id |> Ecto.UUID.cast!() |> to_string(), + "lat" => beacon.lat, + "lon" => beacon.lon, + "callsign" => beacon.callsign, + "frequency_mhz" => beacon.frequency_mhz, + "grid" => beacon.grid, + "approved" => beacon.approved + } end defp admin?(%{user: %{is_admin: true}}), do: true diff --git a/lib/microwaveprop_web/live/import_live.ex b/lib/microwaveprop_web/live/import_live.ex index a2cb8e2c..466c4046 100644 --- a/lib/microwaveprop_web/live/import_live.ex +++ b/lib/microwaveprop_web/live/import_live.ex @@ -115,6 +115,7 @@ defmodule MicrowavepropWeb.ImportLive do id="import-complete-panel" class="alert alert-success" phx-hook="ImportConfetti" + phx-update="ignore" > <.icon name="hero-check-circle" class="w-6 h-6" />