Commit graph

956 commits

Author SHA1 Message Date
FluxCD
ba23ca2b3e chore: update prop image to git.mcintire.me/graham/prop:main-1776283206-6d69989 [skip ci] 2026-04-15 20:04:08 +00:00
6d69989599
HfMuf physics: distance-adjusted MUF from GIRO MUFD(3000)
First HF prediction building block. GIRO publishes MUFD — the F2-layer
maximum usable frequency at the 3000 km reference distance — directly
from station measurements, already calibrated against CCIR M(3000)F2
climatology. Rather than recomputing MUF from scratch (which would
require the ~2 MB CCIR coefficient tables), this module treats the
measured MUFD as the anchor and scales it to the actual path distance
using a thin-layer secant-of-incidence ratio.

That keeps the measurement calibration (~3.3× foF2 at our fixture, vs
~5.1× from a naive thin-layer formula) and only uses the thin-layer
math for the relative distance correction.

* adjust_mufd/2 — scales MUFD(3000) to an arbitrary hop distance.
  At D=3000 it's a no-op; shorter paths get lower MUF (near-vertical),
  longer paths within the single-hop window get higher MUF.
* fot/1 — standard 0.85 × MUF Frequency of Optimum Traffic.
* hf_score/2 — 0-100 band-vs-MUF score with 5-tier curve:
  100 (≤ 0.75×MUF) / 80 (FOT) / 50 (≤ MUF) / 20 (fringe) / 0 (dead).

Limitations are in the moduledoc: single-hop only, nearest-station
bias, no LUF/D-layer absorption, no Kp geomagnetic storm modeling.
Those are separate commits once this has bake time.

Not yet wired into PathLive — follow-up commit will add HF bands to
BandConfig and wire the panel.
2026-04-15 14:59:39 -05:00
FluxCD
fa2c9643c9 chore: update prop image to git.mcintire.me/graham/prop:main-1776282948-923c8a4 [skip ci] 2026-04-15 19:57:06 +00:00
923c8a468d
SpaceWeather: SWPC JSON ingestion (Kp, F10.7, GOES X-ray)
Adds the second ionospheric data source layer. Polls NOAA SWPC's free
public JSON services every 5 minutes for three products:

* Planetary K-index (1-min cadence) → geomagnetic_observations
* 10.7 cm solar flux (hourly) → solar_flux_observations
* GOES X-ray flux (1-min, 0.1-0.8nm long-wave band only) →
  solar_xray_observations

All three products feed HF / Es scoring inputs that the propagation
engine will start using in a follow-up commit:

* Kp drives HF absorption and Es damping (memory notes Kp is inversely
  correlated with tropo/Es stability).
* F10.7 is the SSN proxy for ITU-R P.533 HF MUF prediction.
* GOES X-ray long-wave flux is the short-wave fade (SWF) / D-layer
  absorption proxy — a C-class flare starts attenuating HF within
  seconds of the X-ray peak.

New context `Microwaveprop.SpaceWeather` exposes `upsert_*/1` and
`latest_*/0` per product; `SpaceWeatherFetchWorker` pulls all three
independently so one endpoint's outage doesn't block the others.
Fixtures captured from live SWPC endpoints on 2026-04-15 for the
parser tests.

Not yet wired into scoring — that's a separate commit.
2026-04-15 14:55:24 -05:00
FluxCD
6a33866133 chore: update prop image to git.mcintire.me/graham/prop:main-1776282553-83ea123 [skip ci] 2026-04-15 19:51:05 +00:00
83ea123e22
PathLive: ionosphere readout panel for VHF paths
Closes the GIRO → SporadicE → display loop. When a 144 or 440 MHz path
is calculated, PathLive looks up the nearest polled ionosonde's latest
foEs via Ionosphere.nearest_foes/3, runs SporadicE.es_score for the
actual (band, distance) pair, and renders a panel showing:

* Live foEs / foF2 / station-code / timestamp
* Computed single-hop Es MUF for this exact path length
* 0-100 Es score for the selected band
* Human-readable interpretation ("strong opening", "tropo only",
  "out of single-hop window", etc.)

