Commit graph

1562 commits

Author SHA1 Message Date
915cd1f5a0
feat(rover): replace max-drive-time slider with max-distance in miles 2026-04-25 17:21:57 -05:00
FluxCD
9b81e776ca chore: update prop image to git.mcintire.me/graham/prop:main-1777155369-a9a3d22 [skip ci] 2026-04-25 22:18:19 +00:00
a9a3d22ae6
feat(rover): drop highlight marker on selected candidate 2026-04-25 17:15:53 -05:00
5631444abd
feat(rover): auto-resolve grid from callsign on add; inline-edit grid per row; mi/ft units 2026-04-25 17:14:32 -05:00
FluxCD
ea95ae3596 chore: update prop image to git.mcintire.me/graham/prop:main-1777154970-cbc20d0 [skip ci] 2026-04-25 22:12:14 +00:00
cbc20d03bf
feat(rover): remove cell click popup; map clicks are no-ops 2026-04-25 17:09:13 -05:00
9bfd345e8d
feat(accounts): derive 10-char home_grid from explicit lat/lon 2026-04-25 17:04:45 -05:00
9796f06ba2
feat(accounts): auto-prefill home QTH from QRZ on register + backfill on boot 2026-04-25 17:03:01 -05:00
b1fe077863
fix(path): single grid decode + zero-downtime rust-worker rollouts 2026-04-25 16:57:00 -05:00
FluxCD
055be81442 chore: update prop image to git.mcintire.me/graham/prop:main-1777153931-771299c [skip ci] 2026-04-25 21:54:10 +00:00
771299c951
feat(accounts): add Home QTH section to user settings page 2026-04-25 16:51:54 -05:00
784caa7699
feat(rover): drive-radius circle resizes live as max-drive-time slider drags 2026-04-25 16:42:49 -05:00
e2dfc05939
fix(rover): keep cells without elevation; surface diagnostic warnings as flash 2026-04-25 16:41:31 -05:00
9ebb134a03
feat(rover): home accepts grid/callsign/address; station markers labeled; imperial scale 2026-04-25 16:40:03 -05:00
FluxCD
4108a98e5d chore: update prop image to git.mcintire.me/graham/prop:main-1777153008-c5d6633 [skip ci] 2026-04-25 21:38:59 +00:00
c5d6633beb
feat(rover): URL station persistence + 10-digit grids on calculated values 2026-04-25 16:36:34 -05:00
150762d21d
feat(rover): render fixed stations as small dots, not triangle+label divIcons 2026-04-25 16:32:55 -05:00
6f25993998
fix(rover): wire delete X button to actually remove station rows 2026-04-25 16:32:10 -05:00
10fcc764f6
feat(rover): live slider labels via RoverSlider hook for forecast hour + drive time 2026-04-25 16:30:38 -05:00
db1539086b
feat(rover): drop Min Elev Gain section, link 'Sign in to save' to log-in 2026-04-25 16:28:32 -05:00
c13a5f53be
feat(rover): full-bleed layout matching /map and /weather; remove Mode selector 2026-04-25 16:26:33 -05:00
7c506a6453
feat(rover): redesigned LiveView with right-docked sidebar, Calculate flow, smoothed contours 2026-04-25 16:26:33 -05:00
448d3636a1
feat(rover): Oban worker enriches station elevation post-insert 2026-04-25 16:26:33 -05:00
2e462b0697
feat(rover): end-to-end Calculate pipeline 2026-04-25 16:26:33 -05:00
d80ca24e2e
feat(rover): bulk elevation lookup wrapper 2026-04-25 16:26:33 -05:00
25d07c0d42
feat(rover): pure scoring math (link margin, aggregator, drive time) 2026-04-25 16:26:33 -05:00
18f04a4345
feat(rover): user-scoped FixedStation context with ownership guard 2026-04-25 16:26:33 -05:00
21d3adfd2b
feat(rover): FixedStation schema with grid-derived lat/lon 2026-04-25 16:26:33 -05:00
FluxCD
b6fdfdf639 chore: update prop image to git.mcintire.me/graham/prop:main-1777151041-ce9319d [skip ci] 2026-04-25 21:13:54 +00:00
ce9319d43b
feat(user-settings): mark beacon monitors as not yet working
The monitor client and ingestion pipeline aren't shipped, so warn
users that registering a monitor here doesn't do anything yet.
2026-04-25 16:03:34 -05:00
124a9d105f
chore: ignore python __pycache__ across the tree 2026-04-25 16:03:01 -05:00
5932a5e4f9
feat(accounts): add home QTH fields to User 2026-04-25 16:02:43 -05:00
cf7dec79cf
feat(rover): add ModeThresholds for SSB/CW/Q65 SNR table 2026-04-25 15:59:52 -05:00
418f6426e6
fix(path): handle ProfilesFile cell shape correctly + log async exits
The /path calculator showed "0 / 9 HRRR points" in production despite
the on-disk profile store being current. Root cause: profile_from_cell/2
treated the cell's :profile key as a wrapper sub-map and called
Map.put_new on it — but the :profile key actually holds the vertical
pressure-level LIST. Every point sample crashed with BadMapError, the
crash propagated as {:exit, _} through Task.async_stream, and the
consumer silently dropped all 9 results.

