Commit graph

965 commits

Author SHA1 Message Date
1faeb60e3c
Add cdo to runtime image for NARR GRIB1 decoding
NARR is GRIB1 and wgrib2 doesn't decode GRIB1 messages
(rd_grib2_seq_msg, grib1 message ignored, use wgrib). cdo handles
GRIB1 + the Lambert conformal projection + nearest-neighbor remap
that NARR's Grid 221 needs. Pulls in libnetcdf/libhdf5/libudunits2
transitively, ~30-60 MB image growth.
2026-04-15 18:54:02 -05:00
FluxCD
8320becee7 chore: update prop image to git.mcintire.me/graham/prop:main-1776291073-13180d5 [skip ci] 2026-04-15 22:12:52 +00:00
13180d572e
NARR backfill plan + spike-captured inventory fixture
Replaces the broken ERA5/CDS path with NCEP NARR fetched anonymously
from NCEI. NARR is GRIB1, anonymous HTTPS, byte-range-fetchable, with
1979-01-01 → 2014-10-02 coverage that lines up perfectly against the
HRRR archive start. Variables match the existing era5_profiles schema
1:1 (DPT is a direct surface dewpoint record, no SPFH derivation
needed at the surface).

Phase 1 spike done by hand: byte-range fetched 5 surface records and
3 pressure-level records for DFW 2010-06-15 12Z. wgrib2 turned out to
NOT read GRIB1 messages (rd_grib2_seq_msg, grib1 message ignored, use
wgrib), so the plan now uses cdo for both GRIB1 decoding and Lambert
conformal nearest-neighbor remap. cdo -merge is the right operator
for combining records of different variables — cdo -cat is for time
concat and silently drops mismatched levels, shell cat produces an
invalid composite. cdo emits param numbers as varNNN; mapping table
is in the plan's Architecture section.

Spike values for DFW 2010-06-15 12Z (all physically sane):
  TMP_2m=296.94 K (23.8°C)   DPT_2m=294.49 K (21.3°C)
  HPBL=703 m                  PRES_sfc=99383 Pa
  PWAT=45.1 mm                TMP_850mb=291.52 K
  HGT_850mb=1547.8 m          SPFH_850mb=10.4 g/kg

Plan file kept at the original 2026-04-15-merra2-historical-backfill
filename for git history continuity even though the implementation
is NARR not MERRA-2 (history block at the top documents the pivot).
2026-04-15 17:10:46 -05:00
FluxCD
7c5cbcfdcd chore: update prop image to git.mcintire.me/graham/prop:main-1776290697-26015ce [skip ci] 2026-04-15 22:05:50 +00:00
26015ce8ea
ProfilesFile.read/1: drop [:safe] flag, trust our own files
The persisted grid_data files contain atoms (:base_m, :top_m,
:thickness_m, :min_freq_ghz, :native_min_gradient, :ducts, etc.) that
live in modules which aren't eagerly loaded during application start.
warm_grid_cache_from_latest_profile runs in Application.start/2 before
those modules get pulled in by their first call sites, so [:safe] was
rejecting our own legitimate files at boot — every weather page hit
crashed with "invalid or unsafe external representation of a term".

The whole point of [:safe] is to refuse atoms from untrusted external
ETF input. ProfilesFile.write!/2 in this same module is the only writer
of these files, so the input is trusted by definition — drop the flag.
The test additions round-trip the production data shape including duct
fields as a regression guard.
2026-04-15 17:04:34 -05:00
FluxCD
04c8a92db8 chore: update prop image to git.mcintire.me/graham/prop:main-1776287079-9a78f8c [skip ci] 2026-04-15 21:06:23 +00:00
9a78f8c70c
ERA5: tighten stuck threshold to 8h and pause prod queues for diagnosis
Prod has 0 era5_profiles rows after ~500 submit cycles: every CDS job
either gets marked "rejected" (likely cap mismatch) or runs for 13+h
without transitioning to successful, so the handle_both_done branch
has literally never fired and the ERA5Batch: stored log line has
never been emitted once. Tighten Era5PollWorker's @stuck_after_seconds
from 18h to 8h so dead rows recycle within a work shift instead of a
full day, and pause the era5 / era5_submit / era5_poll / era5_batch
queues in runtime.exs so existing rows stay put for inspection while
we figure out why CDS is rejecting everything. Flip paused: true back
once the root cause is identified.
2026-04-15 16:04:12 -05:00
FluxCD
b5fb158fa4 chore: update prop image to git.mcintire.me/graham/prop:main-1776284076-529102d [skip ci] 2026-04-15 20:16:11 +00:00
529102d551
Filter weather grid by bounds before deriving sounding params
Timeline scrubs on /weather were reading a 21 MB NFS ProfilesFile and
then calling SoundingParams.derive on all 92k CONUS points before
filtering to the viewport. Flip the order: filter the raw grid_data map
by bounds first, so derivation work is bounded by the viewport (~4k
points on a DFW box) instead of the full grid.

Add build_grid_cache_rows/3 with an optional bounds argument; the /2
arity is unchanged so PropagationGridWorker still derives the full grid
on f00.
2026-04-15 15:14:11 -05:00
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