defmodule MicrowavepropWeb.ContactLive.ShowCoverageTest do use MicrowavepropWeb.ConnCase, async: true import Microwaveprop.AccountsFixtures import Phoenix.LiveViewTest alias Microwaveprop.Geo alias Microwaveprop.Propagation.BandConfig alias Microwaveprop.Propagation.ScoreCache alias Microwaveprop.Propagation.Scorer alias Microwaveprop.Radio.Contact alias Microwaveprop.Radio.ContactCommonVolumeRadar alias Microwaveprop.Radio.ContactEdit alias Microwaveprop.Repo alias Microwaveprop.Terrain.ElevationClient alias Microwaveprop.Terrain.TerrainProfile alias Microwaveprop.Weather alias Microwaveprop.Weather.HrrrNativeProfile alias Microwaveprop.Weather.HrrrProfile alias Microwaveprop.Weather.NarrProfile alias Microwaveprop.Weather.SolarIndex alias Microwaveprop.Weather.Sounding alias Microwaveprop.Weather.SurfaceObservation alias Microwaveprop.Workers.SolarIndexWorker alias Microwaveprop.Workers.TerrainProfileWorker setup do Req.Test.stub(ElevationClient, fn conn -> Req.Test.json(conn, []) end) Req.Test.stub(Microwaveprop.Weather.HrrrClient, fn conn -> Plug.Conn.send_resp(conn, 404, "not found") end) Req.Test.stub(Microwaveprop.Weather.SolarClient, fn conn -> Req.Test.text(conn, "") end) :ok end defp create_contact(attrs \\ %{}) do default = %{ station1: "W5XD", station2: "K5TR", qso_timestamp: ~U[2026-03-28 18:00:00Z], mode: "CW", band: Decimal.new("1296"), grid1: "EM12", grid2: "EM00", pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7}, distance_km: Decimal.new("295") } merged = Map.merge(default, attrs) {user_id, changeset_attrs} = Map.pop(merged, :user_id) {:ok, contact} = %Contact{user_id: user_id} |> Contact.changeset(changeset_attrs) |> Repo.insert() contact end describe "Show additional branch coverage" do use ExUnitProperties defp admin_fixture! do user = user_fixture() {:ok, user} = Microwaveprop.Accounts.admin_update_user(user, %{is_admin: true}) user end setup do ScoreCache.clear() :ok end test "factor notes render every note-branch for a 10 GHz contact", %{conn: conn} do contact = create_contact(%{ band: Decimal.new("10000"), pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7}, qso_timestamp: ~U[2026-03-28 18:00:00Z] }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [ %{"pres" => 1000.0, "tmpc" => 22.0, "dwpc" => 12.0, "hght" => 100.0} ], hpbl_m: 1200.0, pwat_mm: 32.0, surface_temp_c: 22.0, surface_dewpoint_c: 12.0, surface_pressure_mb: 1020.0, surface_refractivity: 340.0, min_refractivity_gradient: -160.0, ducting_detected: true }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "beneficial at this band" assert html =~ "ducting" assert html =~ "(high)" assert html =~ "summer peak band" assert html =~ "refractivity aid" assert html =~ "None" end test "factor notes render 'absorption' and 'winter peak' branches at 24 GHz", %{conn: conn} do contact = create_contact(%{ band: Decimal.new("24000"), pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7}, qso_timestamp: ~U[2026-01-15 18:00:00Z] }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [ %{"pres" => 1000.0, "tmpc" => 5.0, "dwpc" => -2.0, "hght" => 100.0} ], hpbl_m: 1200.0, pwat_mm: 28.0, surface_temp_c: 5.0, surface_dewpoint_c: -2.0, surface_pressure_mb: 1000.0, surface_refractivity: 310.0, min_refractivity_gradient: -50.0, ducting_detected: false }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "harmful at this band" assert html =~ "absorption" assert html =~ "winter peak band" assert html =~ "(low)" end test "td_note renders 'near saturation' when dewpoint hugs temperature", %{conn: conn} do contact = create_contact(%{ band: Decimal.new("10000"), pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7} }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 19.5, "hght" => 100.0}], hpbl_m: 800.0, pwat_mm: 25.0, surface_temp_c: 20.0, surface_dewpoint_c: 19.5, surface_pressure_mb: 1010.0, surface_refractivity: 340.0, min_refractivity_gradient: -90.0, ducting_detected: false }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "near saturation" assert html =~ "enhanced" assert html =~ "(moderate)" end test "td_note renders 'dry' branch for a large T-Td depression", %{conn: conn} do contact = create_contact(%{ band: Decimal.new("10000"), pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7} }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 30.0, "dwpc" => 0.0, "hght" => 100.0}], hpbl_m: 2200.0, pwat_mm: 8.0, surface_temp_c: 30.0, surface_dewpoint_c: 0.0, surface_pressure_mb: 1012.0, surface_refractivity: 280.0, min_refractivity_gradient: -40.0, ducting_detected: false }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "(dry)" assert html =~ "(normal)" end test "BLOCKED terrain with ducting selects the 'atmospheric ducting' mechanism copy", %{conn: conn} do contact = create_contact(%{ band: Decimal.new("10000"), pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7} }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 15.0, "hght" => 100.0}], hpbl_m: 800.0, pwat_mm: 30.0, surface_temp_c: 20.0, surface_dewpoint_c: 15.0, surface_pressure_mb: 1015.0, surface_refractivity: 340.0, min_refractivity_gradient: -180.0, ducting_detected: true }) Repo.insert!(%TerrainProfile{ contact_id: contact.id, sample_count: 100, path_points: [%{"lat" => 32.9, "lon" => -97.0, "dist_km" => 0.0, "elev" => 200.0}], max_elevation_m: 800.0, min_clearance_m: -50.0, diffraction_db: 22.0, fresnel_hit_count: 5, obstructed_count: 4, verdict: "BLOCKED" }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert String.match?(html, ~r/(tropospheric duct|atmospheric ducting)/) end test "BLOCKED terrain with enhanced refraction but no ducting uses the 'dN/dh' copy", %{conn: conn} do contact = create_contact(%{ band: Decimal.new("10000"), pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7} }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 10.0, "hght" => 100.0}], hpbl_m: 1200.0, pwat_mm: 20.0, surface_temp_c: 20.0, surface_dewpoint_c: 10.0, surface_pressure_mb: 1015.0, surface_refractivity: 320.0, min_refractivity_gradient: -140.0, ducting_detected: false }) Repo.insert!(%TerrainProfile{ contact_id: contact.id, sample_count: 100, path_points: [%{"lat" => 32.9, "lon" => -97.0, "dist_km" => 0.0, "elev" => 200.0}], max_elevation_m: 800.0, min_clearance_m: -20.0, diffraction_db: 18.0, fresnel_hit_count: 3, obstructed_count: 2, verdict: "BLOCKED" }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "dN/dh =" assert html =~ "Enhanced refraction" end test "BLOCKED terrain with no ducting or refraction falls back to diffraction copy", %{conn: conn} do contact = create_contact(%{ band: Decimal.new("10000"), pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7} }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 10.0, "hght" => 100.0}], hpbl_m: 1200.0, pwat_mm: 20.0, surface_temp_c: 20.0, surface_dewpoint_c: 10.0, surface_pressure_mb: 1015.0, surface_refractivity: 320.0, min_refractivity_gradient: -30.0, ducting_detected: false }) Repo.insert!(%TerrainProfile{ contact_id: contact.id, sample_count: 100, path_points: [%{"lat" => 32.9, "lon" => -97.0, "dist_km" => 0.0, "elev" => 200.0}], max_elevation_m: 800.0, min_clearance_m: -20.0, diffraction_db: 14.5, fresnel_hit_count: 3, obstructed_count: 2, verdict: "BLOCKED" }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "diffraction loss" assert html =~ "knife-edge diffraction" end test "clear path with long distance + ducting renders 'extending range' copy", %{conn: conn} do contact = create_contact(%{ band: Decimal.new("10000"), pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7}, distance_km: Decimal.new("295") }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 22.0, "dwpc" => 20.0, "hght" => 100.0}], hpbl_m: 700.0, pwat_mm: 30.0, surface_temp_c: 22.0, surface_dewpoint_c: 20.0, surface_pressure_mb: 1012.0, surface_refractivity: 350.0, min_refractivity_gradient: -200.0, ducting_detected: true }) Repo.insert!(%TerrainProfile{ contact_id: contact.id, sample_count: 100, path_points: [%{"lat" => 32.9, "lon" => -97.0, "dist_km" => 0.0, "elev" => 200.0}], max_elevation_m: 400.0, min_clearance_m: 50.0, diffraction_db: 0.0, fresnel_hit_count: 0, obstructed_count: 0, verdict: "CLEAR" }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "Ducting conditions present" assert String.match?(html, ~r/(extended range|extending range)/) end test "clear path with enhanced refraction but no ducting hits the long-path branch", %{conn: conn} do contact = create_contact(%{ band: Decimal.new("10000"), pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7}, distance_km: Decimal.new("200") }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 10.0, "hght" => 100.0}], hpbl_m: 1200.0, pwat_mm: 20.0, surface_temp_c: 20.0, surface_dewpoint_c: 10.0, surface_pressure_mb: 1012.0, surface_refractivity: 320.0, min_refractivity_gradient: -140.0, ducting_detected: false }) Repo.insert!(%TerrainProfile{ contact_id: contact.id, sample_count: 100, path_points: [%{"lat" => 32.9, "lon" => -97.0, "dist_km" => 0.0, "elev" => 200.0}], max_elevation_m: 400.0, min_clearance_m: 50.0, diffraction_db: 0.0, fresnel_hit_count: 0, obstructed_count: 0, verdict: "CLEAR" }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "Enhanced refraction present" end test "admin edit of private toggle and height triggers the diff branch", %{conn: conn} do contact = create_contact() admin = admin_fixture!() conn = log_in_user(conn, admin) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") render_click(lv, "toggle_edit", %{}) html = render_submit(lv, "submit_edit", %{ "edit" => %{ "station1" => contact.station1, "station2" => contact.station2, "grid1" => contact.grid1, "grid2" => contact.grid2, "mode" => contact.mode, "height1_ft" => "35", "private" => "true" } }) assert html =~ "Contact updated" refreshed = Repo.get!(Contact, contact.id) assert refreshed.height1_ft == 35 assert refreshed.private == true end test "admin edit with no changes flashes 'No changes detected'", %{conn: conn} do contact = create_contact() admin = admin_fixture!() conn = log_in_user(conn, admin) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") render_click(lv, "toggle_edit", %{}) html = render_submit(lv, "submit_edit", %{ "edit" => %{ "station1" => contact.station1, "station2" => contact.station2, "grid1" => contact.grid1, "grid2" => contact.grid2, "mode" => contact.mode } }) assert html =~ "No changes detected" end test "owner submit with identical values returns no-changes flash", %{conn: conn} do owner = user_fixture() {:ok, contact} = %Contact{} |> Contact.changeset(%{ station1: owner.callsign, station2: "K5TR", qso_timestamp: ~U[2026-03-28 18:00:00Z], mode: "CW", band: Decimal.new("1296"), grid1: "EM12", grid2: "EM00", pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7}, distance_km: Decimal.new("295"), user_submitted: true, submitter_email: owner.email }) |> Ecto.Changeset.change(user_id: owner.id) |> Repo.insert() conn = log_in_user(conn, owner) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") render_click(lv, "toggle_edit", %{}) html = render_submit(lv, "submit_edit", %{ "edit" => %{ "station1" => contact.station1, "station2" => contact.station2, "grid1" => contact.grid1, "grid2" => contact.grid2, "mode" => contact.mode } }) assert html =~ "No changes detected" end test "stranger submit with an invalid mode surfaces the changeset error flash", %{conn: conn} do contact = create_contact() stranger = user_fixture() conn = log_in_user(conn, stranger) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") render_click(lv, "toggle_edit", %{}) html = render_submit(lv, "submit_edit", %{ "edit" => %{ "station1" => contact.station1, "station2" => contact.station2, "grid1" => contact.grid1, "grid2" => contact.grid2, "mode" => "BOGUS" } }) assert html =~ "Could not submit edit" assert Repo.aggregate(ContactEdit, :count, :id) == 0 end test "internal_network? false when no session IP is recorded", %{conn: conn} do contact = create_contact() {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "No surface observations found nearby." end test "internal_network? false for an IP just outside the enqueue subnet", %{conn: conn} do conn = %{conn | remote_ip: {8, 8, 8, 8}} contact = create_contact() {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "No surface observations found nearby." end test "internal_network? true when session IP sits inside the enqueue subnet", %{conn: conn} do conn = Phoenix.ConnTest.init_test_session(conn, %{remote_ip: "172.56.0.10"}) contact = create_contact() {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "Fetching surface observations" end test "expanded_soundings MapSet exposes the skew-T table when a sounding is expanded", %{conn: conn} do contact = create_contact() {:ok, station} = Weather.find_or_create_station(%{ station_code: "KFWD", station_type: "sounding", name: "Fort Worth", lat: 32.83, lon: -97.30 }) {:ok, sounding} = %Sounding{} |> Sounding.changeset(%{ station_id: station.id, observed_at: ~U[2026-03-28 12:00:00Z], profile: [ %{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 15.0, "hght" => 100.0}, %{"pres" => 850.0, "tmpc" => 12.0, "dwpc" => 8.0, "hght" => 1500.0} ], level_count: 2, surface_temp_c: 20.0, surface_dewpoint_c: 15.0 }) |> Repo.insert() {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") _ = render_async(lv, 2_000) html_open = render_click(lv, "toggle_profile", %{"id" => sounding.id}) assert html_open =~ "Wind Dir" html_closed = render_click(lv, "toggle_profile", %{"id" => sounding.id}) refute html_closed =~ "Wind Dir" end test "load_pending_edit returns the existing edit for a logged-in user", %{conn: conn} do contact = create_contact() user = user_fixture() Repo.insert!(%ContactEdit{ contact_id: contact.id, user_id: user.id, proposed_changes: %{"station2" => "K9EDIT"}, status: :pending }) conn = log_in_user(conn, user) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render(lv) assert html =~ "You have a pending edit for this contact" end test "fetch_queue_counts hits the Cache.fetch_or_store round trip", %{conn: conn} do {:ok, _job} = Oban.insert(SolarIndexWorker.new(%{"date" => "2026-03-28"})) contact = create_contact() {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render(lv) assert html =~ "in queue" end test "handle_info({:sounding_fetched, …}) with mismatched contact_id is a no-op", %{conn: conn} do contact = create_contact() {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html_before = render_async(lv, 2_000) send(lv.pid, {:sounding_fetched, %{contact_id: Ecto.UUID.generate()}}) html_after = render(lv) assert html_before == html_after end test "rain_scatter mechanism surfaces for a 24 GHz contact with heavy rain pixels", %{conn: conn} do contact = create_contact(%{ band: Decimal.new("24000"), distance_km: Decimal.new("250") }) Repo.insert!(%ContactCommonVolumeRadar{ contact_id: contact.id, observed_at: contact.qso_timestamp, common_volume_km2: 15_000.0, pixel_count: 1200, rain_pixel_count: 400, heavy_rain_pixel_count: 120, max_dbz: 52.0, mean_dbz: 35.0, coverage_pct: 33.0 }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "52.0 dBZ" assert html =~ "Heavy-rain pixels: 120" assert html =~ "Propagation Mechanism" end test "elevation profile hydrates when the SRTM stub returns a populated path", %{conn: conn} do Req.Test.stub(ElevationClient, fn conn -> if conn.host == "api.open-meteo.com" do n = 257 Req.Test.json(conn, %{"elevation" => List.duplicate(300.0, n)}) else Req.Test.json(conn, []) end end) contact = create_contact(%{ band: Decimal.new("10000"), pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7} }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 10.0, "hght" => 100.0}], hpbl_m: 1200.0, pwat_mm: 25.0, surface_temp_c: 20.0, surface_dewpoint_c: 10.0, surface_pressure_mb: 1012.0, surface_refractivity: 320.0, min_refractivity_gradient: -200.0, ducting_detected: false }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "Az:" assert html =~ "El:" end property "Scorer.composite_score always stays within [0, 100]" do all_band_mhz = BandConfig.all_freqs() check all( band_mhz <- StreamData.member_of(all_band_mhz), temp_c <- StreamData.float(min: -40.0, max: 50.0), td_depression <- StreamData.float(min: 0.0, max: 25.0), pressure_mb <- StreamData.float(min: 950.0, max: 1050.0), sky_pct <- StreamData.float(min: 0.0, max: 100.0), wind_kts <- StreamData.float(min: 0.0, max: 40.0), grad <- StreamData.float(min: -300.0, max: 50.0), pwat_mm <- StreamData.float(min: 0.0, max: 60.0), month <- StreamData.integer(1..12), hour <- StreamData.integer(0..23), max_runs: 30 ) do band_config = BandConfig.get(band_mhz) conditions = %{ temp_f: temp_c * 9 / 5 + 32, dewpoint_f: (temp_c - td_depression) * 9 / 5 + 32, temp_c: temp_c, dewpoint_c: temp_c - td_depression, abs_humidity: max(0.1, 10.0 - td_depression * 0.3), min_refractivity_gradient: grad, bl_depth_m: 1000.0, pressure_mb: pressure_mb, prev_pressure_mb: pressure_mb, sky_cover_pct: sky_pct, wind_speed_kts: wind_kts, rain_rate_mmhr: 0.0, pwat_mm: pwat_mm, month: month, utc_hour: hour, utc_minute: 0, latitude: 32.0, longitude: -97.0, best_duct_band_ghz: nil, bulk_richardson: nil } %{score: score} = Scorer.composite_score(conditions, band_config) assert score >= 0 and score <= 100 end end property "obs-table sort handler round-trips for any known field", %{conn: conn} do contact = create_contact() fields_pool = StreamData.member_of(~w(observed_at temp_f dewpoint_f relative_humidity sea_level_pressure_mb)) check all(field <- fields_pool, max_runs: 12) do {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") _ = render_async(lv, 2_000) _ = render_click(lv, "sort", %{"field" => "station_name", "table" => "obs"}) _ = render_click(lv, "sort", %{"field" => field, "table" => "obs"}) _ = render_click(lv, "sort", %{"field" => field, "table" => "obs"}) _ = render_click(lv, "sort", %{"field" => field, "table" => "obs"}) assert render(lv) =~ "Surface Observations" end end end describe "Show failure-state + enrichment-combo render branches" do use ExUnitProperties setup do ScoreCache.clear() :ok end test "terrain_status :failed with no terrain row falls back to the no-profile copy", %{conn: conn} do contact = create_contact() |> Ecto.Changeset.change(terrain_status: :failed) |> Repo.update!() {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "No terrain profile available." refute html =~ "Computing terrain profile" end test "iemre_status :failed shows the generic 'No IEMRE data' copy", %{conn: conn} do contact = create_contact() |> Ecto.Changeset.change(iemre_status: :failed) |> Repo.update!() {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "No IEMRE data available." refute html =~ "Fetching IEMRE data" end test "weather_status :failed renders the empty surface-obs copy with no spinner", %{conn: conn} do contact = create_contact() |> Ecto.Changeset.change(weather_status: :failed) |> Repo.update!() {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "No surface observations found nearby." end test "hrrr_status :unavailable + no NARR row shows the NARR-fallback spinner", %{conn: conn} do contact = create_contact() |> Ecto.Changeset.change(hrrr_status: :unavailable) |> Repo.update!() {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "HRRR unavailable" assert html =~ "fetching NARR reanalysis" end test "NARR fallback with HRRR nil renders atmospheric profile from NARR", %{conn: conn} do contact = create_contact(%{ qso_timestamp: ~U[2010-06-15 18:00:00Z], pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7} }) Repo.insert!(%NarrProfile{ valid_time: ~U[2010-06-15 18:00:00Z], lat: 32.9, lon: -97.0, profile: [%{"pres" => 850.0, "tmpc" => 14.0, "dwpc" => 10.0, "hght" => 1500.0}], surface_temp_c: 24.0, surface_dewpoint_c: 18.0, surface_pressure_mb: 1012.0, hpbl_m: 1150.0, pwat_mm: 28.0, surface_refractivity: 318.0, min_refractivity_gradient: -55.0, ducting_detected: false }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "NARR" assert html =~ "Surface Temp:" assert html =~ "Surface Dewpoint:" end test "radar row without HRRR still renders the mechanism block and radar summary", %{conn: conn} do contact = create_contact(%{band: Decimal.new("10000"), distance_km: Decimal.new("150")}) Repo.insert!(%ContactCommonVolumeRadar{ contact_id: contact.id, observed_at: contact.qso_timestamp, common_volume_km2: 14_000.0, pixel_count: 900, rain_pixel_count: 60, heavy_rain_pixel_count: 8, max_dbz: 38.0, mean_dbz: 19.0, coverage_pct: 6.0 }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "Propagation Mechanism" assert html =~ "38.0 dBZ" assert html =~ "Heavy-rain pixels: 8" assert html =~ "Radar coverage: 6%" end test "solar handle_async populates Solar Conditions with real SFI/Ap/Kp", %{conn: conn} do contact = create_contact(%{qso_timestamp: ~U[2026-03-28 18:00:00Z]}) Repo.insert!(%SolarIndex{ date: ~D[2026-03-28], sfi: 142.0, sunspot_number: 98, ap_index: 6, kp_values: [1.0, 2.0, 2.7, 3.0, 1.3, 0.7, 1.0, 1.3] }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "Solar Conditions" assert html =~ "142" assert html =~ "98" assert html =~ "2.7" end test "solar handle_async for a missing date in an internal subnet enqueues a fetch", %{conn: conn} do contact = create_contact(%{qso_timestamp: ~U[2026-03-28 18:00:00Z]}) conn = %{conn | remote_ip: {172, 56, 0, 50}} {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "Fetching solar index data" assert Process.alive?(lv.pid) end test "soundings-array is rendered alongside HRRR profile when both are present", %{conn: conn} do contact = create_contact(%{ pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7} }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 12.0, "hght" => 100.0}], hpbl_m: 1100.0, pwat_mm: 24.0, surface_temp_c: 20.0, surface_dewpoint_c: 12.0, surface_pressure_mb: 1012.0, surface_refractivity: 322.0, min_refractivity_gradient: -90.0, ducting_detected: false }) {:ok, raob_station} = Weather.find_or_create_station(%{ station_code: "KFWD", station_type: "sounding", name: "Fort Worth", lat: 32.83, lon: -97.30 }) Repo.insert!( Sounding.changeset(%Sounding{}, %{ station_id: raob_station.id, observed_at: ~U[2026-03-28 12:00:00Z], profile: [ %{"pres" => 1000.0, "tmpc" => 22.0, "dwpc" => 18.0, "hght" => 100.0}, %{"pres" => 950.0, "tmpc" => 24.0, "dwpc" => 5.0, "hght" => 600.0} ], level_count: 2, surface_temp_c: 22.0, surface_dewpoint_c: 18.0, surface_refractivity: 345.0, ducting_detected: true, duct_characteristics: [%{"base" => 100.0, "top" => 400.0, "strength" => 8.0}] }) ) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "HRRR" assert html =~ "KFWD" assert html =~ "345" end test "BLOCKED terrain without an elevation profile uses the generic obstructed copy", %{conn: conn} do contact = create_contact(%{ pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7} }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 10.0, "hght" => 100.0}], hpbl_m: 1200.0, pwat_mm: 22.0, surface_temp_c: 20.0, surface_dewpoint_c: 10.0, surface_pressure_mb: 1010.0, surface_refractivity: 320.0, min_refractivity_gradient: -50.0, ducting_detected: false }) Repo.insert!(%TerrainProfile{ contact_id: contact.id, sample_count: 100, path_points: [%{"lat" => 32.9, "lon" => -97.0, "dist_km" => 0.0, "elev" => 200.0}], max_elevation_m: 800.0, min_clearance_m: -10.0, diffraction_db: 10.0, fresnel_hit_count: 1, obstructed_count: 1, verdict: "BLOCKED" }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "Path is terrain-obstructed" end test "FRESNEL_MINOR verdict via elevation profile drives the describe-verdict branch", %{conn: conn} do Req.Test.stub(ElevationClient, fn conn -> if conn.host == "api.open-meteo.com" do Req.Test.json(conn, %{"elevation" => List.duplicate(250.0, 257)}) else Req.Test.json(conn, []) end end) contact = create_contact(%{ pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7} }) Repo.insert!(%TerrainProfile{ contact_id: contact.id, sample_count: 100, path_points: [%{"lat" => 32.9, "lon" => -97.0, "dist_km" => 0.0, "elev" => 250.0}], max_elevation_m: 260.0, min_clearance_m: 30.0, diffraction_db: 0.0, fresnel_hit_count: 0, obstructed_count: 0, verdict: "CLEAR" }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "Az:" assert html =~ "Propagation Mechanism" end test "2304 MHz band with elevated PWAT hits the 10 GHz band_summary branch", %{conn: conn} do contact = create_contact(%{ band: Decimal.new("2304"), pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7} }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 22.0, "dwpc" => 18.0, "hght" => 100.0}], hpbl_m: 1100.0, pwat_mm: 32.0, surface_temp_c: 22.0, surface_dewpoint_c: 18.0, surface_pressure_mb: 1012.0, surface_refractivity: 345.0, min_refractivity_gradient: -90.0, ducting_detected: false }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "At 10 GHz, the elevated moisture" end test "47 GHz band with elevated PWAT hits the 24G-and-up absorption copy", %{conn: conn} do contact = create_contact(%{ band: Decimal.new("47000"), pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7} }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 15.0, "hght" => 100.0}], hpbl_m: 1200.0, pwat_mm: 30.0, surface_temp_c: 20.0, surface_dewpoint_c: 15.0, surface_pressure_mb: 1012.0, surface_refractivity: 330.0, min_refractivity_gradient: -60.0, ducting_detected: false }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "high moisture" assert html =~ "water vapor absorption" end test "144 MHz band falls through every band_summary clause (no moisture copy)", %{conn: conn} do contact = create_contact(%{ band: Decimal.new("144"), pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7} }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 10.0, "dwpc" => -5.0, "hght" => 100.0}], hpbl_m: 800.0, pwat_mm: 8.0, surface_temp_c: 10.0, surface_dewpoint_c: -5.0, surface_pressure_mb: 1015.0, surface_refractivity: 290.0, min_refractivity_gradient: -30.0, ducting_detected: false }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) refute html =~ "At 10 GHz, the elevated moisture" refute html =~ "water vapor absorption" assert html =~ "144 MHz" end test "1296 MHz band renders through the analysis without 10 GHz copy", %{conn: conn} do contact = create_contact(%{ band: Decimal.new("1296"), pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7} }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 10.0, "dwpc" => 0.0, "hght" => 100.0}], hpbl_m: 800.0, pwat_mm: 10.0, surface_temp_c: 10.0, surface_dewpoint_c: 0.0, surface_pressure_mb: 1015.0, surface_refractivity: 300.0, min_refractivity_gradient: -40.0, ducting_detected: false }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "1296 MHz" refute html =~ "At 10 GHz, the elevated moisture" end test "75 GHz + low PWAT skips the moisture narrative", %{conn: conn} do contact = create_contact(%{ band: Decimal.new("75000"), pos1: %{"lat" => 32.9, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7} }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: 32.9, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 5.0, "dwpc" => -15.0, "hght" => 100.0}], hpbl_m: 700.0, pwat_mm: 5.0, surface_temp_c: 5.0, surface_dewpoint_c: -15.0, surface_pressure_mb: 1020.0, surface_refractivity: 280.0, min_refractivity_gradient: -30.0, ducting_detected: false }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "75 GHz" refute html =~ "water vapor absorption" end test "admin edit with proposed == %{} (no-op submit) surfaces 'No changes detected'", %{conn: conn} do contact = create_contact() admin = user_fixture() |> Ecto.Changeset.change(is_admin: true) |> Repo.update!() conn = log_in_user(conn, admin) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") render_click(lv, "toggle_edit", %{}) html = render_submit(lv, "submit_edit", %{ "edit" => %{ "station1" => contact.station1, "station2" => contact.station2, "grid1" => contact.grid1, "grid2" => contact.grid2, "mode" => contact.mode, "band" => Decimal.to_string(contact.band) } }) assert html =~ "No changes detected" end test "admin edit changing mode to FT8 applies the update", %{conn: conn} do contact = create_contact(%{mode: "CW"}) admin = user_fixture() |> Ecto.Changeset.change(is_admin: true) |> Repo.update!() conn = log_in_user(conn, admin) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") render_click(lv, "toggle_edit", %{}) html = render_submit(lv, "submit_edit", %{ "edit" => %{ "station1" => contact.station1, "station2" => contact.station2, "grid1" => contact.grid1, "grid2" => contact.grid2, "mode" => "FT8" } }) assert html =~ "Contact updated" assert Repo.get!(Contact, contact.id).mode == "FT8" end test "admin edit changing mode to SSB applies the update", %{conn: conn} do contact = create_contact(%{mode: "CW"}) admin = user_fixture() |> Ecto.Changeset.change(is_admin: true) |> Repo.update!() conn = log_in_user(conn, admin) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") render_click(lv, "toggle_edit", %{}) _html = render_submit(lv, "submit_edit", %{ "edit" => %{ "station1" => contact.station1, "station2" => contact.station2, "grid1" => contact.grid1, "grid2" => contact.grid2, "mode" => "SSB" } }) assert Repo.get!(Contact, contact.id).mode == "SSB" end test "sort handler returns a sorted list of the same length for a seeded obs set", %{conn: conn} do contact = create_contact(%{qso_timestamp: ~U[2026-03-28 18:00:00Z]}) for code <- ~w(KDFW KDAL KAFW KGKY) do {:ok, station} = Weather.find_or_create_station(%{ station_code: code, station_type: "asos", name: "#{code} Airport", lat: 32.9, lon: -97.02 }) Repo.insert!( SurfaceObservation.changeset(%SurfaceObservation{}, %{ station_id: station.id, observed_at: contact.qso_timestamp, temp_f: 60.0 + :rand.uniform() * 20, dewpoint_f: 40.0 + :rand.uniform() * 20, relative_humidity: 50.0, sea_level_pressure_mb: 1014.0 }) ) end {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "4 stations" for f <- ~w(station_name observed_at temp_f dewpoint_f relative_humidity sea_level_pressure_mb bogus_key) do html = render_click(lv, "sort", %{"field" => f, "table" => "obs"}) assert html =~ "Surface Observations" assert html =~ "4 stations" end end test "fetch_queue_counts returns a map after seeding multiple distinct queues", %{conn: conn} do contact = create_contact() {:ok, _} = Oban.insert(SolarIndexWorker.new(%{"date" => Date.to_iso8601(~D[2026-03-28])})) {:ok, _} = Oban.insert(TerrainProfileWorker.new(%{"contact_id" => contact.id})) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render(lv) assert html =~ "in queue" end test "LV stays alive after render_async completes with no DB rows at all", %{conn: conn} do contact = create_contact() {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "No surface observations found nearby." assert Process.alive?(lv.pid) end property "propagation_mechanism-derived summary stays a binary across random flag combos", %{conn: conn} do bool_gen = StreamData.boolean() check all( ducting? <- bool_gen, enhanced? <- bool_gen, blocked? <- bool_gen, long_path? <- bool_gen, max_runs: 10 ) do dist = if long_path?, do: Decimal.new("200"), else: Decimal.new("50") nonce = System.unique_integer([:positive]) lat1 = 32.0 + nonce / 1_000_000 iter_valid_time = DateTime.add(~U[2026-03-28 18:00:00Z], nonce, :second) contact = create_contact(%{ band: Decimal.new("10000"), qso_timestamp: iter_valid_time, pos1: %{"lat" => lat1, "lon" => -97.0}, pos2: %{"lat" => 30.3, "lon" => -97.7}, distance_km: dist }) Repo.insert!(%HrrrProfile{ valid_time: contact.qso_timestamp, lat: lat1, lon: -97.0, profile: [%{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 12.0, "hght" => 100.0}], hpbl_m: 1200.0, pwat_mm: 22.0, surface_temp_c: 20.0, surface_dewpoint_c: 12.0, surface_pressure_mb: 1012.0, surface_refractivity: 320.0, min_refractivity_gradient: if(enhanced?, do: -150.0, else: -40.0), ducting_detected: ducting? }) Repo.insert!(%TerrainProfile{ contact_id: contact.id, sample_count: 100, path_points: [%{"lat" => 32.9, "lon" => -97.0, "dist_km" => 0.0, "elev" => 200.0}], max_elevation_m: if(blocked?, do: 800.0, else: 400.0), min_clearance_m: if(blocked?, do: -20.0, else: 40.0), diffraction_db: if(blocked?, do: 15.0, else: 0.0), fresnel_hit_count: 0, obstructed_count: if(blocked?, do: 2, else: 0), verdict: if(blocked?, do: "BLOCKED", else: "CLEAR") }) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "Propagation Analysis" assert html =~ "/100" end end property "sort-observations handler keeps the assign as a list of stable length", %{conn: conn} do contact = create_contact(%{qso_timestamp: ~U[2026-03-28 18:00:00Z]}) {:ok, station} = Weather.find_or_create_station(%{ station_code: "KDFW", station_type: "asos", name: "DFW Airport", lat: 32.9, lon: -97.02 }) for i <- 1..3 do Repo.insert!( SurfaceObservation.changeset(%SurfaceObservation{}, %{ station_id: station.id, observed_at: DateTime.add(contact.qso_timestamp, i * 60, :second), temp_f: 60.0 + i, dewpoint_f: 45.0 + i, relative_humidity: 55.0, sea_level_pressure_mb: 1014.0 }) ) end gen_field = StreamData.one_of([ StreamData.member_of(~w(station_name observed_at temp_f dewpoint_f relative_humidity sea_level_pressure_mb)), StreamData.string(:alphanumeric, min_length: 1, max_length: 8) ]) check all(fields <- StreamData.list_of(gen_field, min_length: 1, max_length: 5), max_runs: 10) do {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") html = render_async(lv, 2_000) assert html =~ "3 stations" for f <- fields do html = render_click(lv, "sort", %{"field" => f, "table" => "obs"}) assert html =~ "3 stations" end end end end describe "Show ordering and proximity helpers" do use ExUnitProperties defp seed_distinct_obs(contact) do Enum.map( [ {"KAAA", 32.90, -97.00, 60.0, 40.0, 50.0, 1010.0, 0}, {"KBBB", 32.91, -97.01, 62.0, 42.0, 52.0, 1011.0, 60}, {"KCCC", 32.92, -97.02, 64.0, 44.0, 54.0, 1012.0, 120}, {"KDDD", 32.93, -97.03, 66.0, 46.0, 56.0, 1013.0, 180} ], fn {code, lat, lon, temp, dewp, rh, pres, offset} -> {:ok, station} = Weather.find_or_create_station(%{ station_code: code, station_type: "asos", name: "#{code} Airport", lat: lat, lon: lon }) Repo.insert!( SurfaceObservation.changeset(%SurfaceObservation{}, %{ station_id: station.id, observed_at: DateTime.add(contact.qso_timestamp, offset, :second), temp_f: temp, dewpoint_f: dewp, relative_humidity: rh, sea_level_pressure_mb: pres }) ) code end ) end test "sort_observations orders by station_name ascending and descending", %{conn: conn} do contact = create_contact(%{qso_timestamp: ~U[2026-03-28 18:00:00Z]}) codes = seed_distinct_obs(contact) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") _ = render_async(lv, 2_000) _ = render_click(lv, "sort", %{"field" => "observed_at", "table" => "obs"}) _ = render_click(lv, "sort", %{"field" => "station_name", "table" => "obs"}) asc_codes = lv.pid |> :sys.get_state() |> Map.fetch!(:socket) |> Map.fetch!(:assigns) |> Map.fetch!(:surface_observations) |> Enum.map(& &1.station.station_code) assert asc_codes == Enum.sort(codes) _ = render_click(lv, "sort", %{"field" => "station_name", "table" => "obs"}) desc_codes = lv.pid |> :sys.get_state() |> Map.fetch!(:socket) |> Map.fetch!(:assigns) |> Map.fetch!(:surface_observations) |> Enum.map(& &1.station.station_code) assert desc_codes == Enum.sort(codes, :desc) end test "sort_observations orders by observed_at, temp_f, dewpoint_f, relative_humidity, and pressure", %{conn: conn} do contact = create_contact(%{qso_timestamp: ~U[2026-03-28 18:00:00Z]}) _ = seed_distinct_obs(contact) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") _ = render_async(lv, 2_000) check_field = fn field, reader -> _ = render_click(lv, "sort", %{"field" => field, "table" => "obs"}) obs = lv.pid |> :sys.get_state() |> Map.fetch!(:socket) |> Map.fetch!(:assigns) |> Map.fetch!(:surface_observations) values = Enum.map(obs, reader) assert values == Enum.sort(values), "obs not ascending on #{field}: #{inspect(values)}" end check_field.("observed_at", & &1.observed_at) check_field.("temp_f", & &1.temp_f) check_field.("dewpoint_f", & &1.dewpoint_f) check_field.("relative_humidity", & &1.relative_humidity) check_field.("sea_level_pressure_mb", & &1.sea_level_pressure_mb) end defp seed_distinct_soundings(contact) do Enum.map( [ {"KAAAS", 15.0, 310.0, 20.0, -2.0, 0}, {"KBBBS", 18.0, 315.0, 25.0, -1.0, 3600}, {"KCCCS", 21.0, 320.0, 30.0, 0.0, 7200} ], fn {code, temp, refr, k, li, offset} -> {:ok, station} = Weather.find_or_create_station(%{ station_code: code, station_type: "sounding", name: "#{code} Sounding", lat: 32.9 + :rand.uniform() * 0.001, lon: -97.0 + :rand.uniform() * 0.001 }) Repo.insert!( Sounding.changeset(%Sounding{}, %{ station_id: station.id, observed_at: DateTime.add(contact.qso_timestamp, offset, :second), profile: [%{"pres" => 1000.0, "tmpc" => temp, "dwpc" => temp - 5.0, "hght" => 100.0}], level_count: 1, surface_temp_c: temp, surface_dewpoint_c: temp - 5.0, surface_refractivity: refr, k_index: k, lifted_index: li }) ) code end ) end test "sort_soundings orders by every known sounding field", %{conn: conn} do contact = create_contact(%{qso_timestamp: ~U[2026-03-28 18:00:00Z]}) _ = seed_distinct_soundings(contact) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") _ = render_async(lv, 2_000) _ = render_click(lv, "sort", %{"field" => "observed_at", "table" => "soundings"}) _ = render_click(lv, "sort", %{"field" => "station_name", "table" => "soundings"}) station_names = lv.pid |> :sys.get_state() |> Map.fetch!(:socket) |> Map.fetch!(:assigns) |> Map.fetch!(:soundings) |> Enum.map(&(&1.station.name || &1.station.station_code)) assert station_names == Enum.sort(station_names) check_field = fn field, reader -> _ = render_click(lv, "sort", %{"field" => field, "table" => "soundings"}) rows = lv.pid |> :sys.get_state() |> Map.fetch!(:socket) |> Map.fetch!(:assigns) |> Map.fetch!(:soundings) values = Enum.map(rows, reader) assert values == Enum.sort(values), "soundings not ascending on #{field}: #{inspect(values)}" end check_field.("observed_at", & &1.observed_at) check_field.("surface_temp_c", & &1.surface_temp_c) check_field.("surface_refractivity", & &1.surface_refractivity) check_field.("k_index", & &1.k_index) check_field.("lifted_index", & &1.lifted_index) end test "closest_observations/4 keeps only the five nearest by midpoint distance", %{conn: conn} do contact = create_contact(%{qso_timestamp: ~U[2026-03-28 18:00:00Z]}) mid_lat = (32.9 + 30.3) / 2 mid_lon = (-97.0 + -97.7) / 2 near_to_far = [ {"KMID0", mid_lat, mid_lon}, {"KMID1", mid_lat + 0.05, mid_lon + 0.05}, {"KMID2", mid_lat - 0.05, mid_lon - 0.05}, {"KMID3", mid_lat + 0.10, mid_lon + 0.10}, {"KMID4", mid_lat - 0.10, mid_lon - 0.10}, {"KMID5", mid_lat + 0.20, mid_lon + 0.20}, {"KMID6", mid_lat - 0.20, mid_lon - 0.20} ] _ = Enum.map(near_to_far, fn {code, lat, lon} -> {:ok, station} = Weather.find_or_create_station(%{ station_code: code, station_type: "asos", name: "#{code}", lat: lat, lon: lon }) Repo.insert!( SurfaceObservation.changeset(%SurfaceObservation{}, %{ station_id: station.id, observed_at: contact.qso_timestamp, temp_f: 70.0, dewpoint_f: 55.0, relative_humidity: 55.0, sea_level_pressure_mb: 1015.0 }) ) end) {:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}") _ = render_async(lv, 2_000) obs = lv.pid |> :sys.get_state() |> Map.fetch!(:socket) |> Map.fetch!(:assigns) |> Map.fetch!(:surface_observations) assert length(obs) <= 5 distances = Enum.map(obs, fn o -> abs(o.station.lat - mid_lat) + abs(o.station.lon - mid_lon) end) assert distances == Enum.sort(distances) end test "contact with pos1 but nil pos2 renders without blowing up (half_dist = 0 path)", %{conn: conn} do contact = create_contact() {:ok, contact} = contact |> Ecto.Changeset.change(pos2: nil, grid2: nil, distance_km: nil) |> Repo.update() assert is_nil(contact.pos2) {:ok, lv, html} = live(conn, ~p"/contacts/#{contact.id}") _ = render_async(lv, 2_000) assert html =~ "W5XD" assert Process.alive?(lv.pid) end property "sort_observations/3 preserves list length + element set for any field key", %{conn: conn} do contact = create_contact(%{qso_timestamp: ~U[2026-03-28 18:00:00Z]}) codes = seed_distinct_obs(contact) fields_gen = StreamData.one_of([ StreamData.member_of(~w(station_name observed_at temp_f dewpoint_f relative_humidity sea_level_pressure_mb)), StreamData.string(:alphanumeric, min_length: 1, max_length: 6) ]) check all(fields <- StreamData.list_of(fields_gen, min_length: 1, max_length: 5), max_runs: 10) do {:ok, lv, _} = live(conn, ~p"/contacts/#{contact.id}") _ = render_async(lv, 2_000) for f <- fields do _ = render_click(lv, "sort", %{"field" => f, "table" => "obs"}) obs = lv.pid |> :sys.get_state() |> Map.fetch!(:socket) |> Map.fetch!(:assigns) |> Map.fetch!(:surface_observations) assert length(obs) == length(codes) assert Enum.sort(Enum.map(obs, & &1.station.station_code)) == Enum.sort(codes) end end end property "haversine_km/4 is symmetric: d(a,b,c,d) == d(c,d,a,b)" do check all( lat1 <- StreamData.float(min: -89.0, max: 89.0), lon1 <- StreamData.float(min: -179.0, max: 179.0), lat2 <- StreamData.float(min: -89.0, max: 89.0), lon2 <- StreamData.float(min: -179.0, max: 179.0) ) do ab = Geo.haversine_km(lat1, lon1, lat2, lon2) ba = Geo.haversine_km(lat2, lon2, lat1, lon1) assert_in_delta ab, ba, 1.0e-6 end end end end