defmodule MicrowavepropWeb.StatusLiveTest do use MicrowavepropWeb.ConnCase, async: false import Microwaveprop.AccountsFixtures import Phoenix.LiveViewTest alias Microwaveprop.Cache alias Microwaveprop.Radio.Contact alias Microwaveprop.Repo alias Microwaveprop.Weather.NarrProfile setup %{conn: conn} do Cache.invalidate({MicrowavepropWeb.StatusLive, :count_unprocessed}) Cache.invalidate({MicrowavepropWeb.StatusLive, :stats}) Cache.invalidate({MicrowavepropWeb.StatusLive, :db_stats}) user = user_fixture() {:ok, admin} = user |> Ecto.Changeset.change(%{is_admin: true}) |> Repo.update() conn = log_in_user(conn, admin) {:ok, conn: conn} end defp create_narr_candidate do {:ok, contact} = %Contact{} |> Contact.changeset(%{ station1: "W5TEST", station2: "K5TEST", qso_timestamp: ~U[2010-06-15 18:00:00Z], mode: "CW", band: Decimal.new("1296"), grid1: "EM12", grid2: "EM00", pos1: %{"lat" => 33.0, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7}, distance_km: Decimal.new("300") }) |> Ecto.Changeset.change(%{hrrr_status: :unavailable}) |> Repo.insert() contact end # NARR fetches land in era5_profiles (table rename is a follow-up migration). # The status UI labels this "NARR" now. test "NARR progress bar numerator is actual profile matches not total-minus-candidates", %{conn: conn} do create_narr_candidate() # No NARR profile yet — candidates = 1, done = 0 {:ok, lv, _html} = live(conn, ~p"/status") narr_row = lv |> element(~s|[data-progress-key="narr"]|) |> render() assert narr_row =~ "NARR" assert narr_row =~ "0 / 1" # Insert a matching profile row — done should become 1 {:ok, _} = Repo.insert(%NarrProfile{ valid_time: ~U[2010-06-15 18:00:00Z], lat: 33.0, lon: -97.0, ducting_detected: false }) Cache.invalidate({MicrowavepropWeb.StatusLive, :count_unprocessed}) {:ok, lv, _html} = live(conn, ~p"/status") narr_row = lv |> element(~s|[data-progress-key="narr"]|) |> render() assert narr_row =~ "1 / 1" end # NARR serves pre-2014 contacts only (NCEI archive ends 2014-10-02). A # post-2014 contact with hrrr_status=:unavailable is a HRRR retry case, # not a NARR candidate, and must not inflate the NARR denominator. test "NARR progress excludes post-2014 hrrr-unavailable contacts", %{conn: conn} do # pre-2014 candidate (counts toward NARR) create_narr_candidate() # post-2014 hrrr-unavailable contact (does NOT count toward NARR) {:ok, _} = %Contact{} |> Contact.changeset(%{ station1: "W5POST", station2: "K5POST", qso_timestamp: ~U[2020-06-15 18:00:00Z], mode: "CW", band: Decimal.new("1296"), grid1: "EM12", grid2: "EM00", pos1: %{"lat" => 33.0, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7}, distance_km: Decimal.new("300") }) |> Ecto.Changeset.change(%{hrrr_status: :unavailable}) |> Repo.insert() {:ok, lv, _html} = live(conn, ~p"/status") narr_row = lv |> element(~s|[data-progress-key="narr"]|) |> render() assert narr_row =~ "0 / 1" end # NARR analyses are archived at 3-hour marks (00/03/06/09/12/15/18/21 UTC). # `NarrClient.snap_to_analysis_hour/1` floors the QSO timestamp to the # previous 3-hour mark before fetch. The status numerator query must mirror # that snap — a naive ±30 min window around the raw qso_timestamp misses # the stored profile whenever the contact lands more than 30 min past the # last analysis hour. test "NARR progress counts contacts whose qso_timestamp doesn't land on a 3-hour analysis mark", %{conn: conn} do {:ok, _} = %Contact{} |> Contact.changeset(%{ station1: "W5OFF", station2: "K5OFF", qso_timestamp: ~U[2010-06-15 18:50:00Z], mode: "CW", band: Decimal.new("1296"), grid1: "EM12", grid2: "EM00", pos1: %{"lat" => 33.0, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7}, distance_km: Decimal.new("300") }) |> Ecto.Changeset.change(%{hrrr_status: :unavailable}) |> Repo.insert() {:ok, _} = Repo.insert(%NarrProfile{ valid_time: ~U[2010-06-15 18:00:00Z], lat: 33.0, lon: -97.0, ducting_detected: false }) {:ok, lv, _html} = live(conn, ~p"/status") narr_row = lv |> element(~s|[data-progress-key="narr"]|) |> render() assert narr_row =~ "1 / 1" end # NARR eligibility is defined by qso_timestamp, not hrrr_status. A # pre-2014 contact whose hrrr_status is still :queued (the ping-pong # window before reconcile flips it) must still be counted — otherwise # the denominator misses rows for several cron cycles after submission. test "NARR candidates include pre-2014 contacts regardless of hrrr_status", %{conn: conn} do {:ok, _} = %Contact{} |> Contact.changeset(%{ station1: "W5QUE", station2: "K5QUE", qso_timestamp: ~U[2010-06-15 18:00:00Z], mode: "CW", band: Decimal.new("1296"), grid1: "EM12", grid2: "EM00", pos1: %{"lat" => 33.0, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7}, distance_km: Decimal.new("300") }) |> Ecto.Changeset.change(%{hrrr_status: :queued}) |> Repo.insert() {:ok, lv, _html} = live(conn, ~p"/status") narr_row = lv |> element(~s|[data-progress-key="narr"]|) |> render() assert narr_row =~ "0 / 1" end end