Commit graph

1704 commits

Author SHA1 Message Date
070e6d8099
Drop secret.yaml from kustomization to unblock Flux reconciliation
After the BFG scrub, k8s/secret.yaml is git-ignored (contains plaintext
credentials) but kustomization.yaml still listed it as a resource. Flux
has been failing to kustomize-build the prop-app for six days, so no
deployment since main-1776019122-8d1d7e4 has actually reached prod —
the beacons crash fix, UTC clock fix, ASOS nudging, MRMS, contact
detail async hydration, and email config change were all stuck.

The `prop-secrets` Secret is applied out-of-band with kubectl and lives
independently in the cluster; Flux no longer needs to track it.
2026-04-12 15:05:48 -05:00
FluxCD
ca8299a3f1 chore: update prop image to git.mcintire.me/graham/prop:main-1776024190-a6a71e8 [skip ci] 2026-04-12 20:04:22 +00:00
a6a71e8c94
Fix BackfillEnqueueWorker test timeouts and credo length warnings
The three BackfillEnqueueWorkerTest cases were timing out because the
default types list includes :era5, which cascades through inline Oban
into Era5Client.poll_and_download where Process.sleep blocks for the
full 60s test timeout. Era5Client uses bare Req.get with no plug hook,
so it can't be stubbed via Req.Test the way the other clients are. Pass
explicit non-ERA5 types in the three affected cases — ERA5 has its own
coverage and these tests don't assert anything era5-specific.

Also replace two `length(results) > 0` checks in asos_nudge_test with
`results != []` to silence credo warnings.
2026-04-12 15:02:48 -05:00
FluxCD
20623f9ebf chore: update prop image to git.mcintire.me/graham/prop:main-1776023389-ed67efb [skip ci] 2026-04-12 19:53:20 +00:00
ed67efb256
Add MRMS rain mosaic, fix beacons crash, fix UTC clock flash
MRMS
----
Layer the NOAA MRMS PrecipRate product onto the score grid so rain fade
updates every 2 minutes instead of every hour alongside HRRR. New modules:

- Microwaveprop.Weather.MrmsClient: fetches the latest .grib2.gz off the
  NCEP mirror (Req auto-decompresses so no gunzip step), writes the raw
  GRIB2 to a temp file, and calls the existing wgrib2 wrapper with the
  0.125 propagation grid spec to get interpolated cells. Returns a
  %{{lat, lon} => mm_per_hour} map with missing-value sentinels dropped.

- Microwaveprop.Weather.MrmsCache: ETS-backed GenServer mirroring
  ScoreCache/GridCache. Caches a single "current" entry keyed by
  valid_time with PubSub broadcast so peer nodes stay in sync and only
  the Oban leader pays the fetch + regrid cost.

- Microwaveprop.Workers.MrmsFetchWorker: cron every 2 minutes, short-
  circuits when the cached valid_time already matches the newest file.

Microwaveprop.Propagation.AsosNudge.compute/4 now takes an optional
rain_grid. When a cell has MRMS rain >= 0.1 mm/hr it gets patched onto
the HRRR profile's `precip_mm` field (the scorer already reads it there)
and the cell is re-scored even with no ASOS station nearby. Cells with
MRMS rain below the threshold aren't touched so dry cells keep their
raw HRRR scores (which have the wind/sky/native-gradient signal that
isn't persisted on HrrrProfile rows and would otherwise be lost).

AsosAdjustmentWorker pulls MrmsCache on every tick and passes the grid
through to AsosNudge.compute/4. Also skips the IemClient error branch
that can never happen and handles the ASOS-empty + MRMS-empty case
explicitly. MrmsCache wired into the supervision tree; MrmsFetchWorker
cron entry added to config.exs and dev.exs.

Four new AsosNudge cases cover MRMS-only re-scoring, threshold gating,
and the wet/dry score delta.

Beacons 500
-----------
Beacon.format_freq/1 and format_mw/1 crashed on whole-number floats
(e.g. 24192.0) because `frac == 0.0` could become false under float
rounding while `trim_trailing_zeros/1` stripped the decimal point,
leaving a 1-element list that couldn't be destructured as [_, frac].
Shared format_number/1 helper handles integer input directly and
pattern-matches both the "int-only" and "int + frac" shapes.

Added stream_data property tests covering the whole microwave range for
both integers and floats to catch this class of bug before prod.

UTC clock flash
---------------
The /weather and /map UTC clocks were empty until the JS hook mounted
post-WebSocket, producing a several-second blank spot on initial load
and a clobber risk on sidebar re-renders. Mount now computes a
server-rendered `initial_utc_clock` string and the template seeds the
element with that plus `phx-update="ignore"` so LiveView morphdom won't
overwrite what the hook writes.
2026-04-12 14:49:20 -05:00
FluxCD
be47877c06 chore: update prop image to git.mcintire.me/graham/prop:main-1776022314-a9dc49b [skip ci] 2026-04-12 19:32:16 +00:00
a9dc49b7cc
Note CONUS-only coverage on the about page
Upstream sources (HRRR, ASOS, IEMRE, ITU terrain) are all continental US
only, so make that explicit alongside the existing scoring caveat.
2026-04-12 14:31:30 -05:00
FluxCD
ca1e4cdd0c chore: update prop image to git.mcintire.me/graham/prop:main-1776022075-c318c3a [skip ci] 2026-04-12 19:29:14 +00:00
c318c3a932
Nudge HRRR scores with live ASOS obs between runs
Adds Microwaveprop.Propagation.AsosNudge: a pure IDW bias-field module
that takes ASOS observations + HRRR profiles and returns re-scored grid
rows for every cell within 250km of a reporting station. Upper-air
fields (min_refractivity_gradient, pwat_mm, hpbl_m, profile, duct
metadata) pass through unchanged so HRRR's signal isn't clobbered.

The old AsosAdjustmentWorker was unwired and buggy — nil'd out ~22% of
the scoring weight and wrote orphan timestamps. Replaced with a slim
worker that queries the latest HRRR valid_time, fetches live ASOS
currents, calls AsosNudge.compute/3, and upserts onto
(lat, lon, valid_time, band_mhz) so nudged values overwrite the HRRR
hour cleanly instead of polluting available_valid_times. After each
upsert it warms ScoreCache and broadcasts propagation:updated so live
/map clients refresh.

Cron hooked up every 10 minutes in config.exs and dev.exs. Also cleaned
up the stale "dev has propagation disabled" note in CLAUDE.md.

13 new AsosNudge unit tests cover: residual computation (co-located,
out-of-grid, nil fields), IDW weighting (single station, far station,
two equidistant stations, nil component handling), upper-air
preservation, and the compute/3 entry point's shape and radius filter.

Drive-by Styler formatting touched a handful of unrelated files from
`mix format`.
2026-04-12 14:27:27 -05:00
FluxCD
8225242697 chore: update prop image to git.mcintire.me/graham/prop:main-1776021055-7dcb940 [skip ci] 2026-04-12 19:11:55 +00:00
7ed429364a Stream contact detail sections in as each enrichment loads
The contact detail page used to gather every enrichment source in one
parallel_hydrate/2 bundle and await all five tasks before updating any
assigns, so users watched the whole right-hand column flash in at the
speed of the slowest query (usually hrrr_profiles_for_path against a
42M-row table).

Each source now runs in its own start_async task: :weather, :solar,
:hrrr_path, :terrain, :iemre. Matching handle_async/3 clauses assign
the slot and call refresh_computed/1, which rebuilds elevation_profile
+ propagation_analysis + data_sources from whatever's currently in
assigns. Sections appear independently as their data arrives, and the
existing PubSub handlers (:hrrr_ready, :weather_ready, :terrain_ready,
:solar_ready) still pick up later backfill completions the same way
they did before.
2026-04-12 14:09:45 -05:00
FluxCD
480b6decdf chore: update prop image to git.mcintire.me/graham/prop:main-1776020457-058ff6f [skip ci] 2026-04-12 19:01:43 +00:00
d6e9d80ce9 Send mail as prop@w5isp.com with reply-to graham@mcintire.me
Centralized the From and Reply-To headers in Microwaveprop.Mailer.apply_defaults/1
so UserNotifier and EditNotifier can't drift. Drops the EMAIL_FROM env var
override — both values are hardcoded now.
2026-04-12 14:00:38 -05:00
FluxCD
f7ba94b136 chore: update prop image to git.mcintire.me/graham/prop:main-1776020247-ad202e5 [skip ci] 2026-04-12 18:58:43 +00:00
a24ce027f4 Fix /weather 500 on cold cache, untrack k8s secret
The /weather LiveView was timing out on pod restart because
latest_weather_grid/1 ran load_weather_grid_from_db/1 synchronously on
cache miss — that query reads 176k hrrr_profiles rows with two huge
JSONB columns (profile array + duct_characteristics), and JSONB
decoding blocked the LiveView process past the 15-second pool
ownership timeout.

On cache miss latest_weather_grid/1 now returns empty immediately and
kicks off a deduped background fill through GridCache.claim_fill/1 (a
per-valid_time ETS lock so N concurrent mounts after restart don't each
fire the slow query). When the fill completes it broadcasts
weather:updated, and the handle_info in WeatherMapLive hits the warm
cache and pushes rows over the socket.

Added load_weather_grid/1 as a synchronous sibling for tests and any
caller that genuinely needs inline data. Tests updated to use it and
to clear GridCache in setup so ETS state doesn't leak between cases.

Also untrack k8s/secret.yaml and add both it and k8s/*-secret.yaml to
.gitignore — those manifests hold plaintext SMTP and database
credentials that should not live in the repo. Secrets already in git
history should be rotated separately.
2026-04-12 13:56:59 -05:00
FluxCD
c560323ed9 chore: update prop image to git.mcintire.me/graham/prop:main-1776019122-8d1d7e4 [skip ci] 2026-04-12 18:39:39 +00:00
291214033a Match /map sidebar layout on /weather
Right-hand dark sidebar holds the layer selector, description,
navigation links, and auth menu. Mobile floating panel mirrors the
pattern used on /map so users get the same controls in both views.
2026-04-12 13:38:16 -05:00
FluxCD
0a8c779783 chore: update prop image to git.mcintire.me/graham/prop:main-1776018974-4bc1980 [skip ci] 2026-04-12 18:37:39 +00:00
5102c2044e Merge CI image-tag commits from gitea 2026-04-12 13:35:44 -05:00
cb6cc8a6e2 Fix map forecast hours, blink, and broken prod email
- Forecast hour clicks showed only a small square of coverage because
  :preload_forecast ran at mount using a hardcoded fallback bounding box
  instead of the client's actual viewport. Move the preload trigger into
  handle_event("map_bounds", ...) so the cache always matches the
  viewport the client just asked about.

- Map overlay blinked on load/update because renderScores called Leaflet
  GridLayer.redraw(), which removes every tile element before recreating
  them. Repaint the existing tile canvases in place instead.

- Prod SMTP to mail.smtp2go.com was failing with an "unexpected_message"
  TLS alert: gen_smtp wasn't sending SNI so the wildcard cert endpoint
  rejected the handshake. Set tls: :always with explicit tls_options
  (server_name_indication, versions, verify).
2026-04-12 13:34:12 -05:00
FluxCD
c1c06a6773 chore: update prop image to git.mcintire.me/graham/prop:main-1776017794-e08879a [skip ci] 2026-04-12 18:17:35 +00:00
FluxCD
476050fce5 chore: update prop image to git.mcintire.me/graham/prop:main-1776017752-36689f4 [skip ci] 2026-04-12 18:16:35 +00:00
80de61451c Fix bullet indent on CSV/ADIF upload tabs
list-inside aligned wrapped lines with the bullet marker; use
list-outside + pl-5 so wrapped text indents cleanly past the bullet.
2026-04-12 13:16:06 -05:00
eb1e80958c Normalize ADIF MODE/SUBMODE and document the mapping
- AdifImport.normalize_mode/2 maps ADIF MODE (and SUBMODE when present)
  onto the fixed set of modes the site accepts: CW/SSB/FM/FT8/FT4/Q65.
  SUBMODE takes precedence so `MODE=MFSK, SUBMODE=FT8` correctly imports
  as FT8. USB/LSB/DSB all become SSB; NBFM/WFM become FM.
- Unknown modes (RTTY, PSK31, etc.) map to nil so the row still imports
  — mode is optional on the submission path now.
- AdifImport.mode_mapping_reference/0 returns the user-facing list of
  recognized inputs and their mappings.
- Submit page's ADIF tab renders that list in a table so users can see
  upfront what will happen to their file before uploading.
2026-04-12 13:15:31 -05:00
FluxCD
038f9ad54d chore: update prop image to git.mcintire.me/graham/prop:main-1776017473-76f947d [skip ci] 2026-04-12 18:11:35 +00:00
578b4ede86 Serve /contacts/map payload via HTTP endpoint, parallel hydrate
- Add ContactMapController serving /api/contacts/map with pre-gzipped
  JSON from Microwaveprop.Cache. Browsers handle Content-Encoding: gzip
  natively; payload drops from 5.7 MB raw to 1.5 MB on the wire (73%).
  Cache TTL 10 min, invalidated on contact insert alongside the other
  contact-map cache keys.
- Move contact map payload out of the LiveView's initial HTML entirely.
  Shell renders with the filter chrome (count + band list); the hook
  fetches contacts from the HTTP endpoint in parallel with map init.
  This also avoids Phoenix.Socket's 64 KB frame limit (push_event would
  have required chunked transfer or a global frame size bump).
- Parallelize ContactLive.Show hydrate: five independent DB loads
  (weather, solar, hrrr_path, terrain, iemre) now run concurrently via
  Task.async + Task.await_many. Total latency drops from sum(each) to
  max(each), cutting contact show hydrate time roughly 5x.
2026-04-12 13:10:51 -05:00
FluxCD
01b2a48a93 chore: update prop image to git.mcintire.me/graham/prop:main-1776016575-b686ee9 [skip ci] 2026-04-12 17:57:32 +00:00
253adaf89b Cache /weather grid, defer contact show mount, cache stats
- Add Weather.GridCache: ETS cache of derived HRRR grid rows keyed by
  valid_time, cluster-synced via PubSub. Eagerly warmed from
  PropagationGridWorker after each upsert so /weather map pan/zoom and
  weather_point_detail hit zero DB on warm cache.
- Replace latest_weather_grid DB query path with cache-first lookup +
  DB fallback. hrrr_profiles is 42M rows partitioned; pulling 3-10k
  rows per viewport on every pan was the main cost.
- ContactLive.Show: defer the heavy enrichment loads (weather, solar,
  HRRR, terrain, IEMRE, elevation profile, ITU-R propagation analysis,
  data_sources) into a handle_info(:hydrate) that runs after the shell
  renders. Initial mount now returns nil placeholders; template already
  had :if guards for all of them. Shell-to-first-paint goes from
  ~500ms-2s down to ~20ms.
- Cache fetch_queue_counts for 5s in ContactLive.Show — oban_jobs group
  by query was running on every contact page view.
- Backfill stats: wrap count_unprocessed, fetch_stats, fetch_db_stats
  in Microwaveprop.Cache with 2-5s TTLs; bump refresh debounce from 1s
  to 2s so bulk enrichment events don't thrash the DB.
2026-04-12 12:55:50 -05:00
FluxCD
5b98928fd9 chore: update prop image to git.mcintire.me/graham/prop:main-1776016069-5711118 [skip ci] 2026-04-12 17:48:30 +00:00
6c334d6e18 Cache /contacts, fix map blink, make mode optional
- Add contacts_inserted_at_desc_id_desc_idx so /contacts list runs as an
  Index Scan (~0.4ms) instead of a Seq Scan + top-N heapsort (~77ms on
  58k rows)
- Add Microwaveprop.Cache: tiny ETS-backed TTL cache for memoizing
  expensive-but-stale-tolerant values
- Cache total_entries for unsearched /contacts page loads (30s TTL,
  invalidated on insert)
- Cache the entire /contacts/map payload (pre-encoded JSON + band list),
  moving load_contacts into Radio.contact_map_payload; invalidated on
  new contact insert. Mount goes from ~150-300ms to ~5ms.
- Fix /map overlay blinking on load: reuse the Leaflet GridLayer across
  renderScores calls and just redraw() against the updated ScoreGrid,
  instead of removeLayer + new layer which flashed between tile regens
- Make mode optional on submission: schema change (null: true),
  submission_changeset no longer requires it, blank strings normalise to
  nil, CSV import accepts 6-column (no mode) or 7-column layouts
- core_components .input adds a red asterisk to labels when required,
  giving users a visual cue for which fields must be filled on /submit
2026-04-12 12:47:25 -05:00
FluxCD
8b9d0a29dc chore: update prop image to git.mcintire.me/graham/prop:main-1776014809-7bbd6ad [skip ci] 2026-04-12 17:27:25 +00:00
250709a1b2 Add more caching to make the map feel instant
- ScoreCache stores {band, valid_time} as %{{lat, lon} => score} map so
  point lookups are O(1); adds fetch_point/4 and valid_times/1
- available_valid_times/1 reads directly from ScoreCache when warm,
  falls back to DB on cold start
- point_forecast/3 iterates cached valid_times and uses fetch_point/4
  instead of hitting the DB per click
- NexradCache: node-local ETS cache of decoded n0q PNG pixel buffers
  keyed by 5-minute rounded timestamp; skips ~1-5s HTTP+decode on
  concurrent/repeat clicks within the same window
- MapLive: start_async the rain_scatter fetch so point_detail renders
  immediately with a pending marker; push rain_scatter_update when
  NEXRAD resolves
- MapLive: preload all 18 remaining forecast hours for the current
  viewport after mount/band change/propagation_updated; client caches
  them and renders timeline scrubs instantly without a server roundtrip.
  Adds set_selected_time event for fast-path state sync.
- Propagation map JS: forecastCache map + drawScatterMarkers helper,
  timeline click uses preloaded cache when available
2026-04-12 12:26:25 -05:00
FluxCD
8b407c87ee chore: update prop image to git.mcintire.me/graham/prop:main-1776013912-b322e76 [skip ci] 2026-04-12 17:13:23 +00:00
d880201713 Cache propagation scores in ETS, broadcast across cluster
- Add ScoreCache GenServer with node-local ETS table keyed by
  {band, valid_time}, subscribed to "propagation:cache" PubSub topic so
  every pod stays in sync with a single hourly compute
- scores_at/3 checks cache first, falls back to DB and populates on miss
- PropagationGridWorker warms and broadcasts the cache for each band
  after every forecast hour upsert; prunes >2h old entries
- Replace per-pixel string-keyed Map with flat Int8Array over the CONUS
  grid in propagation_map_hook.ts to eliminate allocations in the tile
  rasterization hot loop (interpolateScore / propagationReach)
2026-04-12 12:11:26 -05:00
FluxCD
6fd0958574 chore: update prop image to git.mcintire.me/graham/prop:main-1776008479-61cb7fb [skip ci] 2026-04-12 15:41:48 +00:00
91fb5cda22 Delay disconnect alerts by 3s to avoid flashing on brief hiccups 2026-04-12 10:40:42 -05:00
FluxCD
e3b2d6315d chore: update prop image to git.mcintire.me/graham/prop:main-1776007906-9cef467 [skip ci] 2026-04-12 15:32:47 +00:00
dd13a5f2a5 Fix three security issues
1. Prevent user_id impersonation in contact submissions
   - Remove user_id from submission_changeset cast fields
   - Pass user_id as a separate server-side argument to create_contact/2
   - Client form params can no longer forge account-linked contacts

2. Restrict flag toggle to admins only
   - Add admin? guard to toggle_flag event handler
   - Hide flag button from non-admin users in template

3. Reset enrichment on timestamp edits
   - Add qso_timestamp to the fields that trigger enrichment reset
   - Prevents stale weather/HRRR/IEMRE data after time changes
2026-04-12 10:31:23 -05:00
FluxCD
361a302736 chore: update prop image to git.mcintire.me/graham/prop:main-1776007679-385cffd [skip ci] 2026-04-12 15:28:45 +00:00
FluxCD
a022321235 chore: update prop image to git.mcintire.me/graham/prop:main-1776007633-515b9fc [skip ci] 2026-04-12 15:27:48 +00:00
163883d0dd Fix donate link contrast on dark mode info alert 2026-04-12 10:27:40 -05:00
7fb340bc35 Fix all remaining credo --strict issues (0 issues)
Aliases: add module aliases for 9 nested module references
Apply: replace apply/3 with direct module attribute calls
Line length: break 1 long spec line
Refactoring: extract helpers to reduce complexity and nesting
in show.ex, radio.ex, weather workers, terrain, duct detection,
backfill dashboard, contact map, and mix tasks
2026-04-12 10:26:53 -05:00
9abbb83469 Fix credo warnings: struct specs, length/1, and test patterns
- Replace %Struct{} with Struct.t() in all @spec annotations
- Replace length(x) > 0 with x != [] in test assertions
- Fix multi-line spec struct references in weather.ex
2026-04-12 10:26:53 -05:00
a07a3c56b4 credo 2026-04-12 10:26:53 -05:00
FluxCD
ae4417d594 chore: update prop image to git.mcintire.me/graham/prop:main-1776004759-c9305f9 [skip ci] 2026-04-12 14:39:31 +00:00
2f7a060f95 Increase test DB query timeout to 60s
The ownership_timeout controls how long a process can hold a checkout,
but the per-query Postgrex timeout (default 15s) is separate. Oban
inline mode chains workers synchronously, so individual queries can
exceed 15s under concurrent test load.
2026-04-12 09:38:59 -05:00
FluxCD
3db8bc85a1 chore: update prop image to git.mcintire.me/graham/prop:main-1776004649-d7acdfe [skip ci] 2026-04-12 14:38:32 +00:00
2769e1a948 Increase test DB ownership timeout to 60s
Prevents sporadic sandbox timeout failures under full concurrent
test load (24 async cases competing for DB connections).
2026-04-12 09:37:08 -05:00
FluxCD
48b365af5e chore: update prop image to git.mcintire.me/graham/prop:main-1776004392-a6efe38 [skip ci] 2026-04-12 14:34:28 +00:00
aba4ca538d Add PayPal donate link to footer and about page 2026-04-12 09:34:01 -05:00