Panel only appears for 144/440 (Es isn't relevant on microwave) and
only when the nearest station has data within the last 2 hours — if
ionosonde data is stale or missing, the panel is omitted entirely
rather than shown with misleading values.

Does not touch the grid scorer on /map — a grid cell has no intrinsic
path length, so Es scoring there is conceptually muddy. PathLive is
the right home for path-specific predictions.
2026-04-15 14:48:47 -05:00
FluxCD
33524ece3d chore: update prop image to git.mcintire.me/graham/prop:main-1776282255-f632ea8 [skip ci] 2026-04-15 19:46:04 +00:00
f632ea83bd
Sporadic-E physics module + Ionosphere.nearest_foes
Adds the missing link between the GIRO ionosonde data we just started
ingesting and the VHF/UHF band scoring:

* `Microwaveprop.Propagation.SporadicE` — ITU-R P.534-6 / Davies 1990
  thin-layer Es MUF (sec(i) · foEs with h=110 km), plus an `es_score/3`
  that maps (foEs, target band, hop distance) into a 0-100 single-hop
  propagation likelihood. Calibrated against literature: 50 MHz Es at
  2000 km needs foEs ≳ 5.5 MHz (routine summer); 144 MHz needs
  foEs ≳ 15.8 MHz (rare Jun/Jul peaks only); 440 MHz Es does not occur
  at physical foEs values. Multi-hop Es and Es-scatter are separate
  factors and explicitly out of scope.

* `Ionosphere.nearest_foes/3` — given a lat/lon, returns the latest
  observation from the nearest polled GIRO station (Millstone Hill or
  Alpena for now), with a configurable staleness cutoff (default 2h).
  Returns `{:error, :stale}` or `{:error, :no_data}` so callers can
  choose whether to apply the Es factor at all.

Neither is wired into the grid scorer yet — that's a separate commit
so the integration can be reviewed on its own.
2026-04-15 14:43:48 -05:00
FluxCD
8afbe1932f chore: update prop image to git.mcintire.me/graham/prop:main-1776281892-361b9de [skip ci] 2026-04-15 19:42:03 +00:00
361b9dec5a
Ionosphere: GIRO ionosonde ingestion (foF2/foEs/hmF2/MUFD)
First step toward physics-based HF / sporadic-E scoring. Polls GIRO's
DIDBase tabular endpoint every 10 minutes for two US ionosondes
(Millstone Hill MHJ45, Alpena AL945), parses the plain-text response
into observation rows, and upserts into a new ionosonde_observations
table keyed on (station_code, valid_time).

This gets foEs (E-layer sporadic critical frequency) into the database
as a direct measurement — the key input for predicting 144 MHz Es
openings via ITU-R P.534-6. foF2 + MUFD are the F2-layer inputs for
HF MUF predictions.

Not yet wired into scoring. Boulder/Wallops/Austin/Idaho/Point Arguello
are in the GIRO catalog but were silent when probed — add them back
if/when they come online.

Next steps: SWPC JSON (Kp, F10.7, sunspot), GOES X-ray flux, D-RAP text,
and the P.534-6 Es scoring factor that uses foEs at midpoint for the
144/440 band configs.
2026-04-15 14:37:43 -05:00
FluxCD
139f9f72ea chore: update prop image to git.mcintire.me/graham/prop:main-1776280977-91c8cc9 [skip ci] 2026-04-15 19:23:59 +00:00
91c8cc9bc7
Add 144 MHz and 440 MHz bands (tropo only)
Both bands share the physics of low microwave: humidity and refractivity
gradient build ducts the same way, and rain + gas absorption are
effectively zero at VHF/UHF. humidity_effect is :beneficial, rain_k /
rain_alpha are (0, 1), and o2_db_km / h2o_coeff are 0.

Range estimates reflect tropo reality: 2m ducts can reach 2500+ km
under exceptional conditions (e.g. documented Hawaii ↔ California
openings), 70cm a bit less. The seasonal base matches the low-microwave
summer-peak curve.

These configs do NOT capture sporadic-E, meteor scatter, or aurora —
those need ionospheric data (GIRO ionosondes, NOAA SWPC) we don't yet
ingest. The scoring on these bands is therefore "tropo-only" and will
underestimate openings driven by ionospheric modes.
2026-04-15 14:22:33 -05:00
FluxCD
1d0aa2848e chore: update prop image to git.mcintire.me/graham/prop:main-1776280381-118ad4e [skip ci] 2026-04-15 19:13:57 +00:00
118ad4e8cc
WeatherMap: actually move the legend to top-right
The previous fix only updated the initial mount path. buildLegend gets
re-created on every layer switch via the swap at renderLayer, and that
code path still used the old isMobile-dependent bottomright position,
which clobbered the top-right placement as soon as the user clicked a
different layer.
2026-04-15 14:12:29 -05:00
33347f7fc2
WeatherMap: legend in top-right, description back in sidebar, play/stop, fix reload loop
Corrections to the previous weather-map pass:

* The "helper" that was supposed to live in the top-right was the
  color legend, not the layer description. Move the Leaflet legend
  control to `topright` (was `bottomright` on desktop).

* Put the layer description back in the desktop sidebar where it
  used to live, now with a "Group · Label" header above the body
  text so it matches the new style in the mockup.

* Add play/stop controls to the weather forecast timeline, ported
  from the propagation map: start plays "now" → end at 1s/step and
  loops; stop returns to the time closest to wall clock. A manual
  click interrupts playback the same way it does on /map.

* Fix the reconnect/reload loop I introduced by making mount do a
  synchronous ProfilesFile read + derive for the full 92k-point
  grid. That was pushing mount past the LV socket join timeout on
  a cold pod, which makes the LV client fall back to a full HTTP
  reload every ~20 seconds (no server-side error). Mount now uses
  the original `latest_weather_grid/1` cache-only hot path and only
  `weather_grid_at/2` (sync disk read) on user timeline scrubs.

* Clean up the playback timer in the hook's `destroyed` callback.
2026-04-15 14:11:53 -05:00
FluxCD
ffc81b789e chore: update prop image to git.mcintire.me/graham/prop:main-1776279469-725a7e6 [skip ci] 2026-04-15 18:58:50 +00:00
725a7e6bda
WeatherMap: forecast-hour timeline + top-right layer description
Two related UI changes driven by the same data:

* Move the selected-layer description out of the sidebar and into a
  dedicated top-right overlay on the map. The sidebar kept the same
  layer buttons but dropped the descriptive text; the new overlay
  shows group + layer name + description in one card. Mobile still
  shows the description in the collapsible panel since it has no
  top-right free real estate.

* Add a forecast-hour timeline bar at the bottom of the map, matching
  the propagation map. WeatherMapLive enumerates ProfilesFile on mount
  (the grid worker has been persisting f00..f18 on disk since
  commit 07ffcf5), pushes data-valid-times for the JS hook to render
  as buttons, and handles a new select_time event by reading the
  per-hour ProfilesFile on demand via Weather.weather_grid_at/2.

weather_grid_at/2 deliberately skips the GridCache write path for
non-latest hours — caching 18 × 92k rows would add ~300 MB per pod.
A ~2 MB ETF decode per scrub is fast enough for a click.

Subscribes to propagation:pipeline so the timeline picks up new
forecast hours live as they land, without waiting for the full chain
to finish.
2026-04-15 13:57:23 -05:00
FluxCD
cacce902d3 chore: update prop image to git.mcintire.me/graham/prop:main-1776278588-790eef1 [skip ci] 2026-04-15 18:44:46 +00:00
790eef1726
PathLive: live-refresh the forecast when new grid scores land
Subscribe to "propagation:updated" on connected mount and, when the
grid worker finishes a forecast-hour step, re-read point_forecast for
the path midpoint using the stashed band_mhz and source/destination.
Terrain, HRRR profiles, and the loss/power budget are independent of
grid scores and stay as-is. Before this, a /path page rendered once at
mount and only refreshed on manual reload.
2026-04-15 13:42:44 -05:00
662d7232e5
ERA5 poll worker: snooze on transient HTTP errors instead of discarding
A brief CDS network blip on 2026-04-15 returned HTTP 5xx from check_status
for multiple concurrently-polling workers. The old `{:error, _}` branches
consumed attempts, max_attempts=10 burned out in seconds, and 13 rows
were discarded with no cleanup — orphaned with no active poll worker,
including two rows that had both CDS legs done and were ready to decode.
Result: zero era5_profiles ever inserted despite 59 tile-months in flight.

Transient HTTP errors are now a `{:snooze, 60}` so a CDS outage pauses
polling instead of killing it. Terminal states (:failed, :rejected,
:not_found, :stuck) still drive the fail/resubmit paths.
2026-04-15 13:42:44 -05:00
FluxCD
b889761a8e chore: update prop image to git.mcintire.me/graham/prop:main-1776273287-02b354f [skip ci] 2026-04-15 17:16:19 +00:00
02b354f5f7
Purge legacy is_grid_point=true rows from hrrr_profiles
The grid worker stopped writing HRRR profiles to the DB on April 14
when the forecast grid moved to /data/scores binary files, but the
24/72h-windowed prune left every is_grid_point=true row older than
72 hours sitting in the table (billions of rows across every
partition back to 2016, ~90% of the 146 GB table size). Nothing
reads them anymore.

Replace prune_old_grid_profiles/0 with purge_grid_point_profiles/0,
which walks every hrrr_profiles partition directly and deletes
is_grid_point=true rows regardless of age. Per-partition DELETEs
avoid a parent-table full scan. QSO-linked rows (is_grid_point=false)
are untouched — those still back contact enrichment and /path.

Call the new purge from the grid worker's end-of-chain hook so any
grid-point row that somehow slips back in gets cleaned up within the
next hour. Add `mix hrrr.purge_grid_points` for a one-shot historical
cleanup.
2026-04-15 12:14:20 -05:00
FluxCD
fe9fb2a66f chore: update prop image to git.mcintire.me/graham/prop:main-1776272300-c3616b3 [skip ci] 2026-04-15 17:00:14 +00:00
c3616b31c7
Surface HRRR weather details on /path
Adds an Atmospheric Profile section to the link calculator that shows
the HRRR data actually feeding the prediction: valid/run time, a
per-point table (src/mid/dst) with surface temp, dewpoint, pressure,
HPBL, PWAT, surface refractivity and dN/dh, plus a skew-T log-P chart
for the midpoint vertical profile.

Pulls the skew-T chart function out of contact_live into a shared
component so both LiveViews can render it without duplication.
2026-04-15 11:57:51 -05:00
FluxCD
84ffdcd261 chore: update prop image to git.mcintire.me/graham/prop:main-1776269371-7fcb271 [skip ci] 2026-04-15 16:11:01 +00:00
7fcb2715a0
Enlarge /path forecast chart so detail is legible
Viewbox goes from 300x96 to 1200x240 with aspect-[5/1] so the SVG
fills the card instead of shrinking to a ~300 px strip. Fonts,
gridlines (0/25/50/75/100), and point markers scale up to match,
and the x-axis now shows up to 7 evenly spaced hour labels instead
of just start/middle/end.
2026-04-15 11:09:05 -05:00
FluxCD
b76080bb0c chore: update prop image to git.mcintire.me/graham/prop:main-1776268032-e227978 [skip ci] 2026-04-15 15:48:57 +00:00
e227978f2a
Let the /path forecast chart start at the current analysis hour
point_forecast used a strict \`>= now\` cutoff, so the leftmost
"now" sample was always dropped — the HRRR publishing lag means the
newest analysis file is typically 30–60 min behind the wall clock,
which made the filter evict the very hour the chart most wanted to
anchor on. If the chain ever fell behind by even one step the whole
forecast went empty and the chart disappeared.

Use the same window as available_valid_times: keep everything from
one hour before now onward, and fall back to the single newest file
when nothing is fresh enough. Both the cache-path and store-path
flows go through a shared forecast_window helper so the behavior
stays in lockstep with the map timeline.
2026-04-15 10:46:42 -05:00
FluxCD
30b4baf792 chore: update prop image to git.mcintire.me/graham/prop:main-1776267507-222d0fd [skip ci] 2026-04-15 15:39:51 +00:00
222d0fdca3
Fix mojibake in /path loss budget gaseous label
HEEx attribute value syntax (label="...") treats the content as a
raw attribute string, not as an Elixir string literal — so escape
sequences like \u2082 are NOT interpreted and show up verbatim in
the rendered page. Replace with the literal subscript-2 characters
so the label reads 'Gaseous (O₂+H₂O)'.
2026-04-15 10:37:58 -05:00
FluxCD
c6d6d3f097 chore: update prop image to git.mcintire.me/graham/prop:main-1776265627-a9fa1d8 [skip ci] 2026-04-15 15:07:44 +00:00
a9fa1d8344
Pin propagation legend to top-right on desktop too
The forecast timeline at the bottom of the map now spans a lot of
hours horizontally, and on wide screens it ran into the legend
that used to anchor bottom-right — covering the later forecast
hours. Move the legend to top-right on every viewport (it was
already there on mobile for the same reason).
2026-04-15 10:06:37 -05:00
FluxCD
ad31f6763d chore: update prop image to git.mcintire.me/graham/prop:main-1776265421-1a9df52 [skip ci] 2026-04-15 15:04:43 +00:00
1a9df52102
Add play/stop controls to the propagation forecast timeline
New play and stop buttons live directly under the "Propagation
Forecast" label in the bottom timeline bar. Play starts iterating
one forecast hour per second from "Now" forward through every
loadable future hour, then loops back to "Now" and repeats. Stop
clears the interval and returns the map to "Now". A manual click
on any time button cancels playback so the user's selection isn't
overwritten by the next tick.

The existing click handler body moved into a shared
selectTimelineTime() so playback and manual clicks go through the
same cache-or-roundtrip path. A server-driven update_timeline event
(band change or new forecast hour) also tears the timer down before
overwriting timelineData underneath it.
2026-04-15 10:03:14 -05:00
FluxCD
4c1ef56537 chore: update prop image to git.mcintire.me/graham/prop:main-1776261922-11617e7 [skip ci] 2026-04-15 14:06:31 +00:00
11617e730b
Sync map progress chip to what's actually loadable
The "Updating propagation +Nh" chip was double-misleading in prod:
the label's +Nh was relative to the HRRR run time (which lags wall
clock by ~2h), and the progress broadcast fired before the hour was
fetched + persisted — so the chip could read "+8h" while the map
timeline only extended to +4h.

Reframe the label as "through now" / "through +Nh" / "through Nh ago"
by computing the offset from valid_time → now in PipelineStatus.
running_detail, so it matches the semantic the user reads off the
timeline. Move the PubSub broadcast in PropagationGridWorker to fire
after Propagation.replace_scores/2 succeeds, so the chip only
advances once that forecast hour is readable from ScoresFile.
2026-04-15 09:05:03 -05:00
FluxCD
9d2d67305e chore: update prop image to git.mcintire.me/graham/prop:main-1776260243-32fdb25 [skip ci] 2026-04-15 13:38:26 +00:00
32fdb2583d
Shrink PropagationGridWorker per-forecast-hour memory footprint
Two related optimizations in the propagation chain hot path. Both
land on the f01..f18 step that was previously OOM-killing prod
pods after the HRRR pressure-level footprint halved wasn't enough
to fit inside 4 Gi.

1. Skip the GridCache broadcast on forecast hours.

   /weather only ever renders the analysis hour (latest_grid_valid_time
   feeds the map). Building 92k rows, serializing them through PubSub,
   and rebuilding the {lat,lon}→row map on all three replicas was
   pure waste for f01..f18 — no consumer was reading that data. Only
   f00 now calls build_grid_cache_rows + broadcast_put. Point lookups
   for non-analysis hours still work through ProfilesFile on disk
   (weather_point_detail_from_profiles/3) exactly as before.

2. Fold replace_scores into a single streaming pass.

   The old path did `Enum.to_list/1` on the ~460k-entry score stream
   followed by `Enum.group_by/2`, holding two full copies of the grid
   before any file was written. A single `Enum.reduce/3` that folds
   each score into a per-band accumulator keeps only one copy and
   eliminates the group_by intermediate entirely. The public
   signature — an Enumerable in, {:ok, count} out — is unchanged.
2026-04-15 08:36:53 -05:00
FluxCD
3273a578b9 chore: update prop image to git.mcintire.me/graham/prop:main-1776259784-0c5cbbd [skip ci] 2026-04-15 13:35:24 +00:00
0c5cbbdc54
Bump prop pod memory limit 4Gi → 6Gi
Even with the narrow HRRR grid pressure levels (4fa6798), pods are
still OOMKilled partway through the propagation chain. After the
HRRR fetch, the grid worker pipes 92k grid points through native
duct metric merge (~100 MB held for ~2 minutes), builds the full
weather grid cache (92k rows × ~40 fields), runs compute_scores
(460k score maps across 5 bands materialized in a list), and
writes five .ntms files — all while ERA5Submit workers keep
churning through CDS backfill in background processes.

Nodes have ~42% memory headroom on limits; the 2 Gi bump fits
comfortably. Shrinking the in-process allocations is a better
long-term fix but needs a proper pass through compute_scores and
persist_profiles. This gets prod unstuck in the meantime so the
forecast timeline can actually populate.
2026-04-15 08:29:09 -05:00
FluxCD
38f6c37194 chore: update prop image to git.mcintire.me/graham/prop:main-1776259195-4fa6798 [skip ci] 2026-04-15 13:20:21 +00:00
4fa67984f3
Split HRRR pressure levels for grid hot path vs per-contact profiles
The skew-T commit (30c1018) doubled @pressure_levels from 13 to 25 so
new contact fetches would cover the full troposphere. That list is
also what PropagationGridWorker pulls per forecast hour, which
doubled the GRIB footprint (~57 MB compressed + 92k points × 25
levels × 3 vars decoded through wgrib2) and pushed prod pods over
their 4 Gi OOMKill threshold. Every chain died during f00 and the
map timeline never got beyond now and now+1h because the .ntms files
for f02-f18 were never written.

Split the constant:

  * @profile_pressure_levels (25 levels, 1000-100 mb) drives the
    per-contact HrrrClient.fetch_profile path so the skew-T plot
    keeps its full-atmosphere trace.

  * @grid_pressure_levels (13 levels, 1000-700 mb) drives the grid
    hot path. That's the band SoundingParams.derive reads for
    min_refractivity_gradient, and native hybrid-sigma data
    (native_min_gradient) takes priority over the pressure-level
    fallback anyway, so upper-air levels contribute nothing to
    scoring — pure memory waste on this path.

build_profile/1 still iterates the full 25-level list; grid fetches
simply populate the 13 near-surface slots and skip the rest.

Makes HrrrClient.pressure_messages public with a :grid | :profile
variant so the split is testable from outside the module.
2026-04-15 08:19:33 -05:00
FluxCD
e3fc09eee3 chore: update prop image to git.mcintire.me/graham/prop:main-1776206578-2d9e5a2 [skip ci] 2026-04-14 22:44:07 +00:00
2d9e5a2d1f
Make the map timeline read .ntms files authoritatively
Two related fixes so the main map reliably picks up new binary
propagation score files as soon as PropagationGridWorker writes them.

1. Propagation.available_valid_times/1 previously preferred ScoreCache
   over ScoresFile, using the cache as an index of what was available.
   The cache is a lazy ETS of whatever hours have been fetched or
   broadcast, which is a strict subset of what's on disk. A new
   forecast hour landing on disk while the cache was warm with older
   entries was invisible to the timeline until the cache happened to
   catch up. Read directly from ScoresFile so the disk store is the
   source of truth.

2. Add Propagation.scores_at_fresh/3 that always reads the .ntms file
   and overwrites the cache entry, and use it from MapLive's
   propagation_updated handler. PropagationGridWorker publishes the
   cache_refresh on `propagation:cache` and the timeline ping on
   `propagation:updated` as separate PubSub broadcasts, so by the time
   MapLive runs through scores_at the ScoreCache GenServer may not
   have processed the refresh yet — fetch_bounds then returns the
   previous chain's bytes. scores_at_fresh takes disk as the source
   of truth for the refresh path and warms the cache as a side effect
   so subsequent readers see the new data.
2026-04-14 17:42:30 -05:00
FluxCD
1a394fa7ae chore: update prop image to git.mcintire.me/graham/prop:main-1776205868-572c285 [skip ci] 2026-04-14 22:32:04 +00:00
572c2851b3
Draft upper-air factors section in algo.md pending backfill
With the native hybrid-sigma profile now storing levels all the way up
to ~19 km, the scorer can finally see the mid-to-upper troposphere.
The five factors that would plausibly add signal — 500 mb dewpoint
depression, 300 mb wind speed, 850→500 mb dθ/dz, tropopause height,
and 500 mb height anomaly — get rationale, derivation notes, and
placeholder weights. Actual weights have to wait for the native
backfill to catch up enough QSO hours for a representative training
sample, at which point the existing gradient-descent recalibration
pass refits the full factor set.
2026-04-14 17:30:39 -05:00
FluxCD
f57ea7b0c9 chore: update prop image to git.mcintire.me/graham/prop:main-1776205589-30c1018 [skip ci] 2026-04-14 22:28:03 +00:00
30c1018400
Extend skew-T plot to cover the full troposphere
Historical contacts showed a skew-T log-P diagram that stopped at 700 mb
because HrrrClient and Era5Client only fetched pressure-level data down
to the top of the boundary layer. The chart canvas already ran up to
100 mb, so the trace clipped mid-atmosphere.

Two complementary fixes:

1. Extend @pressure_levels in HrrrClient and Era5Client with
   650/600/550/500/450/400/350/300/250/200/150/100 mb so new fetches
   cover the full troposphere + lower stratosphere.

2. Prefer the native hybrid-sigma profile for the contact-detail
   skew-T when one has been backfilled for the contact's hour. The
   native profile already stores all 50 hybrid levels up to ~19 km,
   so historical contacts covered by the native backfill get a full
   trace without re-hitting S3. A new HrrrNativeProfile.to_skew_t_profile/1
   converts the parallel arrays into the %{"pres","tmpc","dwpc","hght"}
   list shape the renderer expects, deriving dewpoint from SPFH via the
   Magnus inverse. Weather.find_nearest_native_profile/3 mirrors
   find_nearest_hrrr/3 for the lookup.
2026-04-14 17:25:58 -05:00
FluxCD
9f5b427116 chore: update prop image to git.mcintire.me/graham/prop:main-1776204813-92ef6c6 [skip ci] 2026-04-14 22:15:00 +00:00
92ef6c67fc
Defer beacon coverage estimate until the toggle is flipped on
Previously RangeEstimate.estimate/1 ran on every beacon page mount,
even though the cells data was only rendered after the user flipped
the coverage toggle — a wasted bbox grid pass per page load. Start
with estimate: nil, compute lazily in handle_event("toggle_coverage")
when flipping on, and push the cells payload to the Leaflet hook via
a new load_coverage event. The JS hook no longer reads data-cells on
mount and only builds the cellLayer when the server sends it. Cached
in the socket so re-toggling is instant.
2026-04-14 17:13:05 -05:00