Commit graph

1001 commits

Author SHA1 Message Date
FluxCD
b148e37c8e chore: update prop image to git.mcintire.me/graham/prop:main-1776361644-ac4e43c [skip ci] 2026-04-16 17:48:38 +00:00
ac4e43c726
Remove 500-limit on BackfillEnqueueWorker cron
10K+ HRRR-pending contacts were starving the 200-ish NARR candidates
out of the 500 per-run slot each 30 min because NARR candidates sort
last by the :hrrr_status priority. Drop the cap so every eligible
contact gets enqueued on each cron run; queue concurrency still paces
the actual fetches. Worker keeps supporting an explicit "limit" arg
for ad-hoc runs and tests.
2026-04-16 12:46:56 -05:00
FluxCD
2871f63cca chore: update prop image to git.mcintire.me/graham/prop:main-1776361263-f93f5c9 [skip ci] 2026-04-16 17:42:38 +00:00
f93f5c9a57
Rename 9cm band 3456 → 3400
The US 9cm amateur allocation was cut from 3300-3500 to 3300-3450 MHz
in 2020, so 3456 (the legacy weak-signal calling frequency) is no
longer in-band. Move the canonical key to 3400, migrate existing
contacts, and keep 3456 input resolving to 3400 via the
nearest-band snap so historical ADIF/CSV logs still import cleanly.
2026-04-16 12:40:38 -05:00
FluxCD
bc7f3f4137 chore: update prop image to git.mcintire.me/graham/prop:main-1776360998-36adf87 [skip ci] 2026-04-16 17:37:37 +00:00
36adf875b7
Add 50/144/222 MHz bands, rename 440→432
Expands submittable-contact bands to include 6m, 2m, 1.25m, and 70cm
(as 432 rather than the old 440 placeholder). Each new band gets an
explicit allocation window in BandResolver.nearest_band so ADIF FREQ
fields near the amateur allocations resolve correctly while 60-900 MHz
frequencies outside those windows are still rejected. Microwave
(>= 900 MHz) snapping is unchanged — nearest-band match across the
full @allowed_bands list.

Also adds BandConfig entries for 50 and 222 (tropo-only config, same
pattern as 144/432). Sporadic-E / F2 / meteor scatter modeling is not
yet in scope — ionosphere data is only used to compute an Es readout
on /path for 50/144/222/432.
2026-04-16 12:36:10 -05:00
FluxCD
90df646c94 chore: update prop image to git.mcintire.me/graham/prop:main-1776360352-11a50bf [skip ci] 2026-04-16 17:27:34 +00:00
11a50bf2c2
Disable PDF export on live_table pages
PDF generation was failing in production. CSV export still works.
2026-04-16 12:25:25 -05:00
FluxCD
5e5663fbb1 chore: update prop image to git.mcintire.me/graham/prop:main-1776360188-01d554f [skip ci] 2026-04-16 17:24:33 +00:00
01d554f2e7
Hourly ContactPositionBackfillWorker fills null pos1/pos2
Safety net for rows that land via direct DB writes (manual fixes, bulk
imports) and bypass Radio.create_contact's grid-resolution requirement.
Runs every hour on the :admin queue with a 500-row per-invocation cap.
Fills whichever side resolves — if one grid is invalid, the other still
gets populated; distance_km is only written when both positions end up
set.
2026-04-16 12:22:34 -05:00
FluxCD
1a8e55dfb0 chore: update prop image to git.mcintire.me/graham/prop:main-1776355088-bbeb95f [skip ci] 2026-04-16 15:59:59 +00:00
bbeb95ff5d
CSV/ADIF import: upsert existing contacts on grid/mode refinement
Widen dedup match to 4-char grid prefix so an upload with a longer grid
(e.g. FN42aa25) finds the existing FN42 contact. Within a match, classify
as a refinement when the upload strictly extends the existing grid or
fills a missing mode; as a contradiction (still skipped) when grids or
modes genuinely disagree. Refinements flow through a new preview bucket,
update the existing row in place, and re-enqueue enrichment when the
position changed.
2026-04-16 10:57:37 -05:00
FluxCD
824143ad64 chore: update prop image to git.mcintire.me/graham/prop:main-1776351221-be6f1a1 [skip ci] 2026-04-16 14:55:32 +00:00
be6f1a1d28
CsvImport: header-driven column mapping
The /contacts LiveTable export produces a 12-column CSV with label-style
headers (Station 1 / Grid 1 / QSO (UTC) / ...) in a different column
order plus extras (id, distance_km, hrrr_status, flagged_invalid,
inserted_at). The old positional import rejected it with "expected 6 or
7 columns, got 12".

