docs/tests: moduledocs on 18 LiveViews, MapLive event coverage, Rust pipeline roundtrip
- Add one-line moduledocs to 18 LiveViews that were carrying @moduledoc false. Each line names the route and summarizes what the page does so a future reader knows where to look before opening the file. - Add MapLive handle_event tests for toggle_radar, select_time / set_selected_time, point_detail, map_bounds, retry_initial_scores. Assertions focus on socket-state transitions and "didn't crash" rather than raw HTML — map_bounds in particular has no visible render-side effect (it push_events the new scores to the JS hook). - Rust pipeline: integration-shaped test that hand-builds a 2-cell surface + pressure grid, runs merge → cell_to_conditions → precompute_band_invariants → composite_score_with → write_atomic → decode, asserting header + body length round-trip. Closes the 'pipeline's happy path is only covered by unit tests' gap.
This commit is contained in:
parent
944c2dc1d4
commit
ff950f9e40
20 changed files with 190 additions and 18 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.AboutLive do
|
defmodule MicrowavepropWeb.AboutLive do
|
||||||
@moduledoc false
|
@moduledoc "Static `/about` page — project overview, credits, contact info."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
|
|
||||||
alias Microwaveprop.Repo
|
alias Microwaveprop.Repo
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.Admin.ContactEditLive do
|
defmodule MicrowavepropWeb.Admin.ContactEditLive do
|
||||||
@moduledoc false
|
@moduledoc "Admin-only contact editor at `/admin/contacts/:id/edit` for manual QSO fixes."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Radio.ContactEdit
|
use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Radio.ContactEdit
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.AlgoLive do
|
defmodule MicrowavepropWeb.AlgoLive do
|
||||||
@moduledoc false
|
@moduledoc "Renders `algo.md` as HTML at `/algo` — documents the scoring algorithm."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
|
|
||||||
@external_resource "algo.md"
|
@external_resource "algo.md"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.BeaconLive.Form do
|
defmodule MicrowavepropWeb.BeaconLive.Form do
|
||||||
@moduledoc false
|
@moduledoc "Shared new/edit form for beacon records, used by `BeaconLive.Index`."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
|
|
||||||
alias Microwaveprop.Beacons
|
alias Microwaveprop.Beacons
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.BeaconLive.Index do
|
defmodule MicrowavepropWeb.BeaconLive.Index do
|
||||||
@moduledoc false
|
@moduledoc "`/beacons` list + admin controls for the beacon catalog."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Beacons.Beacon
|
use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Beacons.Beacon
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.BeaconLive.Show do
|
defmodule MicrowavepropWeb.BeaconLive.Show do
|
||||||
@moduledoc false
|
@moduledoc "Single-beacon detail page with reception-log history at `/beacons/:id`."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
|
|
||||||
alias Microwaveprop.Beacons
|
alias Microwaveprop.Beacons
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.ContactLive.Index do
|
defmodule MicrowavepropWeb.ContactLive.Index do
|
||||||
@moduledoc false
|
@moduledoc "`/contacts` table with filter/sort + pagination over QSO records."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Radio.Contact
|
use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Radio.Contact
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.ContactLive.Show do
|
defmodule MicrowavepropWeb.ContactLive.Show do
|
||||||
@moduledoc false
|
@moduledoc "Per-QSO detail at `/contacts/:id`: weather, HRRR, radar, terrain, charts."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
|
|
||||||
import MicrowavepropWeb.Components.SkewTChart
|
import MicrowavepropWeb.Components.SkewTChart
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.ContactMapLive do
|
defmodule MicrowavepropWeb.ContactMapLive do
|
||||||
@moduledoc false
|
@moduledoc "`/contacts/map` plots QSO endpoints + great-circle paths on a Leaflet map."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
|
|
||||||
alias Microwaveprop.Radio
|
alias Microwaveprop.Radio
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.MapLive do
|
defmodule MicrowavepropWeb.MapLive do
|
||||||
@moduledoc false
|
@moduledoc "`/map` — main propagation heatmap with forecast timeline + point detail drawer."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
use LiveStash
|
use LiveStash
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.PathLive do
|
defmodule MicrowavepropWeb.PathLive do
|
||||||
@moduledoc false
|
@moduledoc "`/path` terrain-profile + propagation analysis for a pair of grids/coords."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
|
|
||||||
import MicrowavepropWeb.Components.SkewTChart
|
import MicrowavepropWeb.Components.SkewTChart
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.PrivacyLive do
|
defmodule MicrowavepropWeb.PrivacyLive do
|
||||||
@moduledoc false
|
@moduledoc "Static `/privacy` page."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.StatusLive do
|
defmodule MicrowavepropWeb.StatusLive do
|
||||||
@moduledoc false
|
@moduledoc "`/status` ops dashboard — enrichment progress, queue depths, DB size."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.SubmitLive do
|
defmodule MicrowavepropWeb.SubmitLive do
|
||||||
@moduledoc false
|
@moduledoc "`/submit` QSO submission form; enqueues enrichment jobs on save."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
use LiveStash
|
use LiveStash
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.UserManagementLive.Edit do
|
defmodule MicrowavepropWeb.UserManagementLive.Edit do
|
||||||
@moduledoc false
|
@moduledoc "Admin edit page for a single user (roles, suspension)."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
|
|
||||||
alias Microwaveprop.Accounts
|
alias Microwaveprop.Accounts
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.UserManagementLive.Index do
|
defmodule MicrowavepropWeb.UserManagementLive.Index do
|
||||||
@moduledoc false
|
@moduledoc "Admin user list at `/admin/users`."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Accounts.User
|
use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Accounts.User
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.UserProfileLive do
|
defmodule MicrowavepropWeb.UserProfileLive do
|
||||||
@moduledoc false
|
@moduledoc "Signed-in user's own profile + callsign linkage at `/account`."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
|
|
||||||
alias Microwaveprop.Accounts
|
alias Microwaveprop.Accounts
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
defmodule MicrowavepropWeb.WeatherMapLive do
|
defmodule MicrowavepropWeb.WeatherMapLive do
|
||||||
@moduledoc false
|
@moduledoc "`/weather` — HRRR-derived forecast fields (temp, Td, wind, refractivity) on a map."
|
||||||
use MicrowavepropWeb, :live_view
|
use MicrowavepropWeb, :live_view
|
||||||
|
|
||||||
alias Microwaveprop.Weather
|
alias Microwaveprop.Weather
|
||||||
|
|
|
||||||
|
|
@ -358,6 +358,101 @@ mod tests {
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(matches!(err, PipelineError::F00Reserved));
|
assert!(matches!(err, PipelineError::F00Reserved));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Integration-ish: hand-build a 2-cell surface + pressure grid,
|
||||||
|
/// merge → cell_to_conditions → scorer → scores_file::write_atomic
|
||||||
|
/// → scores_file::read, asserting the full chain agrees with
|
||||||
|
/// itself. Covers the post-fetch hot path (the one that scales
|
||||||
|
/// with grid size) without standing up wgrib2.
|
||||||
|
#[test]
|
||||||
|
fn merge_score_write_read_roundtrip() {
|
||||||
|
use chrono::TimeZone;
|
||||||
|
|
||||||
|
// decoder encodes lat/lon at millidegree precision in the key.
|
||||||
|
fn latlon_to_key(lat: f64, lon: f64) -> (i32, i32) {
|
||||||
|
((lat * 1000.0).round() as i32, (lon * 1000.0).round() as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sfc = PointGrid::new();
|
||||||
|
let mut prs = PointGrid::new();
|
||||||
|
|
||||||
|
let cells = [(32.0_f64, -97.0_f64), (32.03_f64, -97.03_f64)];
|
||||||
|
for &(lat, lon) in &cells {
|
||||||
|
let mut s = CellValues::new();
|
||||||
|
s.insert("TMP:2 m above ground".into(), 295.0);
|
||||||
|
s.insert("DPT:2 m above ground".into(), 285.0);
|
||||||
|
s.insert("UGRD:10 m above ground".into(), 2.0);
|
||||||
|
s.insert("VGRD:10 m above ground".into(), 1.0);
|
||||||
|
s.insert("TCDC:entire atmosphere".into(), 20.0);
|
||||||
|
s.insert("PRES:surface".into(), 101_000.0);
|
||||||
|
s.insert("HPBL:surface".into(), 500.0);
|
||||||
|
s.insert(
|
||||||
|
"PWAT:entire atmosphere (considered as a single layer)".into(),
|
||||||
|
20.0,
|
||||||
|
);
|
||||||
|
sfc.insert(latlon_to_key(lat, lon), s);
|
||||||
|
|
||||||
|
let mut p = CellValues::new();
|
||||||
|
p.insert("TMP:1000 mb".into(), 295.0);
|
||||||
|
p.insert("DPT:1000 mb".into(), 285.0);
|
||||||
|
p.insert("HGT:1000 mb".into(), 100.0);
|
||||||
|
p.insert("TMP:975 mb".into(), 293.0);
|
||||||
|
p.insert("DPT:975 mb".into(), 280.0);
|
||||||
|
p.insert("HGT:975 mb".into(), 300.0);
|
||||||
|
prs.insert(latlon_to_key(lat, lon), p);
|
||||||
|
}
|
||||||
|
|
||||||
|
let merged = merge_grids(sfc, prs);
|
||||||
|
assert_eq!(merged.len(), 2);
|
||||||
|
|
||||||
|
let valid_time = Utc.with_ymd_and_hms(2026, 6, 15, 18, 0, 0).unwrap();
|
||||||
|
|
||||||
|
let prepared: Vec<_> = merged
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(key, cell)| {
|
||||||
|
let (lat, lon) = crate::decoder::key_to_latlon(key);
|
||||||
|
let c = cell_to_conditions(&cell, lat, lon, &valid_time)?;
|
||||||
|
let inv = scorer::precompute_band_invariants(&c);
|
||||||
|
Some((lat, lon, c, inv))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert_eq!(prepared.len(), 2);
|
||||||
|
|
||||||
|
let band = &band_config::all_bands()[0];
|
||||||
|
let scores: Vec<ScorePoint> = prepared
|
||||||
|
.iter()
|
||||||
|
.map(|(lat, lon, c, inv)| {
|
||||||
|
let r = scorer::composite_score_with(c, band, Some(*inv));
|
||||||
|
ScorePoint {
|
||||||
|
lat: *lat,
|
||||||
|
lon: *lon,
|
||||||
|
score: r.score,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert_eq!(scores.len(), 2);
|
||||||
|
for s in &scores {
|
||||||
|
assert!(s.score <= 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to an atomic .prop file and read it back.
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
scores_file::write_atomic(dir.path(), band.freq_mhz, valid_time, &scores).unwrap();
|
||||||
|
|
||||||
|
let path = scores_file::path_for(dir.path(), band.freq_mhz, valid_time);
|
||||||
|
let bytes = std::fs::read(&path).expect("read back score file");
|
||||||
|
let decoded = scores_file::decode(&bytes).expect("decode");
|
||||||
|
// Decoded grid covers the full CONUS bounding box as a dense
|
||||||
|
// byte array; our 2 score points land at their respective
|
||||||
|
// cells and the rest are sentinel no-data. Spot-check: header
|
||||||
|
// fields survived the round trip and the body is the expected
|
||||||
|
// size for a row_major n_rows × n_cols grid.
|
||||||
|
assert_eq!(decoded.band_mhz, band.freq_mhz);
|
||||||
|
assert_eq!(
|
||||||
|
decoded.body.len(),
|
||||||
|
decoded.n_rows as usize * decoded.n_cols as usize
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,83 @@ defmodule MicrowavepropWeb.MapLiveTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "toggle_radar" do
|
||||||
|
test "flips the radar layer visibility assign", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/map")
|
||||||
|
|
||||||
|
refute :sys.get_state(lv.pid).socket.assigns.radar_visible
|
||||||
|
render_click(lv, "toggle_radar")
|
||||||
|
assert :sys.get_state(lv.pid).socket.assigns.radar_visible
|
||||||
|
render_click(lv, "toggle_radar")
|
||||||
|
refute :sys.get_state(lv.pid).socket.assigns.radar_visible
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "select_time / set_selected_time" do
|
||||||
|
test "set_selected_time accepts an iso8601 string and updates the assign",
|
||||||
|
%{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/map")
|
||||||
|
|
||||||
|
now_iso = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
|
||||||
|
render_hook(lv, "set_selected_time", %{"time" => now_iso})
|
||||||
|
|
||||||
|
# The handler accepts the event without crashing. Whether the new
|
||||||
|
# time is actually adopted depends on valid_times, which is empty
|
||||||
|
# in test (no ScoresFiles); that's fine — we're testing the
|
||||||
|
# handler path exists and doesn't raise on malformed/new input.
|
||||||
|
assert Process.alive?(lv.pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "select_time event is accepted and doesn't crash the LV",
|
||||||
|
%{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/map")
|
||||||
|
render_click(lv, "select_time", %{"time" => "2026-04-21T22:00:00Z"})
|
||||||
|
assert Process.alive?(lv.pid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "point_detail" do
|
||||||
|
test "handler pushes a `point_detail` event to the client without crashing",
|
||||||
|
%{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/map")
|
||||||
|
|
||||||
|
# point_detail does no server-rendered DOM update — it calls
|
||||||
|
# push_event/3 with the detail payload and a JS hook consumes it.
|
||||||
|
# The assertion is that the handler returns cleanly even in the
|
||||||
|
# "no HRRR profile" path (dev/test has empty .prop files).
|
||||||
|
render_click(lv, "point_detail", %{"lat" => 32.9, "lon" => -97.04})
|
||||||
|
assert Process.alive?(lv.pid)
|
||||||
|
assert render(lv) =~ "propagation-map"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "map_bounds" do
|
||||||
|
test "stores the viewport and debounces a forecast preload", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/map")
|
||||||
|
|
||||||
|
render_hook(lv, "map_bounds", %{
|
||||||
|
"south" => 30.0,
|
||||||
|
"north" => 35.0,
|
||||||
|
"west" => -100.0,
|
||||||
|
"east" => -95.0
|
||||||
|
})
|
||||||
|
|
||||||
|
bounds = :sys.get_state(lv.pid).socket.assigns.bounds
|
||||||
|
assert bounds["south"] == 30.0
|
||||||
|
assert bounds["north"] == 35.0
|
||||||
|
assert bounds["west"] == -100.0
|
||||||
|
assert bounds["east"] == -95.0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "retry_initial_scores" do
|
||||||
|
test "event is accepted without crashing the LV", %{conn: conn} do
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/map")
|
||||||
|
render_click(lv, "retry_initial_scores")
|
||||||
|
assert Process.alive?(lv.pid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "daily outlook strip" do
|
describe "daily outlook strip" do
|
||||||
test "no longer renders on the main map", %{conn: conn} do
|
test "no longer renders on the main map", %{conn: conn} do
|
||||||
{:ok, _lv, html} = live(conn, ~p"/map")
|
{:ok, _lv, html} = live(conn, ~p"/map")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue