Commit graph

1126 commits

Author SHA1 Message Date
70e65ba034
refactor(map): drop the 7-day outlook strip, fix missing Analysis, trim timeline
- Remove the 7-day outlook strip and daily_outlook_at/3. It was
  derived from point_forecast, which is capped at HRRR's 18-hour
  horizon, so it never had 7 days of data to show.
- factors_for: fall back to the nearest persisted analysis profile
  at or before the requested time. Only f00 hours persist profiles,
  so clicking any other hour used to yield an empty Analysis and
  factor table. After the recent cursor fix lands 'Now' on a
  forecast hour more often, this regressed further.
- available_valid_times/1: cap the forward horizon to 18h from now.
  Leftover valid_times from prior cycles used to pile on the
  timeline without adding information.
2026-04-19 08:17:47 -05:00
FluxCD
240bc30df8 chore: update prop image to git.mcintire.me/graham/prop:main-1776604171-28a9eb3 [skip ci] 2026-04-19 13:11:34 +00:00
28a9eb3a92
fix(map-timeline): never label a future forecast hour as 'Now'
Both the Elixir initial-cursor pick and the JS timeline renderer
used nearest-by-absolute-distance, so any wall-clock time past
:30 snapped the 'Now' label onto the next top-of-hour forecast —
up to ~30 min in the future. Switch to the latest valid_time at
or before wall-clock, with a fallback to the earliest time when
every slot is future (shouldn't happen for HRRR, safe default).
2026-04-19 08:09:14 -05:00
6c652ef2d4
feat(contacts): private flag with scope-aware visibility
Contacts can be marked private at submit time (single form, CSV
import, ADIF import) and edit time. Private contacts are hidden
from the public map, the public /u/callsign profile, and the
browse table for anonymous and non-owning viewers. The original
submitter and admins see them inline on the browse table with a
"Yes" badge, and the detail page shows a lock icon. Non-authorized
viewers get a 404 on the detail page.
2026-04-18 17:49:01 -05:00
FluxCD
69f4c81899 chore: update prop image to git.mcintire.me/graham/prop:main-1776551705-4e6c87e [skip ci] 2026-04-18 22:37:31 +00:00
4e6c87eca2
feat(telemetry): broaden Instrument span coverage
Adds spans to 15 previously-unmeasured hot paths so every question we
might ask while tuning has a histogram to answer it:

External I/O:
- iem.fetch_iemre (gridded weather reanalysis)
- mrms.list_latest / mrms.download (precip radar)
- rtma.fetch_observation
- ncei.fetch_metar (historical 5-min METAR backfill)
- solar.fetch_indices (GFZ solar indices)
- swpc.fetch (SWPC Kp/F10.7/X-ray)
- giro.fetch (ionosonde)
- qrz.request, geocoder.geocode (callsign enrichment)
- srtm.download_tile (terrain tile download + gunzip)
- hrrr.download_grib_ranges (parallel byte-range fetch phase)

Subprocess:
- wgrib2.extract_grid / extract_grid_from_file / extract_grid_from_file_mapped

LiveView hot paths:
- propagation.scores_at (map score fetch + cache hit/miss counter)
- propagation.point_forecast (sparkline)
- propagation.point_detail (click-to-inspect)
- propagation.daily_outlook_at (/map outlook strip)

Worker-level end-to-end:
- worker.terrain_profile
- worker.mechanism_classify
- worker.mrms_fetch

Each event is registered in Microwaveprop.PromEx.InstrumentPlugin as
a Prometheus histogram (default / long buckets as appropriate) plus
a counter for the scores_at cache hit/miss ratio. Prometheus at
10.0.15.25 will start seeing the new series on the next scrape after
deploy.
2026-04-18 17:25:33 -05:00
FluxCD
d9ffda43d3 chore: update prop image to git.mcintire.me/graham/prop:main-1776549124-30d5bb7 [skip ci] 2026-04-18 21:55:26 +00:00
30d5bb7d79
feat(k8s): NodePort service for external Prometheus scraping
A dedicated Prometheus at 10.0.15.25 on the private network can now
scrape the cluster via `<any-node-ip>:30090/metrics`. Scrape rotates
across pods; BEAM metrics stay representative as a sample and the
oban queue-depth gauge reads from the shared DB so all pods report
the same value.
2026-04-18 16:51:46 -05:00
9af50a9e34
fix(metrics): serve scrape body via PromEx.get_metrics
PromEx.Plug's internal path check doesn't match when forwarded under
/metrics (conn.path_info is [] after the forward strips the prefix),
so it silently returned without sending a response. Call
PromEx.get_metrics/1 directly and send the text body ourselves —
tested, returns 200 with all 23 registered event handlers producing
proper Prometheus histogram output.
2026-04-18 16:50:37 -05:00
FluxCD
80b0dcd5ba chore: update prop image to git.mcintire.me/graham/prop:main-1776548438-c7bc3ed [skip ci] 2026-04-18 21:47:23 +00:00
c7bc3ed5d0
feat(telemetry): PromEx Prometheus exporter at /metrics
Wires PromEx with its built-in Application / BEAM / Phoenix / Ecto /
Oban plugins plus a custom `Microwaveprop.PromEx.InstrumentPlugin`
that registers Prometheus histograms for every
`Microwaveprop.Instrument` span (HRRR/NEXRAD/IEM/GEFS/NARR/UWYO
/Elevation fetches, DB batch upserts, terrain analysis, radar
aggregation, propagation score band). Queue-depth gauges from the
10-second poller land at `microwaveprop_oban_queue_count{queue,state}`.

Router forwards `/metrics` to MicrowavepropWeb.MetricsPlug which
optionally requires a bearer token (`PROMETHEUS_AUTH_TOKEN` env var in
runtime.exs); leave it unset to expose metrics publicly and restrict
at the ingress / Cloudflare Access layer instead.

Example external Prometheus scrape config:

    scrape_configs:
      - job_name: microwaveprop
        scrape_interval: 30s
        metrics_path: /metrics
        scheme: https
        authorization:
          type: Bearer
          credentials: <token>
        static_configs:
          - targets: ['prop.w5isp.com:443']
2026-04-18 16:39:39 -05:00
FluxCD
0833ee44f9 chore: update prop image to git.mcintire.me/graham/prop:main-1776548026-2da74c5 [skip ci] 2026-04-18 21:36:02 +00:00
2da74c5cd8
feat(telemetry): wide instrumentation + bump hrrr to 2 per pod
Config:
- runtime.exs hrrr queue 1 → 2 (6 concurrent HRRR jobs across 3 pods)

New helper Microwaveprop.Instrument.span/3 wraps :telemetry.span with
a [:microwaveprop | event_suffix] prefix so metrics can key off it.

Spans added (each emits duration + result tag):
- HrrrClient.fetch_grid / fetch_profile / fetch_idx
- NexradClient.fetch_frame + decode_png
- IemClient.fetch_asos / fetch_raob
- GefsClient.fetch_grid_profiles
- NarrClient.fetch_profile_at
- UwyoSoundingClient.fetch_sounding
- ElevationClient.fetch_elevation_profile
- Weather.upsert_hrrr_profiles_batch / upsert_gefs_profiles_batch
- Propagation.replace_scores
- PropagationGridWorker.compute_scores_algorithm
- TerrainAnalysis.analyse
- CommonVolumeRadarWorker.aggregate_stats

Telemetry catalog (MicrowavepropWeb.Telemetry):
- Oban job duration / queue_time / count / exception by (worker, queue, state)
- Per-span summary metrics for every instrumented phase above
- Periodic (10s) poller emits oban queue depth by (queue, state) — drops
  into the /admin/dashboard Metrics tab immediately

Also drops the now-redundant "fetching n0q frame" and "fetching <url>"
info lines from CommonVolumeRadarWorker / NexradClient; the span events
cover that and the worker's "ingested" line stays for per-job signal.
2026-04-18 16:33:34 -05:00
FluxCD
a69933a589 chore: update prop image to git.mcintire.me/graham/prop:main-1776547343-1a62e51 [skip ci] 2026-04-18 21:24:59 +00:00
1a62e51f82
chore(radar): drop per-fetch ok-bytes log line 2026-04-18 16:22:04 -05:00
ced5e3d1a6
feat(radar): log NEXRAD fetches + worker progress
Previously the CommonVolumeRadarWorker ran silently — no URL logged,
no sign in the logs that it was doing any work. Added info-level
fetch/ok/error logs in NexradClient (same shape as WeatherFetchWorker
RAOB logs) plus start-of-work + ingestion lines in the worker itself.
Unchanged: the existing "no frame" warning and all control flow.
2026-04-18 16:20:59 -05:00
af6c676df5
chore(branding): rename site to "Microwave Propagation"
Nav bar and browser tab now show "Microwave Propagation" instead of
"NTMS Propagation Prediction". The About and Privacy pages still
reference NTMS since those describe the organization running the
service, which is unchanged.
2026-04-18 16:18:21 -05:00
FluxCD
78bf4b5664 chore: update prop image to git.mcintire.me/graham/prop:main-1776546307-be62e27 [skip ci] 2026-04-18 21:08:55 +00:00
be62e272e9
feat(contacts): widen sounding search, backfill on demand
Contact detail now searches for soundings in progressively wider radii
(150 → 300 → 600 → 1000 km) before declaring "No soundings found".

When even 1000 km turns up nothing, enqueue WeatherFetchWorker RAOB
jobs for every station at the widest non-empty radius. The worker
passes the contact_id through so it can PubSub back to
contact_enrichment:<id> as each sounding lands, and the LiveView
rehydrates without a refresh.

Also adds Weather.soundings_with_widening_radius/1 and
ContactWeatherEnqueueWorker.enqueue_raob_fetch_with_widening/1 so the
same logic can be reused from other callers.
2026-04-18 16:04:50 -05:00
FluxCD
d3876362ca chore: update prop image to git.mcintire.me/graham/prop:main-1776545634-a9a355b [skip ci] 2026-04-18 20:57:52 +00:00
a9a355be2a
feat(format): miles-primary distance formatting sitewide
Add Microwaveprop.Format.distance_km/1 and a matching formatDistanceKm
helper for the JS side. Both emit "X mi (Y km)" with one decimal under
10 mi and whole numbers above.

Replace the ad-hoc format_dist/format_km_mi helpers with the shared
formatter and update every user-facing distance render: contact
detail, contacts index column, user profile contact/involving tables,
path finder summary + sounding list, contacts map line popups, beacon
coverage tooltip, and propagation map range estimate + rain scatter
cell popup.
2026-04-18 15:53:40 -05:00
FluxCD
1f89b5ddae chore: update prop image to git.mcintire.me/graham/prop:main-1776545313-ca07c02 [skip ci] 2026-04-18 20:51:38 +00:00
ca07c0288d
feat(contacts): antenna height fields feed terrain analysis
Add optional height1_ft/height2_ft to contact schema, submit form, and
edit form (direct + suggest-edit paths). Heights flow through to the
elevation-profile terrain analysis so clearance and diffraction are
computed from the actual antenna height instead of a 10-ft default.
Editing heights resets terrain_status and re-enqueues
TerrainProfileWorker so the stored path analysis picks up the new
geometry.

Also fix mark_likely_duct/3: the pattern was grabbing the element
instead of the index, so no duct ever got flagged as the likely
propagation path even when extract_ducts returned a valid layer.
2026-04-18 15:48:14 -05:00
FluxCD
67d4b2eca7 chore: update prop image to git.mcintire.me/graham/prop:main-1776543737-5a25c6d [skip ci] 2026-04-18 20:29:32 +00:00
5a25c6d649
ci: install buildx and use docker buildx build --push
The Dockerfile uses `--mount=type=cache` which requires BuildKit.
The static docker tarball only ships the CLI, so the plugin has to
be installed separately. Also flip the build step to
`docker buildx build --push` so build and push happen in one pass
instead of two (avoids the intermediate local-daemon load).
2026-04-18 15:21:30 -05:00
508834633e
ci: install docker CLI from static tarball, not apt
The runner image's bookworm apt repos have been failing GPG
verification with "At least one invalid signature was encountered"
on every job. Clearing lists and reinstalling debian-archive-keyring
on the host doesn't help because the runner spins up a fresh
container per job. Dodge apt entirely by grabbing the static Docker
27.5.1 client binary from download.docker.com. The binary talks to
the host's /var/run/docker.sock that act_runner mounts into the job
container, which is all we need for build + push.
2026-04-18 15:01:24 -05:00
a6af6b115a
feat(map): 7-day outlook strip
Adds a compact seven-card horizontal strip to both mobile and desktop
map sidebars showing the per-day peak score at the viewport center
for the selected band. Cards are color-coded (emerald for 80+,
rose for bad) so the weekend verdict reads at a glance.

- Propagation.daily_outlook_at/3: groups point_forecast entries by
  UTC date, picks each day's peak, returns ascending order
- MapLive mounts with today's outlook for the initial center; the
  select_band handler refreshes it from the viewport midpoint so the
  strip tracks the user's current band and rough location
- Empty-state message covers the case where no extended-horizon
  scores have landed yet (fresh deploy, or before the first GEFS
  run completes)

This is the consumer of the GEFS pipeline — once the cron starts
running and f024-f168 scores accumulate, days 2-7 of the strip
populate automatically.
2026-04-18 14:46:44 -05:00
4d0c15e3b8
feat(gefs): score extended-horizon grid and seed cron
Wires the GEFS fetch pipeline into the existing scoring machinery:

- GefsFetchWorker runs Propagation.score_grid_point over every
  fetched grid cell and persists the result via replace_scores/2, so
  Day 2-7 valid_times show up on the map through the same
  Propagation.scores_at/3 path HRRR uses
- Empty-args perform/1 seeds the chain by enqueueing f024-f168 (25
  jobs at 6-hour cadence) for the most recent GEFS run that should
  be published given NOMADS' ~3-4h publication lag
- Cron: seeds at 05:30, 11:30, 17:30, 23:30 UTC — 5 hours after each
  00/06/12/18Z run
- GefsClient.build_profile now emits :wind_u / :wind_v atom keys to
  match the scorer's input shape; DB columns keep the *_mps suffix
  for unit clarity, remapped at write time

GEFS pgrb2a lacks HPBL, native-level duct data, NEXRAD, and
commercial-link degradation — the extended-horizon score is a
rougher signal than the HRRR-driven one but covers the window HRRR
can't reach.
2026-04-18 14:41:41 -05:00
0537a1831b
feat(gefs): ingest worker and grid-profile extraction
- GefsClient.build_profile/1: converts raw wgrib2 output into the
  scorer-shaped profile, deriving Td from RH via Magnus at both the
  surface and each pressure level
- GefsClient.fetch_grid_profiles/3: downloads the idx sidecar from
  NOMADS, byte-ranges only the surface messages, hands the slim
  GRIB2 binary to wgrib2 -lola for bilinear regridding from 0.5°
  onto the CONUS 0.125° grid
- GefsFetchWorker: one Oban job per (run_time, forecast_hour),
  upserts into gefs_profiles and runs SoundingParams.derive so the
  same refractivity/ducting metrics flow through

Oban gets a new :gefs queue (2 slots dev/config, 1 slot per prod
pod). Test env gets a gefs_req_options Req.Test stub slot so future
integration tests can drive the full fetch path. No cron wiring or
scorer integration yet — that's Step 3.
2026-04-18 14:36:03 -05:00
a5c7dce147
feat(gefs): scaffold extended-horizon forecast ingestion
Lays the groundwork for a Day 2-7 propagation outlook driven by NCEP
GEFS ensemble-mean output, picking up where HRRR's 18-hour horizon
leaves off.

- GefsClient: NOMADS URL builder, forecast-hour list (3h to +240,
  6h to +384), ensemble-mean message inventory, and a Magnus
  dewpoint derivation (pgrb2a publishes RH rather than Td)
- gefs_profiles table + GefsProfile schema mirroring hrrr_profiles
  so the scorer can run over rows from either source
- Weather.upsert_gefs_profile/1 and upsert_gefs_profiles_batch/1

No worker or UI yet — those land in follow-up commits.
2026-04-18 14:28:44 -05:00
FluxCD
d54f4cd907 chore: update prop image to git.mcintire.me/graham/prop:main-1776538587-cf6812f [skip ci] 2026-04-18 18:59:58 +00:00
cf6812f154
fix(contact): stop loading-flash on already-enriched contacts
Two related fixes:

- Drop the global contact_enrichment:{hrrr,weather,solar} subscriptions.
  These topics fire for every QSO's enrichment completion, and the
  handlers blindly re-queried data for the currently-displayed contact,
  causing spurious re-renders whenever any enrichment worker finished
  anywhere in the system. The per-contact terrain subscription stays —
  that one is correctly scoped.

- Only add a slot to `hydration_pending` when the contact's status
  column says the work is genuinely in flight (:queued or :processing).
  Previously every connected mount flashed spinners over all eight
  enrichment sections, even for contacts whose data was already cached
  and about to return instantly from the async task.

Also drop the Donate-to-NTMS footer link.
2026-04-18 13:56:01 -05:00
FluxCD
871582ee09 chore: update prop image to git.mcintire.me/graham/prop:main-1776538284-e3248e3 [skip ci] 2026-04-18 18:54:58 +00:00
e3248e335b
revert(map): remove antenna-height control
The antenna-height input only scaled the displayed range labels and
the viewshed max-range — it never fed into the propagation score
itself. Showing the control implied the colored overlay responded to
it, which was misleading. Viewshed now uses a fixed 10 m baseline,
matching the previous 33 ft default.
2026-04-18 13:50:56 -05:00
FluxCD
72d4ad4815 chore: update prop image to git.mcintire.me/graham/prop:main-1776537850-5488744 [skip ci] 2026-04-18 18:47:55 +00:00
5488744f44
feat(contact): loading spinners, callsign-aware summaries, axis callsigns
Three improvements to the contact detail page:

* Track async hydration per source in `hydration_pending` so the
  Terrain / Soundings / Atmospheric Profile sections render a loading
  spinner while their task is in-flight instead of briefly flashing
  "no data available" before the payload arrives.
* Propagation analysis summary now names the origin station by
  callsign ("Path is terrain-obstructed at N mi from W5XD") instead
  of the generic "station 1" placeholder.
* Elevation profile chart labels the 0-mi and max-mi ticks with the
  matching callsign so it's obvious which end of the profile is
  which station.
2026-04-18 13:43:35 -05:00
FluxCD
35130a6c16 chore: update prop image to git.mcintire.me/graham/prop:main-1776537504-7330d64 [skip ci] 2026-04-18 18:41:54 +00:00
7330d64c5a
feat(contacts): surface tropo duct count and suggest-edit hint
Propagation analysis panel now adds a "Tropo duct detected at N/M HRRR
samples along the path" line whenever any HRRR sample flags ducting,
even if the overall mechanism classification doesn't end up pointing at
the duct (e.g. short paths, clear-LoS contacts). Also rewords the map
caveat to invite users with better coordinates to file a suggested
edit — clickable when logged in, otherwise a login link.
2026-04-18 13:37:54 -05:00
FluxCD
bccc0b66c1 chore: update prop image to git.mcintire.me/graham/prop:main-1776537173-e827cac [skip ci] 2026-04-18 18:36:53 +00:00
e827cacc48
feat(map): scale propagation ranges with antenna height
Antenna height now drives typical/extended/exceptional range estimates
and viewshed max range via a sqrt(h_m/10) factor clamped to [0.6, 2.0].
Baseline 33 ft matches the existing defaults. Changing the height input
now pushes update_band_info and refreshes the clicked point's detail
panel plus viewshed so the range-estimate line and coverage shape track
the user's actual station height.
2026-04-18 13:32:24 -05:00
FluxCD
8bded0c6e8 chore: update prop image to git.mcintire.me/graham/prop:main-1776536769-d64e502 [skip ci] 2026-04-18 18:29:49 +00:00
d64e502908
fix(maps): same reconnect/visibility fix for rover and weather hooks
rover_map_hook and weather_map_hook share the same viewport-scoped
data flow as the propagation map — they hit the same blank-tile bug
after tab hide or socket reconnect. Add reconnected() + visibilitychange
handlers and document the requirement in CLAUDE.md so future hooks that
push map_bounds don't regress.
2026-04-18 13:25:38 -05:00
53cc2c9121
fix(map): refresh overlay on reconnect and tab visibility change
When the tab was hidden or the LiveView socket dropped, the server-side
bounds/scores assigns reset while the hook's scoreGrid retained its old
viewport data. Tiles Leaflet created or regenerated outside that stale
extent painted blank, leaving the overlay visibly truncated.
2026-04-18 13:23:39 -05:00
FluxCD
738973456f chore: update prop image to git.mcintire.me/graham/prop:main-1776534846-32ce43f [skip ci] 2026-04-18 17:57:41 +00:00
32ce43f04a
feat(map): show last-deployed stamp in map sidebar
Deploy stamp was in Layouts.app's nav but the map page uses a custom
full-screen layout that skips the standard nav, so the info wasn't
visible on the page most users land on. Add a compact
"Deployed 2h ago" under the sidebar's data-timestamp/pipeline-status
block with the full UTC time as a tooltip, matching the shape of
Layouts.deploy_stamp.
2026-04-18 12:53:37 -05:00
FluxCD
5a14756348 chore: update prop image to git.mcintire.me/graham/prop:main-1776534169-b5decef [skip ci] 2026-04-18 17:46:20 +00:00
b5decefa8e
docs(algo): align algo.md with Richardson-gated scoring implementation
Three small corrections now that the Richardson gate is actually
wired into Scorer.score_refractivity/5:

- Fix arity reference /4 → /5 in Part 2c.
- Reconcile the backtest section's "bulk_richardson is dead" finding
  with the new gate wiring — they're compatible claims. Richardson
  is dead as a standalone ML discriminator (stable and unstable
  inversions both produce ducts), but useful as a gate on the
  native-duct boost (a duct with high Ri is likely to mix out
  before the signal gets through).
- Note that nil Richardson is treated as "no info" so older native
  profiles without the column still receive the unconditional boost.
2026-04-18 12:42:20 -05:00
d3b0420457
feat(scoring): Richardson-gated native-duct boost
Added 5-arity Scorer.score_refractivity/5 that takes bulk_richardson
alongside best_duct_band_ghz. The existing 1.15× boost now only
applies when Richardson is in the stable regime (< 25) — native-
profile duct cells average 8.7-18.4 for ducting vs 38.3 for
non-ducting, so a duct-band reading under turbulent conditions is
more likely to be torn up by mechanical mixing than to carry a
beyond-LOS signal.

Wiring:
- Weather.nearest_native_duct_info/3 returns {ghz, richardson}; the
  bare nearest_native_duct_ghz/3 now delegates.
- PathLive and Propagation.score_with_algorithm/7 both pull the
  Richardson value into the conditions map alongside best_duct_band.
- Scorer.composite_score/2 reads conditions[:bulk_richardson] and
  passes it into score_refractivity/5.
- Backward compat: 4-arity variant and nil Richardson keep the old
  unconditional-boost behaviour so existing callers don't regress.
2026-04-18 12:40:57 -05:00
FluxCD
eff3382628 chore: update prop image to git.mcintire.me/graham/prop:main-1776533735-f40e88e [skip ci] 2026-04-18 17:39:13 +00:00
f40e88e305
docs: plan for GFS-driven extended-horizon forecasts (18 h → 240 h)
HRRR caps at f018, so beyond ~18 hours the map and path calculator
go blank. This plan scopes a GFS open-data ingest that runs
alongside HRRR to drive the 18-240 hour forecast window, with a
visible seam on the timeline where the resolution drops from 3 km
to 0.25°.

Scope: GFS 0.25° CONUS subset from AWS S3, four runs/day on the
standard cycle, chained fetch/score workers that mirror the HRRR
pipeline shape, per-source binary score files so HRRR and GFS stay
independent. ECMWF IFS HRES listed as a follow-up once GFS works.
2026-04-18 12:35:08 -05:00