Replace the positional parser with header-driven mapping:
- Parse the header row, normalize each cell (downcase + trim), and look
  it up in an alias map that covers both the raw field names and the
  export labels.
- Required columns: station1, station2, grid1, grid2, band, qso_timestamp
  (mode stays optional). If any are missing the header, return
  {:error, {:missing_required_columns, [...]}}.
- Unknown columns are silently ignored so the export's extras don't break
  round-trip.
- Per-row validation stays the same (changeset errors still reported by
  row number).

Existing 6- or 7-column positional CSVs that already have a header still
work — the header parser recognizes `station1`, `station2`, etc.
directly.
2026-04-16 09:53:14 -05:00
c7f9aac3f9
NarrClient: parse cdo numeric codes for cdo-version independence
Prod cdo 2.5.1 (Debian) emits named shortnames (tpag10, 2tag2, saip, code11…)
where cdo 2.6.0 (Mac) emits var11/var33/etc. The parser only recognized
varNN, so every prod NARR fetch failed with "no parseable values in cdo
output" and all 350 in-flight jobs discarded.

Switch cdo args from `-outputtab,name,lev,value` to `-outputtab,code,...`.
cdo then outputs the raw numeric NCEP GRIB1 parameter code regardless of
version, and @cdo_code_to_name maps that integer to the semantic name we
already use downstream (TMP/HGT/SPFH/…).

Verified by reproducing on both a 2010 NARR file (fixture) and a 2007 file
fetched live — both now emit identical numeric code rows.
2026-04-16 09:45:23 -05:00
FluxCD
c1cac4f394 chore: update prop image to git.mcintire.me/graham/prop:main-1776350332-f005ddc [skip ci] 2026-04-16 14:40:29 +00:00
f005ddcaac
NarrClient: include cdo output snippet in parse-failure errors
Prod hit "no parseable values in cdo output" for a 2007 record.
Without the actual cdo output it's guesswork whether it's a different
parameter-table format, an empty result, or something else. Capture
stderr (stderr_to_stdout: true) and include first 500 chars of output
so the error message has enough signal to diagnose.
2026-04-16 09:38:27 -05:00
FluxCD
a2cdee7a31 chore: update prop image to git.mcintire.me/graham/prop:main-1776349903-55d4f28 [skip ci] 2026-04-16 14:33:27 +00:00
55d4f289c2
NarrClient.in_coverage?/1 + callsite guards (1979 → 2014-10-02)
Post-2014 contacts with hrrr_status=:unavailable were being dispatched to
NARR, which 404s because NCEI's archive ends 2014-10-02. 14K of the 14.3K
candidates in prod are post-2014 — these are HRRR's responsibility.

- NarrClient.in_coverage?/1 returns true only inside 1979-01-01 → 2014-10-02
- narr_jobs_for_contact and maybe_enqueue_narr (contact_live) bail out of
  coverage returning []
- BackfillEnqueueWorker.type_filter scopes :narr to qso_timestamp <
  coverage_end so the cron doesn't keep picking post-2014 candidates
2026-04-16 09:31:19 -05:00
FluxCD
f5132a991d chore: update prop image to git.mcintire.me/graham/prop:main-1776349371-3604896 [skip ci] 2026-04-16 14:24:23 +00:00
3604896726
Rename ERA5 → NARR across the codebase
- Schema: Era5Profile → NarrProfile; table renamed via migration
  rename_era5_profiles_to_narr_profiles (ALTER TABLE + rename indexes,
  atomic and instant, no row rewrite)
- Weather helpers: find_nearest_era5/era5_for_contact/era5_profiles_for_path
  → find_nearest_narr/narr_for_contact/narr_profiles_for_path
- BackfillEnqueueWorker: :era5 → :narr type (virtual, no narr_status column);
  reconcile_stale_queued_to_unavailable and status_priority_order now skip
  virtual types via @virtual_types. Fixes the prod crash where the cron tried
  to update a non-existent era5_status column.
- ContactWeatherEnqueueWorker: build_era5_jobs → build_narr_jobs,
  era5_jobs_for_contact → narr_jobs_for_contact
- ContactLive: @era5 → @narr, maybe_enqueue_era5 → maybe_enqueue_narr;
  UI label "ERA5 (0.25°)" → "NARR (32 km)"
- Cron (runtime.exs): args types list now "narr" instead of "era5"
- /status: progress row, status-by-type card, totals.narr_profiles, and
  table-count lookup all target narr_profiles
- Drop obsolete Era5CdsJob schema + era5_cds_jobs table (inspection
  artifact from the retired CDS pipeline; 34 orphan rows in prod)