Fix: stop wrapping. Cells are already flat HrrrProfile-shaped maps;
just stamp lat/lon (from the caller, since cells don't carry their
own coords — those are the map key) and valid_time onto the cell.

Audit + log every other async error path so the next silent failure
isn't invisible:
- PathLive HRRR point lookup
- Propagation.point_forecast per-hour reads
- Viewshed ray crashes
- IemClient ASOS network fetches
- RtmaClient range-download tasks
- Recalibrator factor-vector batches (positive + negative samples)
- MapLive forecast preload tasks
- RoverLive station resolution

LiveViews already had handle_async/3 exit clauses with logging. The
gap was always in Task.async_stream consumers that wrote {:exit, _} -> []
without surfacing the reason.

Add the rule to CLAUDE.md and project memory so this never repeats.

Also fix a pre-existing skewt_svg.ex compiler warning where
@critical_label_min_dy was used before being defined.
2026-04-25 15:57:20 -05:00
FluxCD
ba6c4100f9 chore: update prop image to git.mcintire.me/graham/prop:main-1777149212-6967614 [skip ci] 2026-04-25 20:35:43 +00:00
6967614dfb
fix(skewt): stack overlapping critical-level labels + widen rail
LCL/LFC and 0 °C/WBZ landed within ~5 px of each other on the right-
edge label rail and stomped on each other's text. Sort by y, bump
each subsequent label down by at least 14 px from the previous one,
and draw a thin leader from the true tick to the bumped baseline so
nothing reads as misaligned.

Also widened the SVG from 720 → 820 user units so longer entries
like `LCL 925 mb` no longer get clipped by the right edge.
2026-04-25 15:33:19 -05:00
2fc434df56
feat(path): hover-on-dot factor breakdown + tag fallback factors
The 18-hour forecast chart on /path now shows the same weighted-
criteria panel the /map popup builds when the user clicks a grid
cell. Hovering a dot fires a server roundtrip that fetches
Propagation.point_detail at the path's midpoint for that valid_time;
results are cached client-side so re-hovering is instant.

`Propagation.point_detail/4` now returns a `:profile_source` tag —
`:exact`, `{:fallback, fallback_valid_time}`, or `:unavailable` —
distinguishing real per-hour breakdowns (Rust pipeline writes a
profile file for every f00..f18 step) from rare cycle-miss fallbacks
where we approximate from a recent analysis profile. Stale comment
about "only f00 persists profiles" updated.

