fix: resolve 18 bugs across LiveViews, schemas, tests, and logic
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 4m35s
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 4m35s
- MonitorLive.Show: safe nil-guard on current_scope for anonymous access - Admin.MonitorLive.Index: add phx-update=stream to enable stream ops - ImportLive: require owner/admin authorization, not_found redirect - MapLive: store timer refs in assigns, cancel before reschedule - 10 schemas: add missing foreign_key_constraint on belongs_to - Soundings: preload :station to eliminate N+1 in path analysis - PathAnalysis: defensive preload of :station on soundings - GridTaskEnqueuer: wrap reclaim_stale_running in Repo.transaction() - HrdpsClient: replace String.to_atom with compile-time atom literals - Contacts: fix extract_latlon false return for lon=0.0 - Tests: remove duplicate Mox.defmock, unblock swallowed task exits, bump refute_receive timeouts from 50ms to 200ms
This commit is contained in:
parent
c4e71e41b3
commit
d31e783776
24 changed files with 158 additions and 94 deletions
|
|
@ -36,6 +36,13 @@ defmodule Microwaveprop.Accounts.UserApiToken do
|
||||||
|
|
||||||
@type t :: %__MODULE__{}
|
@type t :: %__MODULE__{}
|
||||||
|
|
||||||
|
@spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
|
||||||
|
def changeset(token, attrs) do
|
||||||
|
token
|
||||||
|
|> cast(attrs, [:name, :user_id])
|
||||||
|
|> foreign_key_constraint(:user_id)
|
||||||
|
end
|
||||||
|
|
||||||
@doc "Returns the bearer-token prefix (e.g. `mwp_`)."
|
@doc "Returns the bearer-token prefix (e.g. `mwp_`)."
|
||||||
@spec token_prefix() :: String.t()
|
@spec token_prefix() :: String.t()
|
||||||
def token_prefix, do: @prefix
|
def token_prefix, do: @prefix
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,7 @@ defmodule Microwaveprop.Beacons.Beacon do
|
||||||
|> validate_number(:lon, greater_than_or_equal_to: -180, less_than_or_equal_to: 180)
|
|> validate_number(:lon, greater_than_or_equal_to: -180, less_than_or_equal_to: 180)
|
||||||
|> validate_length(:callsign, min: 3, max: 10)
|
|> validate_length(:callsign, min: 3, max: 10)
|
||||||
|> validate_grid_format()
|
|> validate_grid_format()
|
||||||
|
|> foreign_key_constraint(:user_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp round_coord(nil), do: nil
|
defp round_coord(nil), do: nil
|
||||||
|
|
|
||||||
|
|
@ -169,25 +169,35 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuer do
|
||||||
cutoff = DateTime.utc_now() |> DateTime.add(-cutoff_seconds, :second) |> DateTime.truncate(:second)
|
cutoff = DateTime.utc_now() |> DateTime.add(-cutoff_seconds, :second) |> DateTime.truncate(:second)
|
||||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||||
|
|
||||||
{failed_n, _} =
|
{failed_n, requeued_n} =
|
||||||
"grid_tasks"
|
fn ->
|
||||||
|> where(
|
{failed_count, _} =
|
||||||
[t],
|
"grid_tasks"
|
||||||
t.status == "running" and t.claimed_at < ^cutoff and t.attempt >= ^@max_reclaim_attempts
|
|> where(
|
||||||
)
|
[t],
|
||||||
|> Repo.update_all(
|
t.status == "running" and t.claimed_at < ^cutoff and t.attempt >= ^@max_reclaim_attempts
|
||||||
set: [
|
)
|
||||||
status: "failed",
|
|> Repo.update_all(
|
||||||
error: "reclaim-orphan: exceeded max_reclaim_attempts",
|
set: [
|
||||||
claimed_at: nil,
|
status: "failed",
|
||||||
updated_at: now
|
error: "reclaim-orphan: exceeded max_reclaim_attempts",
|
||||||
]
|
claimed_at: nil,
|
||||||
)
|
updated_at: now
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
{requeued_n, _} =
|
{rq_count, _} =
|
||||||
"grid_tasks"
|
"grid_tasks"
|
||||||
|> where([t], t.status == "running" and t.claimed_at < ^cutoff)
|
|> where([t], t.status == "running" and t.claimed_at < ^cutoff)
|
||||||
|> Repo.update_all(set: [status: "queued", claimed_at: nil, updated_at: now])
|
|> Repo.update_all(set: [status: "queued", claimed_at: nil, updated_at: now])
|
||||||
|
|
||||||
|
{failed_count, rq_count}
|
||||||
|
end
|
||||||
|
|> Repo.transaction()
|
||||||
|
|> case do
|
||||||
|
{:ok, {fc, rqc}} -> {fc, rqc}
|
||||||
|
{:error, reason} -> raise "reclaim_stale_running transaction failed: #{inspect(reason)}"
|
||||||
|
end
|
||||||
|
|
||||||
if failed_n > 0 or requeued_n > 0 do
|
if failed_n > 0 or requeued_n > 0 do
|
||||||
Logger.info("GridTaskEnqueuer: reclaimed stale-running grid_tasks (requeued=#{requeued_n}, failed=#{failed_n})")
|
Logger.info("GridTaskEnqueuer: reclaimed stale-running grid_tasks (requeued=#{requeued_n}, failed=#{failed_n})")
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ defmodule Microwaveprop.Propagation.PathAnalysis do
|
||||||
duplicated duct-detection and mechanism-classification logic.
|
duplicated duct-detection and mechanism-classification logic.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
alias Microwaveprop.Repo
|
||||||
alias Microwaveprop.Terrain.ElevationClient
|
alias Microwaveprop.Terrain.ElevationClient
|
||||||
alias Microwaveprop.Terrain.TerrainAnalysis
|
alias Microwaveprop.Terrain.TerrainAnalysis
|
||||||
|
|
||||||
|
|
@ -564,6 +565,7 @@ defmodule Microwaveprop.Propagation.PathAnalysis do
|
||||||
defp boundary_layer_detail(_), do: nil
|
defp boundary_layer_detail(_), do: nil
|
||||||
|
|
||||||
defp sounding_ducting_detail(soundings) when is_list(soundings) do
|
defp sounding_ducting_detail(soundings) when is_list(soundings) do
|
||||||
|
soundings = Repo.preload(soundings, :station)
|
||||||
ducting = Enum.filter(soundings, & &1.ducting_detected)
|
ducting = Enum.filter(soundings, & &1.ducting_detected)
|
||||||
|
|
||||||
if ducting == [] do
|
if ducting == [] do
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ defmodule Microwaveprop.Pskr.FeatureBin do
|
||||||
record
|
record
|
||||||
|> cast(attrs, @cast_fields)
|
|> cast(attrs, @cast_fields)
|
||||||
|> validate_required(@required_fields)
|
|> validate_required(@required_fields)
|
||||||
|
|> foreign_key_constraint(:run_id)
|
||||||
|> unique_constraint([:run_id, :band, :feature, :bin_label])
|
|> unique_constraint([:run_id, :band, :feature, :bin_label])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,8 @@ defmodule Microwaveprop.Radio.Contact do
|
||||||
contact
|
contact
|
||||||
|> cast(attrs, @required_fields ++ @optional_fields)
|
|> cast(attrs, @required_fields ++ @optional_fields)
|
||||||
|> validate_required(@required_fields)
|
|> validate_required(@required_fields)
|
||||||
|
|> foreign_key_constraint(:user_id)
|
||||||
|
|> foreign_key_constraint(:flagged_by_user_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
@submission_fields ~w(station1 station2 qso_timestamp mode band grid1 grid2 submitter_email user_declared_prop_mode height1_ft height2_ft private notes)a
|
@submission_fields ~w(station1 station2 qso_timestamp mode band grid1 grid2 submitter_email user_declared_prop_mode height1_ft height2_ft private notes)a
|
||||||
|
|
@ -127,6 +129,8 @@ defmodule Microwaveprop.Radio.Contact do
|
||||||
|> validate_height(:height2_ft)
|
|> validate_height(:height2_ft)
|
||||||
|> normalize_blank_notes()
|
|> normalize_blank_notes()
|
||||||
|> validate_length(:notes, max: 2000)
|
|> validate_length(:notes, max: 2000)
|
||||||
|
|> foreign_key_constraint(:user_id)
|
||||||
|
|> foreign_key_constraint(:flagged_by_user_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Collapse whitespace-only notes to `nil` so the DB column reflects
|
# Collapse whitespace-only notes to `nil` so the DB column reflects
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ defmodule Microwaveprop.Radio.ContactCommonVolumeRadar do
|
||||||
row
|
row
|
||||||
|> cast(attrs, @required ++ @optional)
|
|> cast(attrs, @required ++ @optional)
|
||||||
|> validate_required(@required)
|
|> validate_required(@required)
|
||||||
|
|> foreign_key_constraint(:contact_id)
|
||||||
|> unique_constraint(:contact_id)
|
|> unique_constraint(:contact_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ defmodule Microwaveprop.Radio.Contacts do
|
||||||
|
|
||||||
def contact_path_points(%{pos1: pos1, pos2: pos2}), do: build_path_points(extract_latlon(pos1), extract_latlon(pos2))
|
def contact_path_points(%{pos1: pos1, pos2: pos2}), do: build_path_points(extract_latlon(pos1), extract_latlon(pos2))
|
||||||
|
|
||||||
defp extract_latlon(%{"lat" => lat} = pos) when is_number(lat), do: if(pos["lon"], do: {lat, pos["lon"]})
|
defp extract_latlon(%{"lat" => lat} = pos) when is_number(lat), do: if(is_number(pos["lon"]), do: {lat, pos["lon"]})
|
||||||
defp extract_latlon(_), do: nil
|
defp extract_latlon(_), do: nil
|
||||||
|
|
||||||
defp build_path_points({lat1, lon1}, {lat2, lon2}) do
|
defp build_path_points({lat1, lon1}, {lat2, lon2}) do
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ defmodule Microwaveprop.Rover.FixedStation do
|
||||||
|> validate_required([:lat, :lon])
|
|> validate_required([:lat, :lon])
|
||||||
|> validate_number(:lat, greater_than_or_equal_to: -90.0, less_than_or_equal_to: 90.0)
|
|> validate_number(:lat, greater_than_or_equal_to: -90.0, less_than_or_equal_to: 90.0)
|
||||||
|> validate_number(:lon, greater_than_or_equal_to: -180.0, less_than_or_equal_to: 180.0)
|
|> validate_number(:lon, greater_than_or_equal_to: -180.0, less_than_or_equal_to: 180.0)
|
||||||
|
|> foreign_key_constraint(:user_id)
|
||||||
|> unique_constraint(:callsign, name: :fixed_stations_user_id_callsign_index)
|
|> unique_constraint(:callsign, name: :fixed_stations_user_id_callsign_index)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,5 +40,6 @@ defmodule Microwaveprop.Rover.Location do
|
||||||
|> validate_number(:lon, greater_than_or_equal_to: -180.0, less_than_or_equal_to: 180.0)
|
|> validate_number(:lon, greater_than_or_equal_to: -180.0, less_than_or_equal_to: 180.0)
|
||||||
|> validate_inclusion(:status, @statuses)
|
|> validate_inclusion(:status, @statuses)
|
||||||
|> validate_length(:notes, max: 4000)
|
|> validate_length(:notes, max: 4000)
|
||||||
|
|> foreign_key_constraint(:user_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ defmodule Microwaveprop.RoverPlanning.Mission do
|
||||||
|> validate_number(:station_height_ft, greater_than_or_equal_to: 0.0)
|
|> validate_number(:station_height_ft, greater_than_or_equal_to: 0.0)
|
||||||
|> validate_length(:notes, max: 4000)
|
|> validate_length(:notes, max: 4000)
|
||||||
|> validate_at_least_one_station()
|
|> validate_at_least_one_station()
|
||||||
|
|> foreign_key_constraint(:user_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
# The multi-checkbox form sends a hidden empty-string sentinel so
|
# The multi-checkbox form sends a hidden empty-string sentinel so
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ defmodule Microwaveprop.RoverPlanning.Path do
|
||||||
|> validate_required([:mission_id, :rover_location_id, :station_id, :band_mhz, :status])
|
|> validate_required([:mission_id, :rover_location_id, :station_id, :band_mhz, :status])
|
||||||
|> validate_inclusion(:status, @statuses)
|
|> validate_inclusion(:status, @statuses)
|
||||||
|> validate_number(:band_mhz, greater_than: 0)
|
|> validate_number(:band_mhz, greater_than: 0)
|
||||||
|
|> foreign_key_constraint(:mission_id)
|
||||||
|
|> foreign_key_constraint(:rover_location_id)
|
||||||
|
|> foreign_key_constraint(:station_id)
|
||||||
|> unique_constraint([:mission_id, :rover_location_id, :station_id, :band_mhz],
|
|> unique_constraint([:mission_id, :rover_location_id, :station_id, :band_mhz],
|
||||||
name: :rover_mission_paths_unique
|
name: :rover_mission_paths_unique
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ defmodule Microwaveprop.RoverPlanning.Station do
|
||||||
|> validate_required([:lat, :lon])
|
|> validate_required([:lat, :lon])
|
||||||
|> validate_number(:lat, greater_than_or_equal_to: -90.0, less_than_or_equal_to: 90.0)
|
|> validate_number(:lat, greater_than_or_equal_to: -90.0, less_than_or_equal_to: 90.0)
|
||||||
|> validate_number(:lon, greater_than_or_equal_to: -180.0, less_than_or_equal_to: 180.0)
|
|> validate_number(:lon, greater_than_or_equal_to: -180.0, less_than_or_equal_to: 180.0)
|
||||||
|
|> foreign_key_constraint(:mission_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
# When `input` looks like a callsign or grid, geocode it and populate
|
# When `input` looks like a callsign or grid, geocode it and populate
|
||||||
|
|
|
||||||
|
|
@ -53,12 +53,12 @@ defmodule Microwaveprop.Weather.HrdpsClient do
|
||||||
|
|
||||||
@pressure_var_kinds [{:tmp, "TMP"}, {:depr, "DEPR"}, {:hgt, "HGT"}]
|
@pressure_var_kinds [{:tmp, "TMP"}, {:depr, "DEPR"}, {:hgt, "HGT"}]
|
||||||
|
|
||||||
# All variable atoms are generated from compile-time constants
|
# All pressure-level variable atoms are created as atom literals at compile
|
||||||
# (@grid_pressure_levels + @pressure_var_kinds = 21 atoms max) — safe
|
# time. The bounded cross-product of @grid_pressure_levels × @pressure_var_kinds
|
||||||
# to use String.to_atom/1 here; not reachable from user input.
|
# (7 levels × 3 vars = 21 atoms) ensures no runtime atom creation.
|
||||||
@pressure_vars (for level <- @grid_pressure_levels,
|
@pressure_vars (for level <- @grid_pressure_levels,
|
||||||
{kind, msc} <- @pressure_var_kinds do
|
{kind, msc} <- @pressure_var_kinds do
|
||||||
atom = String.to_atom("#{kind}_#{level}mb")
|
atom = :"#{kind}_#{level}mb"
|
||||||
msc_level = "ISBL_#{level |> Integer.to_string() |> String.pad_leading(4, "0")}"
|
msc_level = "ISBL_#{level |> Integer.to_string() |> String.pad_leading(4, "0")}"
|
||||||
{atom, msc, msc_level}
|
{atom, msc, msc_level}
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,8 @@ defmodule Microwaveprop.Weather.Soundings do
|
||||||
s.observed_at,
|
s.observed_at,
|
||||||
^timestamp
|
^timestamp
|
||||||
),
|
),
|
||||||
limit: 1
|
limit: 1,
|
||||||
|
preload: [:station]
|
||||||
)
|
)
|
||||||
|> Repo.one()
|
|> Repo.one()
|
||||||
|> case do
|
|> case do
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ defmodule MicrowavepropWeb.Admin.MonitorLive.Index do
|
||||||
<th class="w-1"></th>
|
<th class="w-1"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="monitors" phx-update="stream">
|
||||||
<tr :for={{id, monitor} <- @streams.monitors}>
|
<tr :for={{id, monitor} <- @streams.monitors}>
|
||||||
<td class="font-semibold">{monitor.name}</td>
|
<td class="font-semibold">{monitor.name}</td>
|
||||||
<td class="text-sm">
|
<td class="text-sm">
|
||||||
|
|
|
||||||
|
|
@ -16,22 +16,42 @@ defmodule MicrowavepropWeb.ImportLive do
|
||||||
@impl true
|
@impl true
|
||||||
def mount(%{"id" => id}, _session, socket) do
|
def mount(%{"id" => id}, _session, socket) do
|
||||||
case load_run(id) do
|
case load_run(id) do
|
||||||
nil ->
|
nil -> not_found_redirect(socket)
|
||||||
{:ok,
|
%ImportRun{} = run -> mount_authorized(socket, run)
|
||||||
socket
|
end
|
||||||
|> put_flash(:error, "Import not found.")
|
end
|
||||||
|> push_navigate(to: ~p"/submit")}
|
|
||||||
|
|
||||||
%ImportRun{} = run ->
|
defp mount_authorized(socket, run) do
|
||||||
_ =
|
case authorized_to_view?(socket, run) do
|
||||||
if connected?(socket) do
|
:ok ->
|
||||||
Phoenix.PubSub.subscribe(@pubsub, "csv_import:#{run.id}")
|
if connected?(socket) do
|
||||||
end
|
Phoenix.PubSub.subscribe(@pubsub, "csv_import:#{run.id}")
|
||||||
|
end
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, "Import ##{String.slice(run.id, 0, 8)}")
|
|> assign(:page_title, "Import ##{String.slice(run.id, 0, 8)}")
|
||||||
|> assign(:run, run)}
|
|> assign(:run, run)}
|
||||||
|
|
||||||
|
:not_found ->
|
||||||
|
not_found_redirect(socket)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp not_found_redirect(socket) do
|
||||||
|
{:ok,
|
||||||
|
socket
|
||||||
|
|> put_flash(:error, "Import not found.")
|
||||||
|
|> push_navigate(to: ~p"/")}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp authorized_to_view?(socket, run) do
|
||||||
|
viewer = socket.assigns[:current_scope] && socket.assigns.current_scope.user
|
||||||
|
|
||||||
|
cond do
|
||||||
|
viewer && viewer.is_admin -> :ok
|
||||||
|
viewer && viewer.email == run.submitter_email -> :ok
|
||||||
|
true -> :not_found
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,9 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
grid_visible: false,
|
grid_visible: false,
|
||||||
radar_visible: false,
|
radar_visible: false,
|
||||||
deploy_iso: Calendar.strftime(build_ts, "%Y-%m-%d %H:%M UTC"),
|
deploy_iso: Calendar.strftime(build_ts, "%Y-%m-%d %H:%M UTC"),
|
||||||
deploy_ago: format_deploy_ago(build_ts)
|
deploy_ago: format_deploy_ago(build_ts),
|
||||||
|
pipeline_timer: nil,
|
||||||
|
advance_timer: nil
|
||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -95,13 +97,29 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
if connected?(socket) do
|
if connected?(socket) do
|
||||||
:ok = Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:updated")
|
:ok = Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:updated")
|
||||||
:ok = Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:pipeline")
|
:ok = Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:pipeline")
|
||||||
_ = Process.send_after(self(), :refresh_pipeline_status, @pipeline_status_refresh_ms)
|
socket = schedule_pipeline_status_timer(socket)
|
||||||
_ = Process.send_after(self(), :advance_now_cursor, @advance_now_cursor_ms)
|
schedule_advance_timer(socket)
|
||||||
|
else
|
||||||
|
socket
|
||||||
end
|
end
|
||||||
|
|
||||||
socket
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp schedule_pipeline_status_timer(socket) do
|
||||||
|
cancel_timer_if_exists(socket.assigns[:pipeline_timer])
|
||||||
|
ref = Process.send_after(self(), :refresh_pipeline_status, @pipeline_status_refresh_ms)
|
||||||
|
assign(socket, :pipeline_timer, ref)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp schedule_advance_timer(socket) do
|
||||||
|
cancel_timer_if_exists(socket.assigns[:advance_timer])
|
||||||
|
ref = Process.send_after(self(), :advance_now_cursor, @advance_now_cursor_ms)
|
||||||
|
assign(socket, :advance_timer, ref)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp cancel_timer_if_exists(nil), do: :ok
|
||||||
|
defp cancel_timer_if_exists(ref) when is_reference(ref), do: Process.cancel_timer(ref)
|
||||||
|
defp cancel_timer_if_exists(_), do: :ok
|
||||||
|
|
||||||
# Compact relative-time rendering of the build timestamp. Matches the
|
# Compact relative-time rendering of the build timestamp. Matches the
|
||||||
# format used by Layouts.deploy_stamp/1 so the nav and the map
|
# format used by Layouts.deploy_stamp/1 so the nav and the map
|
||||||
# sidebar stay consistent.
|
# sidebar stay consistent.
|
||||||
|
|
@ -325,7 +343,7 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(:advance_now_cursor, socket) do
|
def handle_info(:advance_now_cursor, socket) do
|
||||||
Process.send_after(self(), :advance_now_cursor, @advance_now_cursor_ms)
|
socket = schedule_advance_timer(socket)
|
||||||
|
|
||||||
if socket.assigns.tracking_now? do
|
if socket.assigns.tracking_now? do
|
||||||
new_time = closest_to_now(socket.assigns.valid_times)
|
new_time = closest_to_now(socket.assigns.valid_times)
|
||||||
|
|
@ -350,7 +368,7 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(:refresh_pipeline_status, socket) do
|
def handle_info(:refresh_pipeline_status, socket) do
|
||||||
Process.send_after(self(), :refresh_pipeline_status, @pipeline_status_refresh_ms)
|
socket = schedule_pipeline_status_timer(socket)
|
||||||
|
|
||||||
new_status = PipelineStatus.current()
|
new_status = PipelineStatus.current()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,25 +12,33 @@ defmodule MicrowavepropWeb.MonitorLive.Show do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(%{"id" => id}, _session, socket) do
|
def mount(%{"id" => id}, _session, socket) do
|
||||||
user = socket.assigns.current_scope.user
|
viewer = socket.assigns[:current_scope] && socket.assigns.current_scope.user
|
||||||
monitor = BeaconMonitors.get_monitor!(id)
|
monitor = BeaconMonitors.get_monitor!(id)
|
||||||
|
|
||||||
if monitor.user_id == user.id do
|
cond do
|
||||||
beacons = Repo.all(from b in Beacon, where: b.approved == true, order_by: b.callsign)
|
viewer && monitor.user_id == viewer.id ->
|
||||||
measurements = BeaconMeasurements.list_recent_for_monitor(monitor.id, 20)
|
beacons = Repo.all(from b in Beacon, where: b.approved == true, order_by: b.callsign)
|
||||||
|
measurements = BeaconMeasurements.list_recent_for_monitor(monitor.id, 20)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, "My Monitor: #{monitor.name}")
|
|> assign(:page_title, "My Monitor: #{monitor.name}")
|
||||||
|> assign(:monitor, monitor)
|
|> assign(:monitor, monitor)
|
||||||
|> assign(:beacons, beacons)
|
|> assign(:beacons, beacons)
|
||||||
|> assign(:measurements, measurements)
|
|> assign(:measurements, measurements)
|
||||||
|> assign_form(monitor)}
|
|> assign_form(monitor)}
|
||||||
else
|
|
||||||
{:ok,
|
viewer ->
|
||||||
socket
|
{:ok,
|
||||||
|> put_flash(:error, "You don't have access to that monitor.")
|
socket
|
||||||
|> push_navigate(to: ~p"/users/settings")}
|
|> put_flash(:error, "You don't have access to that monitor.")
|
||||||
|
|> push_navigate(to: ~p"/users/settings")}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
{:ok,
|
||||||
|
socket
|
||||||
|
|> put_flash(:info, "Please log in to view this monitor.")
|
||||||
|
|> push_navigate(to: ~p"/users/log-in")}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,7 @@ defmodule Microwaveprop.ObanErrorReporterTest do
|
||||||
capture_log(fn -> execute_exception(meta_transient) end)
|
capture_log(fn -> execute_exception(meta_transient) end)
|
||||||
|
|
||||||
assert_receive {:transient, 303}
|
assert_receive {:transient, 303}
|
||||||
refute_receive {:permanent, 303}, 50
|
refute_receive {:permanent, 303}, 200
|
||||||
|
|
||||||
meta_permanent =
|
meta_permanent =
|
||||||
build_metadata(%{
|
build_metadata(%{
|
||||||
|
|
@ -213,7 +213,7 @@ defmodule Microwaveprop.ObanErrorReporterTest do
|
||||||
capture_log(fn -> execute_exception(meta_permanent) end)
|
capture_log(fn -> execute_exception(meta_permanent) end)
|
||||||
|
|
||||||
assert_receive {:permanent, 404}
|
assert_receive {:permanent, 404}
|
||||||
refute_receive {:transient, 404}, 50
|
refute_receive {:transient, 404}, 200
|
||||||
end
|
end
|
||||||
|
|
||||||
test "a raising callback does not propagate, and the log still lands" do
|
test "a raising callback does not propagate, and the log still lands" do
|
||||||
|
|
|
||||||
|
|
@ -657,7 +657,7 @@ defmodule Microwaveprop.PropagationTest do
|
||||||
_ = Propagation.scores_at(10_000, valid_time)
|
_ = Propagation.scores_at(10_000, valid_time)
|
||||||
|
|
||||||
assert_receive {:telemetry, [:microwaveprop, :propagation, :scores_at, :cache], %{hit: true}}
|
assert_receive {:telemetry, [:microwaveprop, :propagation, :scores_at, :cache], %{hit: true}}
|
||||||
refute_receive {:telemetry, [:microwaveprop, :propagation, :scores_at, :stop], _}, 50
|
refute_receive {:telemetry, [:microwaveprop, :propagation, :scores_at, :stop], _}, 200
|
||||||
after
|
after
|
||||||
:telemetry.detach(handler_id)
|
:telemetry.detach(handler_id)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ defmodule Microwaveprop.RepoListenerTest do
|
||||||
msg = notification("contact_status_changed", "not-json-at-all{")
|
msg = notification("contact_status_changed", "not-json-at-all{")
|
||||||
|
|
||||||
assert {:noreply, %{pid: nil}} = RepoListener.handle_info(msg, %{pid: nil})
|
assert {:noreply, %{pid: nil}} = RepoListener.handle_info(msg, %{pid: nil})
|
||||||
refute_receive {:contact_status_changed, _}, 50
|
refute_receive {:contact_status_changed, _}, 200
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -76,7 +76,7 @@ defmodule Microwaveprop.RepoListenerTest do
|
||||||
msg = notification("oban_job_changed", "{\"broken\":")
|
msg = notification("oban_job_changed", "{\"broken\":")
|
||||||
|
|
||||||
assert {:noreply, %{pid: nil}} = RepoListener.handle_info(msg, %{pid: nil})
|
assert {:noreply, %{pid: nil}} = RepoListener.handle_info(msg, %{pid: nil})
|
||||||
refute_receive {:oban_job_changed, _}, 50
|
refute_receive {:oban_job_changed, _}, 200
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -88,8 +88,8 @@ defmodule Microwaveprop.RepoListenerTest do
|
||||||
msg = notification("contact_status_changed", Jason.encode!(%{"contact_id" => "abc"}))
|
msg = notification("contact_status_changed", Jason.encode!(%{"contact_id" => "abc"}))
|
||||||
|
|
||||||
assert {:noreply, _} = RepoListener.handle_info(msg, %{pid: nil})
|
assert {:noreply, _} = RepoListener.handle_info(msg, %{pid: nil})
|
||||||
refute_receive {:oban_job_changed, _}, 50
|
refute_receive {:oban_job_changed, _}, 200
|
||||||
refute_receive {:contact_status_changed, _}, 50
|
refute_receive {:contact_status_changed, _}, 200
|
||||||
end
|
end
|
||||||
|
|
||||||
test "oban_job_changed does not broadcast on db:contact_status" do
|
test "oban_job_changed does not broadcast on db:contact_status" do
|
||||||
|
|
@ -99,8 +99,8 @@ defmodule Microwaveprop.RepoListenerTest do
|
||||||
msg = notification("oban_job_changed", Jason.encode!(%{"id" => 1}))
|
msg = notification("oban_job_changed", Jason.encode!(%{"id" => 1}))
|
||||||
|
|
||||||
assert {:noreply, _} = RepoListener.handle_info(msg, %{pid: nil})
|
assert {:noreply, _} = RepoListener.handle_info(msg, %{pid: nil})
|
||||||
refute_receive {:contact_status_changed, _}, 50
|
refute_receive {:contact_status_changed, _}, 200
|
||||||
refute_receive {:oban_job_changed, _}, 50
|
refute_receive {:oban_job_changed, _}, 200
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -117,8 +117,8 @@ defmodule Microwaveprop.RepoListenerTest do
|
||||||
msg = notification("some_other_channel", Jason.encode!(%{"x" => 1}))
|
msg = notification("some_other_channel", Jason.encode!(%{"x" => 1}))
|
||||||
|
|
||||||
assert {:noreply, %{pid: nil}} = RepoListener.handle_info(msg, %{pid: nil})
|
assert {:noreply, %{pid: nil}} = RepoListener.handle_info(msg, %{pid: nil})
|
||||||
refute_receive {:contact_status_changed, _}, 50
|
refute_receive {:contact_status_changed, _}, 200
|
||||||
refute_receive {:oban_job_changed, _}, 50
|
refute_receive {:oban_job_changed, _}, 200
|
||||||
end
|
end
|
||||||
|
|
||||||
test "ignores arbitrary info messages without crashing" do
|
test "ignores arbitrary info messages without crashing" do
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,10 @@ defmodule Microwaveprop.ValkeyTest do
|
||||||
|
|
||||||
import Mox
|
import Mox
|
||||||
|
|
||||||
# Defined here (not in test_helper.exs) so test_helper can load even
|
|
||||||
# when the precommit chain has purged Mox from the code path.
|
|
||||||
alias Microwaveprop.Valkey
|
alias Microwaveprop.Valkey
|
||||||
alias Microwaveprop.Valkey.Conn
|
alias Microwaveprop.Valkey.Conn
|
||||||
alias Microwaveprop.Valkey.MockAdapter
|
alias Microwaveprop.Valkey.MockAdapter
|
||||||
|
|
||||||
Mox.defmock(MockAdapter, for: Microwaveprop.Valkey.Adapter)
|
|
||||||
|
|
||||||
# Register Mox mock as the adapter and fake a Redix connection so
|
# Register Mox mock as the adapter and fake a Redix connection so
|
||||||
# configured?() returns true, letting command/pipeline run through the mock.
|
# configured?() returns true, letting command/pipeline run through the mock.
|
||||||
setup do
|
setup do
|
||||||
|
|
|
||||||
|
|
@ -99,13 +99,9 @@ defmodule Mix.Tasks.Prop.CompareTest do
|
||||||
# read_history is exercised.
|
# read_history is exercised.
|
||||||
File.write!(history_path, "not-valid-json\n", [:append])
|
File.write!(history_path, "not-valid-json\n", [:append])
|
||||||
|
|
||||||
try do
|
ExUnit.CaptureIO.capture_io(fn ->
|
||||||
ExUnit.CaptureIO.capture_io(fn ->
|
Compare.run(["--days", "1", "--samples", "10", "--output", output_dir])
|
||||||
Compare.run(["--days", "1", "--samples", "10", "--output", output_dir])
|
end)
|
||||||
end)
|
|
||||||
catch
|
|
||||||
:exit, _ -> :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
# The history file remains; new entries appended.
|
# The history file remains; new entries appended.
|
||||||
assert File.exists?(history_path)
|
assert File.exists?(history_path)
|
||||||
|
|
@ -119,13 +115,9 @@ defmodule Mix.Tasks.Prop.CompareTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
try do
|
ExUnit.CaptureIO.capture_io(fn ->
|
||||||
ExUnit.CaptureIO.capture_io(fn ->
|
Compare.run(["--days", "1", "--samples", "100", "--output", output_dir])
|
||||||
Compare.run(["--days", "1", "--samples", "100", "--output", output_dir])
|
end)
|
||||||
end)
|
|
||||||
catch
|
|
||||||
:exit, _ -> :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
latest_path = Path.join(output_dir, "latest.json")
|
latest_path = Path.join(output_dir, "latest.json")
|
||||||
|
|
||||||
|
|
@ -140,13 +132,9 @@ defmodule Mix.Tasks.Prop.CompareTest do
|
||||||
test "seeded contact + matching HRRR walks the scoring + write path", %{output_dir: output_dir} do
|
test "seeded contact + matching HRRR walks the scoring + write path", %{output_dir: output_dir} do
|
||||||
_ = seed_contact_and_hrrr()
|
_ = seed_contact_and_hrrr()
|
||||||
|
|
||||||
try do
|
ExUnit.CaptureIO.capture_io(fn ->
|
||||||
ExUnit.CaptureIO.capture_io(fn ->
|
Compare.run(["--days", "1", "--samples", "10", "--output", output_dir])
|
||||||
Compare.run(["--days", "1", "--samples", "10", "--output", output_dir])
|
end)
|
||||||
end)
|
|
||||||
catch
|
|
||||||
:exit, _ -> :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
# Each successful run writes latest.json + history.jsonl. If the
|
# Each successful run writes latest.json + history.jsonl. If the
|
||||||
# task short-circuited (no rows / no model), at least one of these
|
# task short-circuited (no rows / no model), at least one of these
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue