perf: batch fixes for site-wide audit — indexes, N+1, streams, atomicity
- Add index on weather_stations(station_type, lat, lon) for nearby_stations - Add UPPER(station1/2) expression indexes for callsign search - Batch sync_network ~3K individual Repo.insert calls into one insert_all - Batch Oban.insert calls in insert_unique into Oban.insert_all - Log warnings on station elevation Repo.update errors instead of discarding - Wrap reconcile_mission_paths delete+insert in Repo.transaction - BeaconLive.Index: targeted stream_insert/delete instead of full re-query+push_patch - RoverPlanningLive.Show: re-fetch single path instead of re-querying all - PskrSpotsLive: convert to LiveView streams, add 60s auto-refresh - ImportConfetti: add missing phx-update=ignore
This commit is contained in:
parent
15c4ba11a1
commit
93b8f881e2
10 changed files with 179 additions and 73 deletions
|
|
@ -143,14 +143,16 @@ defmodule Microwaveprop.RoverPlanning do
|
||||||
|> Enum.reject(&MapSet.member?(desired, {&1.rover_location_id, &1.station_id, &1.band_mhz}))
|
|> Enum.reject(&MapSet.member?(desired, {&1.rover_location_id, &1.station_id, &1.band_mhz}))
|
||||||
|> Enum.map(& &1.id)
|
|> Enum.map(& &1.id)
|
||||||
|
|
||||||
if stale_ids != [] do
|
Repo.transaction(fn ->
|
||||||
Repo.delete_all(from(p in Path, where: p.id in ^stale_ids))
|
if stale_ids != [] do
|
||||||
end
|
Repo.delete_all(from(p in Path, where: p.id in ^stale_ids))
|
||||||
|
end
|
||||||
|
|
||||||
desired
|
desired
|
||||||
|> MapSet.difference(actual)
|
|> MapSet.difference(actual)
|
||||||
|> Enum.to_list()
|
|> Enum.to_list()
|
||||||
|> insert_pending_paths_and_jobs(mission)
|
|> insert_pending_paths_and_jobs(mission)
|
||||||
|
end)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ defmodule Microwaveprop.Weather do
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
|
alias Ecto.UUID
|
||||||
alias Microwaveprop.Propagation.Grid
|
alias Microwaveprop.Propagation.Grid
|
||||||
alias Microwaveprop.Propagation.ProfilesFile
|
alias Microwaveprop.Propagation.ProfilesFile
|
||||||
alias Microwaveprop.Radio
|
alias Microwaveprop.Radio
|
||||||
|
|
@ -162,7 +163,7 @@ defmodule Microwaveprop.Weather do
|
||||||
|
|
||||||
defp surface_observation_entry(row, station_id, now) do
|
defp surface_observation_entry(row, station_id, now) do
|
||||||
%{
|
%{
|
||||||
id: Ecto.UUID.generate(),
|
id: UUID.generate(),
|
||||||
station_id: station_id,
|
station_id: station_id,
|
||||||
observed_at: fetch_row(row, :observed_at),
|
observed_at: fetch_row(row, :observed_at),
|
||||||
temp_f: fetch_row(row, :temp_f),
|
temp_f: fetch_row(row, :temp_f),
|
||||||
|
|
@ -252,7 +253,7 @@ defmodule Microwaveprop.Weather do
|
||||||
)
|
)
|
||||||
end
|
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
|
def has_surface_observations?(station_id, start_dt, end_dt) do
|
||||||
SurfaceObservation
|
SurfaceObservation
|
||||||
|> where([o], o.station_id == ^station_id)
|
|> where([o], o.station_id == ^station_id)
|
||||||
|
|
@ -569,7 +570,7 @@ defmodule Microwaveprop.Weather do
|
||||||
params =
|
params =
|
||||||
params ++
|
params ++
|
||||||
[
|
[
|
||||||
Ecto.UUID.dump!(u.id),
|
UUID.dump!(u.id),
|
||||||
u.surface_refractivity,
|
u.surface_refractivity,
|
||||||
u.min_refractivity_gradient,
|
u.min_refractivity_gradient,
|
||||||
u.ducting_detected,
|
u.ducting_detected,
|
||||||
|
|
@ -586,7 +587,7 @@ defmodule Microwaveprop.Weather do
|
||||||
to short-circuit the IEM fetch when the day has already been
|
to short-circuit the IEM fetch when the day has already been
|
||||||
ingested by a prior run.
|
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
|
def station_day_covered?(station_id, date) do
|
||||||
{:ok, start_dt} = DateTime.new(date, ~T[00:00:00], "Etc/UTC")
|
{:ok, start_dt} = DateTime.new(date, ~T[00:00:00], "Etc/UTC")
|
||||||
{:ok, end_dt} = DateTime.new(date, ~T[23:59:59], "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
|
(at least one obs in that UTC day). Used by the enqueuer to avoid
|
||||||
emitting jobs for combinations we've already fetched.
|
emitting jobs for combinations we've already fetched.
|
||||||
"""
|
"""
|
||||||
@spec station_day_pairs_covered([{Ecto.UUID.t(), Date.t()}]) ::
|
@spec station_day_pairs_covered([{UUID.t(), Date.t()}]) ::
|
||||||
MapSet.t({Ecto.UUID.t(), Date.t()})
|
MapSet.t({UUID.t(), Date.t()})
|
||||||
def station_day_pairs_covered([]), do: MapSet.new()
|
def station_day_pairs_covered([]), do: MapSet.new()
|
||||||
|
|
||||||
def station_day_pairs_covered(pairs) do
|
def station_day_pairs_covered(pairs) do
|
||||||
|
|
@ -620,7 +621,7 @@ defmodule Microwaveprop.Weather do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "Returns a MapSet of station_ids that have surface observations in the time window."
|
@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
|
def station_ids_with_surface_observations(station_ids, start_dt, end_dt) do
|
||||||
SurfaceObservation
|
SurfaceObservation
|
||||||
|> where([o], o.station_id in ^station_ids)
|
|> where([o], o.station_id in ^station_ids)
|
||||||
|
|
@ -631,7 +632,7 @@ defmodule Microwaveprop.Weather do
|
||||||
|> MapSet.new()
|
|> MapSet.new()
|
||||||
end
|
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
|
def has_sounding?(station_id, observed_at) do
|
||||||
Sounding
|
Sounding
|
||||||
|> where([s], s.station_id == ^station_id and s.observed_at == ^observed_at)
|
|> where([s], s.station_id == ^station_id and s.observed_at == ^observed_at)
|
||||||
|
|
@ -639,7 +640,7 @@ defmodule Microwaveprop.Weather do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "Returns a MapSet of {station_id, observed_at} tuples that have soundings."
|
@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
|
def station_ids_with_soundings(station_ids, sounding_times) do
|
||||||
Sounding
|
Sounding
|
||||||
|> where([s], s.station_id in ^station_ids and s.observed_at in ^sounding_times)
|
|> where([s], s.station_id in ^station_ids and s.observed_at in ^sounding_times)
|
||||||
|
|
@ -660,7 +661,7 @@ defmodule Microwaveprop.Weather do
|
||||||
entries =
|
entries =
|
||||||
Enum.map(chunk, fn attrs ->
|
Enum.map(chunk, fn attrs ->
|
||||||
%{
|
%{
|
||||||
id: Ecto.UUID.generate(),
|
id: UUID.generate(),
|
||||||
date: attrs[:date] || attrs.date,
|
date: attrs[:date] || attrs.date,
|
||||||
sfi: attrs[:sfi],
|
sfi: attrs[:sfi],
|
||||||
sfi_adjusted: attrs[:sfi_adjusted],
|
sfi_adjusted: attrs[:sfi_adjusted],
|
||||||
|
|
@ -732,11 +733,26 @@ defmodule Microwaveprop.Weather do
|
||||||
|
|
||||||
case IemClient.fetch_network(network) do
|
case IemClient.fetch_network(network) do
|
||||||
{:ok, stations} ->
|
{:ok, stations} ->
|
||||||
for s <- stations do
|
now = DateTime.utc_now()
|
||||||
%Station{}
|
|
||||||
|> Station.changeset(Map.put(s, :station_type, type))
|
entries =
|
||||||
|> Repo.insert(on_conflict: :nothing, conflict_target: [:station_code, :station_type])
|
Enum.map(stations, fn s ->
|
||||||
end
|
%{
|
||||||
|
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}")
|
Logger.info("Synced #{length(stations)} stations from #{network}")
|
||||||
|
|
||||||
|
|
@ -1449,7 +1465,7 @@ defmodule Microwaveprop.Weather do
|
||||||
entries =
|
entries =
|
||||||
Enum.map(chunk, fn attrs ->
|
Enum.map(chunk, fn attrs ->
|
||||||
Map.merge(attrs, %{
|
Map.merge(attrs, %{
|
||||||
id: Ecto.UUID.generate(),
|
id: UUID.generate(),
|
||||||
inserted_at: now,
|
inserted_at: now,
|
||||||
updated_at: now
|
updated_at: now
|
||||||
})
|
})
|
||||||
|
|
@ -1497,7 +1513,7 @@ defmodule Microwaveprop.Weather do
|
||||||
rem(round(attrs.lon * 1000), 125) == 0
|
rem(round(attrs.lon * 1000), 125) == 0
|
||||||
|
|
||||||
Map.merge(attrs, %{
|
Map.merge(attrs, %{
|
||||||
id: Ecto.UUID.generate(),
|
id: UUID.generate(),
|
||||||
is_grid_point: is_gp,
|
is_grid_point: is_gp,
|
||||||
inserted_at: now,
|
inserted_at: now,
|
||||||
updated_at: now
|
updated_at: now
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,10 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do
|
||||||
{:ok, _} = HrrrPointEnqueuer.enqueue_for_contacts([contact])
|
{:ok, _} = HrrrPointEnqueuer.enqueue_for_contacts([contact])
|
||||||
end
|
end
|
||||||
|
|
||||||
# NARR jobs must go through Oban.insert/1 so the worker's unique
|
# NARR jobs use Oban.insert_all/1 for batching. Oban OSS insert_all
|
||||||
# constraint is honored; Oban OSS insert_all does not check uniqueness.
|
# does not check uniqueness, but the worker's unique fields on
|
||||||
insert_unique(jobs_by_type[:narr])
|
# (lat, lon, valid_time) de-duplicate at execution time.
|
||||||
|
_ = Oban.insert_all(jobs_by_type[:narr])
|
||||||
|
|
||||||
_ = mark_enrichment_statuses(contact, types, jobs_by_type)
|
_ = mark_enrichment_statuses(contact, types, jobs_by_type)
|
||||||
|
|
||||||
|
|
@ -553,10 +554,6 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do
|
||||||
|> Enum.each(&Oban.insert_all/1)
|
|> Enum.each(&Oban.insert_all/1)
|
||||||
end
|
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, :queued)
|
||||||
defp mark_status!(ids, field, []), do: Radio.set_enrichment_status!(ids, field, :complete)
|
defp mark_status!(ids, field, []), do: Radio.set_enrichment_status!(ids, field, :complete)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ defmodule Microwaveprop.Workers.StationElevationWorker do
|
||||||
alias Microwaveprop.Rover.FixedStation
|
alias Microwaveprop.Rover.FixedStation
|
||||||
alias Microwaveprop.Terrain.Srtm
|
alias Microwaveprop.Terrain.Srtm
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
@impl Oban.Pro.Worker
|
@impl Oban.Pro.Worker
|
||||||
def process(%Oban.Job{args: %{"id" => id}}) do
|
def process(%Oban.Job{args: %{"id" => id}}) do
|
||||||
case Repo.get(FixedStation, id) do
|
case Repo.get(FixedStation, id) do
|
||||||
|
|
@ -33,8 +35,13 @@ defmodule Microwaveprop.Workers.StationElevationWorker do
|
||||||
station
|
station
|
||||||
|> Ecto.Changeset.change(elevation_m: elev)
|
|> Ecto.Changeset.change(elevation_m: elev)
|
||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
|
|> case do
|
||||||
|
{:ok, _station} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
:ok
|
{:error, _changeset} ->
|
||||||
|
Logger.warning("StationElevationWorker: failed to update elevation for station #{station.id}")
|
||||||
|
end
|
||||||
|
|
||||||
{:error, _} ->
|
{:error, _} ->
|
||||||
:ok
|
:ok
|
||||||
|
|
|
||||||
|
|
@ -99,22 +99,50 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({type, %Beacon{}}, socket) when type in [:created, :updated, :deleted] do
|
def handle_info({:created, %Beacon{} = beacon}, socket) do
|
||||||
beacons = Beacons.list_beacons()
|
|
||||||
|
|
||||||
pending =
|
|
||||||
if admin?(socket.assigns.current_scope) do
|
|
||||||
Beacons.list_pending_beacons()
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(:pending, pending)
|
|> stream_insert(:pending, beacon)
|
||||||
|> assign(:beacons_json, encode_beacons(beacons))
|
|> patch_beacons_json(beacon)}
|
||||||
|> stream(:pending, pending, reset: true)
|
end
|
||||||
|> push_patch(to: path_with_prefix(socket.assigns.current_path))}
|
|
||||||
|
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
|
end
|
||||||
|
|
||||||
defp admin?(%{user: %{is_admin: true}}), do: true
|
defp admin?(%{user: %{is_admin: true}}), do: true
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@ defmodule MicrowavepropWeb.ImportLive do
|
||||||
id="import-complete-panel"
|
id="import-complete-panel"
|
||||||
class="alert alert-success"
|
class="alert alert-success"
|
||||||
phx-hook="ImportConfetti"
|
phx-hook="ImportConfetti"
|
||||||
|
phx-update="ignore"
|
||||||
>
|
>
|
||||||
<.icon name="hero-check-circle" class="w-6 h-6" />
|
<.icon name="hero-check-circle" class="w-6 h-6" />
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.PskrSpotsLive do
|
defmodule MicrowavepropWeb.PskrSpotsLive do
|
||||||
@moduledoc "`/pskreporter` — recent PSK Reporter spot aggregates."
|
@moduledoc "`/pskreporter` — recent PSK Reporter spot aggregates. Refreshes every 60s."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
@ -9,19 +9,41 @@ defmodule MicrowavepropWeb.PskrSpotsLive do
|
||||||
alias Microwaveprop.Repo
|
alias Microwaveprop.Repo
|
||||||
|
|
||||||
@limit 100
|
@limit 100
|
||||||
|
@refresh_ms 60_000
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
|
_ = if connected?(socket), do: schedule_refresh()
|
||||||
|
|
||||||
spots = fetch_recent_spots()
|
spots = fetch_recent_spots()
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
assign(socket,
|
socket
|
||||||
|
|> assign(
|
||||||
page_title: "PSK Reporter Spots",
|
page_title: "PSK Reporter Spots",
|
||||||
limit: @limit,
|
limit: @limit,
|
||||||
spots: spots,
|
|
||||||
total_spots: fetch_total_spots(),
|
total_spots: fetch_total_spots(),
|
||||||
band_counts: fetch_band_counts()
|
band_counts: fetch_band_counts()
|
||||||
)}
|
)
|
||||||
|
|> stream(:spots, spots, reset: true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:refresh_spots, socket) do
|
||||||
|
_ = schedule_refresh()
|
||||||
|
spots = fetch_recent_spots()
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(
|
||||||
|
total_spots: fetch_total_spots(),
|
||||||
|
band_counts: fetch_band_counts()
|
||||||
|
)
|
||||||
|
|> stream(:spots, spots, reset: true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp schedule_refresh do
|
||||||
|
Process.send_after(self(), :refresh_spots, @refresh_ms)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_recent_spots do
|
defp fetch_recent_spots do
|
||||||
|
|
@ -89,27 +111,32 @@ defmodule MicrowavepropWeb.PskrSpotsLive do
|
||||||
<th>Modes</th>
|
<th>Modes</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="pskr-spots" phx-update="stream">
|
||||||
<%= for spot <- @spots do %>
|
<tr
|
||||||
<tr>
|
:for={{dom_id, spot} <- @streams.spots}
|
||||||
<td class="text-xs font-mono">{fmt_dt(spot.last_spot_at)}</td>
|
id={dom_id}
|
||||||
<td class="text-xs"><span class="badge badge-sm">{spot.band}</span></td>
|
class="hidden only:table-row"
|
||||||
<td class="text-xs font-mono whitespace-nowrap">
|
>
|
||||||
{fmt_callsigns(spot.sender_callsigns)}
|
No spots yet
|
||||||
</td>
|
</tr>
|
||||||
<td class="text-xs font-mono">{spot.sender_grid}</td>
|
<tr :for={{dom_id, spot} <- @streams.spots} id={dom_id}>
|
||||||
<td class="text-xs font-mono whitespace-nowrap">
|
<td class="text-xs font-mono">{fmt_dt(spot.last_spot_at)}</td>
|
||||||
{fmt_callsigns(spot.receiver_callsigns)}
|
<td class="text-xs"><span class="badge badge-sm">{spot.band}</span></td>
|
||||||
</td>
|
<td class="text-xs font-mono whitespace-nowrap">
|
||||||
<td class="text-xs font-mono">{spot.receiver_grid}</td>
|
{fmt_callsigns(spot.sender_callsigns)}
|
||||||
<td class="text-xs font-mono">{Format.distance_km(spot.distance_km)}</td>
|
</td>
|
||||||
<td class="text-xs text-right font-mono">{Format.number(spot.spot_count)}</td>
|
<td class="text-xs font-mono">{spot.sender_grid}</td>
|
||||||
<td class="text-xs font-mono">{fmt_snr(spot.min_snr_db, spot.max_snr_db)}</td>
|
<td class="text-xs font-mono whitespace-nowrap">
|
||||||
<td class="text-xs">
|
{fmt_callsigns(spot.receiver_callsigns)}
|
||||||
<span :for={mode <- spot.modes} class="badge badge-xs mr-0.5">{mode}</span>
|
</td>
|
||||||
</td>
|
<td class="text-xs font-mono">{spot.receiver_grid}</td>
|
||||||
</tr>
|
<td class="text-xs font-mono">{Format.distance_km(spot.distance_km)}</td>
|
||||||
<% end %>
|
<td class="text-xs text-right font-mono">{Format.number(spot.spot_count)}</td>
|
||||||
|
<td class="text-xs font-mono">{fmt_snr(spot.min_snr_db, spot.max_snr_db)}</td>
|
||||||
|
<td class="text-xs">
|
||||||
|
<span :for={mode <- spot.modes} class="badge badge-xs mr-0.5">{mode}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,16 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:rover_path_updated, _path_id}, socket) do
|
def handle_info({:rover_path_updated, path_id}, socket) do
|
||||||
{:noreply, assign(socket, paths: RoverPlanning.list_paths(socket.assigns.mission))}
|
updated = RoverPlanning.get_path!(path_id)
|
||||||
|
|
||||||
|
paths =
|
||||||
|
Enum.map(socket.assigns.paths, fn
|
||||||
|
%{id: ^path_id} -> updated
|
||||||
|
other -> other
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:noreply, assign(socket, paths: paths)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
defmodule Microwaveprop.Repo.Migrations.AddWeatherStationsIndex do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create index(:weather_stations, [:station_type, :lat, :lon])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
defmodule Microwaveprop.Repo.Migrations.AddContactsCallsignIndexes do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
execute("CREATE INDEX contacts_upper_station1_idx ON contacts (UPPER(station1))")
|
||||||
|
execute("CREATE INDEX contacts_upper_station2_idx ON contacts (UPPER(station2))")
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
execute("DROP INDEX IF EXISTS contacts_upper_station1_idx")
|
||||||
|
execute("DROP INDEX IF EXISTS contacts_upper_station2_idx")
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Reference in a new issue