- Misc docstring/comment cleanups (skew_t, about, wgrib2, propagation_train)

Includes a regression test for the virtual-type crash.
2026-04-16 09:22:23 -05:00
FluxCD
9e94759f7a chore: update prop image to git.mcintire.me/graham/prop:main-1776347847-7857d3b [skip ci] 2026-04-16 13:59:17 +00:00
7857d3bc5a
Status page: relabel ERA5 → NARR, include NARR in backfill cron
- /status progress bar, status-by-type card, and internal map keys now
  read "NARR" (data-progress-key="narr", narr_candidates/narr_done).
  era5_profiles table stays as the DB landing spot (historical artifact).
- BackfillEnqueueWorker cron now passes "era5" in types so pre-2014
  contacts whose hrrr_status is :unavailable actually get NarrFetchWorker
  jobs enqueued every 30 min. The :era5 type key still maps internally
  to NarrFetchWorker via era5_jobs_for_contact/1.
2026-04-16 08:57:01 -05:00
FluxCD
17271b264a chore: update prop image to git.mcintire.me/graham/prop:main-1776347392-1d740ca [skip ci] 2026-04-16 13:50:15 +00:00
1d740caf8f
Fix credo warnings: length/1 on list and nested module aliases
- skew_t_test: replace length(list) > 0 with list != []
- conn_case: alias DataCase, GridCache, ScoreCache at top
2026-04-16 08:49:25 -05:00
bcce4b4de9
Add :exports Oban queue for LiveTable CSV/PDF workers
LiveTable.Workers.CsvExportWorker and PdfExportWorker enqueue on :exports.
Prod had 2 pending jobs stuck in `available` with no queue to execute them.
2026-04-16 08:48:36 -05:00
FluxCD
437eeb1c47 chore: update prop image to git.mcintire.me/graham/prop:main-1776346020-1f2a729 [skip ci] 2026-04-16 13:31:10 +00:00
1f2a729d40
Drop dead Era5*/CDS code paths
After the NARR backfill landed, nothing reachable from the app calls
any of these modules. Net removal: ~2700 lines.

Deleted:
- Microwaveprop.Workers.{Era5Fetch,Era5Submit,Era5Poll,Era5MonthBatch}Worker
- Microwaveprop.Weather.{Era5BatchClient,Era5Client}
- Mix.Tasks.Era5Backfill (superseded by BackfillEnqueueWorker cron)
- Matching test files (~1100 LOC of test)
- era5/era5_submit/era5_poll/era5_batch queue blocks from runtime.exs
- era5_req_options Req.Test stub config from test.exs

Kept (actively used or intentionally preserved):
- Era5Profile schema (the era5_profiles table is the NARR target)
- Era5CdsJob schema + its test (inspection handle on the era5_cds_jobs
  table, which retains rows from the failed CDS runs)
- :era5 type key in ContactWeatherEnqueueWorker / BackfillEnqueueWorker
  (the symbol still means "historical backfill", just routed to NARR now)

Swapped the one remaining live Era5FetchWorker call site in
contact_live/show.ex's maybe_enqueue_era5 path to NarrFetchWorker
(with NarrClient.snap_to_analysis_hour for the 3-hourly slot).

Test suite: 1513 tests, 0 failures (-50 from the deleted era5 test
files). No compile warnings. Credo clean.
2026-04-16 08:26:29 -05:00
FluxCD
d70444dd06 chore: update prop image to git.mcintire.me/graham/prop:main-1776297271-6169b31 [skip ci] 2026-04-15 23:58:22 +00:00
6169b31726
Mark Era5* queues as DEPRECATED (replaced by NARR) 2026-04-15 18:54:02 -05:00
4aa23ae67f
ContactWeatherEnqueueWorker: dispatch NarrFetchWorker for historical backfill
Swap the body of era5_jobs_for_contact/1 to enqueue NarrFetchWorker
instead of Era5FetchWorker, and snap qso_timestamp via
NarrClient.snap_to_analysis_hour/1 so it lands on a 3-hourly NARR
analysis slot (00/03/06/09/12/15/18/21 UTC) — NarrFetchWorker raises
on anything else.

The :era5 type key, the era5_jobs_for_contact function name, and
the era5_status field are kept as historical artifacts — they
still refer to the era5_profiles table, which is reused 1:1 by
NARR. A schema rename is a follow-up PR.

The CDS Era5* workers (Era5FetchWorker, Era5SubmitWorker,
Era5PollWorker, Era5MonthBatchWorker, Era5BatchClient) are now
unreachable from the live submission flow. They stay in tree until
the deletion follow-up PR.
2026-04-15 18:54:02 -05:00
e44d7043e5
Add :narr Oban queue to dev and runtime configs
Concurrency 6 in both dev and prod. NARR fetches are network-bound
(byte-range GETs to NCEI) so high concurrency is fine — same shape
as the iemre queue in dev. Required for NarrFetchWorker (added in
the previous commit) to actually pick up jobs.
2026-04-15 18:54:02 -05:00
7c33af7278
Add Microwaveprop.Workers.NarrFetchWorker
Per-contact Oban worker that fetches one NARR profile via
NarrClient.fetch_profile_at/2 and inserts it into era5_profiles.

Replaces Era5FetchWorker for pre-2014 contacts. Unlike the ERA5
router, NARR fetches are per-point so this worker does the fetch
+ insert directly — no downstream batch worker, no routing.

Args shape matches Era5FetchWorker: %{"lat", "lon", "valid_time"}.
Lat/lon get snapped to 0.25° (same dedup granularity as ERA5).
valid_time MUST already be on a 3-hourly NARR analysis slot
(00/03/06/09/12/15/18/21 UTC on the hour) — the worker raises
ArgumentError via NarrClient.url_for/1 otherwise. Caller-side
snapping (in ContactWeatherEnqueueWorker) lands in the next task.

Existence check tightened to ±15 minutes (vs ERA5's ±30) since NARR
is exact 3-hourly. On error, logs a warning and returns {:error,
reason} so Oban retries with backoff (max_attempts: 3).

The :narr Oban queue config and the ContactWeatherEnqueueWorker
swap are deliberately NOT touched in this commit — those are the
next two tasks of the plan.
2026-04-15 18:54:02 -05:00
e5528d0135
NarrClient.extract_profile_from_file/3 + fetch_profile_at/2
extract_profile_from_file shells out to cdo:

    cdo -outputtab,name,lev,value -remapnn,lon=X_lat=Y <merged.grb>

parses the text output (varNNN ↔ NCEP GRIB1 param number lookup),
maps surface records to era5_profiles fields, builds the pressure
profile from each (TMP, HGT, SPFH) triple, and merges in the derived
fields from SoundingParams.derive/1. Returns the same attrs shape as
Era5BatchClient.build_profile_attrs/5 so the existing
bulk_insert_profiles path can ingest NARR rows unchanged.

fetch_profile_at picks records from a fetched inventory, byte-range
fetches each one into a per-record temp file, runs cdo -merge to build
a composite, calls extract_profile_from_file, and cleans up via
try/after.

Drive-by fix: ThetaE.dewpoint_from_spfh/2 returns Kelvin (its existing
test asserts that explicitly). Era5BatchClient was passing that
Kelvin value straight into "dwpc" (Celsius), which would then crash
SoundingParams.compute_refractivity_profile when it called Buck's
saturation-vapor-pressure equation with t=294 instead of t=21. The
bug never surfaced because era5_profiles is empty in production —
no ERA5 backfill has ever succeeded. Fixed in both era5_batch_client
and the new narr_client by subtracting 273.15 in the wrapper. Both
wrappers now have a comment pointing at the K→C conversion.

Test fixture test/fixtures/narr/narr_dfw_2010-06-15_12z.grb is the
spike-captured composite (1.1 MB — Lambert conformal projection
isn't sellonlatbox-subsettable so we can't shrink it further).
2026-04-15 18:54:02 -05:00
081cd47bb5
NarrClient.parse_inventory/2 + fetch_inventory/1
parse_inventory walks the wgrib1-format .inv text and returns a
{var, level} -> {byte_offset, length} index, computing each record's
length from the next record's offset (or file_size for the last one).
fetch_inventory does a GET on the .inv URL plus a HEAD on the .grb URL
to learn its size, then delegates to parse_inventory.

Both stubbable via Req.Test (config/test.exs adds narr_req_options
matching the existing era5_req_options / giro_req_options pattern).
Spike fixture at test/fixtures/narr/narr-a_221_20100615_1200_000.inv
backs the parser test — verifies the ("TMP", "2 m above gnd") and
("WVVFLX", "atmos col") records line up with the real bytes.
2026-04-15 18:54:02 -05:00
b7e19e18cc
Add Microwaveprop.Weather.NarrClient URL/time helpers
First slice of the NARR backfill client: url_for/1,
url_for_inventory/1, and snap_to_analysis_hour/1. Pure functions,
no network. Validates that NARR analyses only exist at 3-hourly
slots (00/03/06/09/12/15/18/21 UTC). The byte-range fetch and
cdo-driven point extraction land in follow-up commits per
docs/plans/2026-04-15-merra2-historical-backfill.md.
2026-04-15 18:54:02 -05:00
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