Map utilities (FACTOR_META, FACTOR_ORDER, factorBar,
factorExplanation, scoreTier, FactorMeta, ScoreTier) are now exported
from propagation_map_hook so the new path hook reuses the exact same
look + copy.
2026-04-25 15:33:19 -05:00
FluxCD
2c30682f2f chore: update prop image to git.mcintire.me/graham/prop:main-1777148470-6170226 [skip ci] 2026-04-25 20:23:32 +00:00
617022668e
chore(layout): move deploy stamp from navbar to footer
Free up navbar space and let every viewport see the timestamp (the
old `hidden md:inline` hid it on phones). Now shows relative + full
UTC inline.
2026-04-25 15:20:55 -05:00
2e60e65e2f
fix(path): read HRRR points from on-disk profile store
The summary card's "0 / 9 points" came from `Weather.find_nearest_hrrr`
only checking the `hrrr_profiles` DB table, which is populated solely
by per-QSO enrichment within ±0.07° / ±1 h. Arbitrary paths off any
recently-enriched contact got nothing.

Switch to ProfilesFile.read_point as the primary source — the hourly
chain publishes full-CONUS f00..f18 cells there — and keep the
DB-table query as the fallback when the on-disk store is empty or
doesn't cover a point. Cell shape is flattened so the template and
build_scoring/6 stay shape-agnostic.
2026-04-25 15:19:34 -05:00
332308b9c3
feat(skewt): fetch full 25-level profile and drop valid_time URL param
The grid-cached profile is trimmed to 13 levels (1000→700 mb) for
memory reasons, so the skew-T plot showed nothing above 700 mb.
HrrrClient.fetch_profile/4 now accepts a forecast_hour opt; SkewtLive
issues a per-point fetch against the same cycle the cached row came
from to get the full 1000→100 mb set, falling back to the cached
truncated cell on any failure.

Time selection no longer round-trips through the URL — `select_time`
updates state in-process and triggers a focused :load_skewt_time
async, so refreshing or sharing a /skewt?q=... link always lands on
the most recent analysis hour.
2026-04-25 15:19:34 -05:00
FluxCD
e2bb79bcf6 chore: update prop image to git.mcintire.me/graham/prop:main-1777145817-3a81d05 [skip ci] 2026-04-25 19:46:21 +00:00
3a81d05b4e
perf(skewt): load profile + sounding asynchronously via start_async
URL-parameter loads (`/skewt?q=…`) were blocking the initial mount
on three serial steps: geocoder/grid resolve, ProfilesFile or
HrrrProfileLookup read, and the SkewtParams/SoundingParams/SkewtSvg
derivation. The address path in particular hits an external API,
which made the page take "forever" to load on first paint.

Restructure SkewtLive so the page chrome (header, search bar) renders
synchronously on mount, and the heavy work runs in a single
`start_async(:load_skewt, …)` task that calls SkewtLocationResolver,
walks the on-disk profile store with a DB fallback, and produces the
SVG. A new `loading?` assign drives a small spinner + "Resolving
location and loading HRRR profile…" message that shows while the task
is in flight; previous data stays on screen so navigation between
forecast hours doesn't blank out the diagram.

handle_async/3 covers three landings:

  * `{:ok, {:ok, result}}` — apply assigns from the resolver
  * `{:ok, {:error, reason}}` — surface as the existing error alert
    (geocoder failure, unknown callsign, etc.)
  * `{:exit, reason}` — log a warning and show a generic retry message

start_async/3 cancels any prior task with the same name on each call,
so a rapid-fire URL patch (e.g. clicking through forecast-hour
buttons before the first response returns) never lands stale data.

Tests: extended SkewtLiveTest from 3 to 4 cases — the URL-param
initial render now asserts the chrome + spinner appear pre-async,
and a separate case `render_async/2`s the view to confirm the
location label arrives once the task completes. 4/4 green.
2026-04-25 14:36:30 -05:00
FluxCD
c3f1875df8 chore: update prop image to git.mcintire.me/graham/prop:main-1777145135-77bc894 [skip ci] 2026-04-25 19:35:19 +00:00
77bc894f56
feat(skewt): add wet-bulb / virtual-T traces and right-edge level rail
Three additions that bring the diagram closer to the SPC sounding
viewer's full-information chart:

  * Wet-bulb temperature trace, plotted as a thinner blue line under
    the dewpoint trace. Comes from a Stipanuk-style bracket between
    the level's dewpoint and dry-bulb temperatures (already used by
    `SkewtParams` for the wet-bulb-zero level — now public so the
    renderer can call it per level).

  * Environment virtual-temperature trace and parcel virtual-T
    trajectory, both as thin pinkish dashed lines. The parcel
    virtual-T is the actual buoyancy curve CAPE integrates against,
    so showing it next to the dry-bulb parcel ascent makes the
    moisture correction visible. New `SkewtParams.virtual_temp_c/2`
    and `/3` helpers, plus `SkewtParams.mixing_ratio/2`.

  * Critical-level rail on the right edge. Each of LCL / LFC / EL /
    0 °C / WBZ that resolves on the current profile gets a short
    horizontal tick at the right of the plot and a `<label> <p> mb`
    tag just outside it — matches the SPC chart's `143 mb (mix)` /
    `158 mb (mix)` annotation column. SkewtLive computes the
    pressures (and converts the height-keyed 0 °C and WBZ from
    SkewtParams back to pressure via `pressure_at_height/2`) before
    handing them to `SkewtSvg.render/2`.

Wind-derived overlays (hodograph, wind barbs) remain off — HRRR
profile data is wind-free above 10 m AGL.

mix test: 27 / 27 across the four /skewt-related test files,
2,902 / 2,902 + 221 properties on the full suite. mix credo --strict
clean (one cyclomatic-complexity refactor extracted along the way).
2026-04-25 14:24:28 -05:00
FluxCD
2ae9befd7f chore: update prop image to git.mcintire.me/graham/prop:main-1777144814-fb0c4f7 [skip ci] 2026-04-25 19:23:15 +00:00
fb0c4f7c77
fix(skewt): only fall back to profiles with all 13 pressure levels
When the on-disk ProfilesFile is empty SkewtLive falls back to the
hrrr_profiles Postgres table, but per-QSO HrrrFetchWorker rows
sometimes land with a partial profile (4–12 levels) when individual
byte-range fetches fail. The fallback was happily picking those rows,
which rendered a stubby Skew-T missing the upper troposphere.

HrrrProfileLookup.{list_valid_times_near, read_point_near}/3 now
filter on `coalesce(cardinality(profile), 0) >= min_profile_levels`,
defaulting to 13 (HRRR's standard pressure-level set: 1000/925/850/
700/500/400/300/250/200/150/100 mb plus the two boundary surfaces).
Pass `min_profile_levels: 0` to keep the partial rows for diagnostic
queries.

Confirmed against the dev mirror: the recent table has ~3.1 M rows
at 13 levels and ~250 k at 4–12 levels; the new default skips the
partial slice cleanly.

Tests: 8/8 HrrrProfileLookupTest cases (added two new ones for the
threshold). mix credo --strict clean.
2026-04-25 14:19:56 -05:00
FluxCD
5a0bd3bbf5 chore: update prop image to git.mcintire.me/graham/prop:main-1777141855-b34b186 [skip ci] 2026-04-25 18:41:06 +00:00
b34b186e61
fix(skewt): clip grid + traces to the plot box
Skewed isotherms, dry adiabats, and mixing-ratio lines were drifting
outside the frame at the corners — most visibly the warm-side
isotherms exiting the right edge near the top of the diagram. The
existing `clip_polyline/1` only filtered with a ±200 px slack, so
segments crossing the boundary still drew past the frame.

Wrap every grid layer + the T/Td/parcel polylines in a
`<g clip-path="url(#skewt-plot-clip)">`. The clipPath defines a
rectangle from (left, top) to (right, bottom). Frame outline and
axis labels stay outside the clip group so they keep rendering at
the box edge unchanged. The hover-cursor crosshair also lives
outside the clip so its readout box can sit flush against the plot
corner without being trimmed.

mix test: 2,902 / 2,902 + 221 properties green. mix credo --strict
clean.
2026-04-25 13:30:25 -05:00
FluxCD
e255fcf3b6 chore: update prop image to git.mcintire.me/graham/prop:main-1777141619-be11749 [skip ci] 2026-04-25 18:29:01 +00:00