Commit graph

1104 commits

Author SHA1 Message Date
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
FluxCD
f4edd60c73 chore: update prop image to git.mcintire.me/graham/prop:main-1776533491-0bf49f4 [skip ci] 2026-04-18 17:35:07 +00:00
0bf49f47ec
docs+ui: present-tense algo.md, NARR replaces ERA5, wider /algo page
algo.md rewrite:
- Strip retrospective language (was/previously/↑ from/Reverted/prior)
  so the doc describes the current algorithm instead of its history.
  Parts 2b/2c/2d lose their dated headings.
- Replace ERA5 references with NARR — ERA5 was never populated in
  prod; NARR is the real historical-backfill path (1979–2014 via
  NCEP / NCEI).
- Fold the sounding ducting-probability table, HPBL binned distance,
  and per-band signal summary into the current-state discussion
  instead of "changes since last refresh" tables.
- Note the bulk_richardson gating on the native-duct boost (code
  follow-up in a separate commit).

Algo page UI: widen .markdown-content from 64rem → 88rem so the
per-band weight + correlation matrices stop overflowing on typical
screens. Wide tables inside markdown content now get their own
horizontal scroll (display: block + overflow-x: auto) rather than
forcing the whole container to scroll.
2026-04-18 12:31:07 -05:00
FluxCD
b261fea460 chore: update prop image to git.mcintire.me/graham/prop:main-1776533024-06feda7 [skip ci] 2026-04-18 17:27:03 +00:00
06feda7110
perf: finish audit follow-ups (path conditions, recalibrator parallelism)
Path calculator conditions map was missing :latitude and
:best_duct_band_ghz. Scorer read them with bracket-notation so the
behaviour was defensively correct but we were leaving signal on the
floor:

- latitude: midpoint of src/dst so `score_season` picks up the right
  latitude-based seasonal shift for northern vs southern paths
- best_duct_band_ghz: queried via new Weather.nearest_native_duct_ghz/3
  at the midpoint so score_refractivity/4 applies the 1.15× boost
  when a native-resolution duct supports the target band

Recalibrator.fit/1 was serialising ~10k per-contact HRRR lookups
(5000 positives + 5000 negatives, one Weather.find_nearest_hrrr each).
Parallelised both sides with Task.async_stream at 20 concurrency —
well under the prod db pool of 30. Cuts a band recalibration from
~minutes of query round-trips to seconds.
2026-04-18 12:23:18 -05:00
FluxCD
1deab8b83d chore: update prop image to git.mcintire.me/graham/prop:main-1776532149-2874e6a [skip ci] 2026-04-18 17:12:59 +00:00
2874e6a93b
perf+fix: parallelize path HRRR queries; fix meteor-shower year boundary
Audit pass found two real issues (most agent-flagged items were false
positives — the hrrr_native_profiles composite index exists,
pwat_mm/bl_depth_m are already in the path conditions map, and the
/1.0 in MechanismClassifier is the standard Elixir int→float coerce).

1. Path calculator's 9-point HRRR sampling was running sequentially,
   ~9× the latency of a single query. Task.async_stream with
   max_concurrency: 9 makes them concurrent so path calc pays roughly
   one query's worth of time instead of nine.

2. MechanismClassifyWorker's meteor-shower window used hardcoded 2024
   peak dates with `%{date | year: peak.year}` projection. That broke
   across year boundaries — a Dec 30 contact couldn't match a Jan 4
   Quadrantids peak even in-range of the ±3 day window. Switched to
   {month, day} tuples and check the peak projected onto year N-1, N,
   N+1 so the window works correctly near Dec 31 / Jan 1.
2026-04-18 12:08:42 -05:00
FluxCD
181ad19c24 chore: update prop image to git.mcintire.me/graham/prop:main-1776528915-379b8ee [skip ci] 2026-04-18 16:22:30 +00:00
379b8ee357
fix(docker): use BuildKit cache mounts for apt so .debs skip the layer
The last attempt still blew past the CI runner's /var cap because
apt's pre-download free-space check runs against the archives dir,
and the image overlay is where that sits. Moving /var/cache/apt and
/var/lib/apt onto BuildKit cache mounts puts the .deb storage on the
builder's cache volume (outside the image overlay), so the free-space
check runs against whatever disk backs the cache — which on the CI
runner has room.

Requires the `syntax=docker/dockerfile:1.6` directive at the top for
the --mount flag. Also removes /etc/apt/apt.conf.d/docker-clean so
the Debian base's post-invoke hook doesn't wipe the cache mount on
every apt run.
2026-04-18 11:07:45 -05:00
6e72c6811d
fix(docker): keep apt cache from filling /var disk during build
The cdo install pulls ~1 GB of transitive deps (netCDF/HDF5/ecCodes/
Fortran) and was exhausting CI's /var/cache/apt partition. Two fixes:

- `APT::Keep-Downloaded-Packages=false` deletes each .deb immediately
  after it's extracted, so the archive cache doesn't retain every
  downloaded package through the end of the install.

- Split cdo into its own RUN layer so the base runtime deps finish
  installing and clean before cdo's cache starts growing.
2026-04-18 11:00:06 -05:00
5dea91d43f
feat(beacons): plot-path link seeds TX chain to match beacon EIRP
The "Plot path to beacon" action now includes `tx_power_dbm` and
`src_gain_dbi` in the /path query string so the path calculator's TX
side emits the same EIRP as the beacon out of the box. The beacon
schema stores `power_mw` as EIRP already (per Beacons.RangeEstimate),
so we take 10·log10(power_mw), split against a default 30 dBi dish,
and clamp the TX power to a 0 dBm floor so very-low-EIRP beacons
produce a sane gain/power split instead of -10 dBm outputs.

Examples:
  10 W EIRP (40 dBm)  → 30 dBi dish + 10 dBm TX
  100 W EIRP (50 dBm) → 30 dBi dish + 20 dBm TX
  100 mW EIRP (20 dBm) → 20 dBi + 0 dBm (clamped)
2026-04-18 10:54:38 -05:00
fbec454917
feat(path): 9-point HRRR sampling + nearest sounding + km/mi display
Path calculator now samples nine HRRR grid cells evenly along the
source→destination line (was three: Source/Midpoint/Destination) so
mid-path ducts that endpoint-only queries missed show up in the duct
count. The UI reports "Duct at N / 9 HRRR points" instead of a binary
flag, and the table lists every sample with its percent-of-path label.

Independent RAOB signal: uses the new `Weather.nearest_sounding_to/3`
to find the closest sounding within 300 km / ±3 h of the midpoint. If
the sounding's `ducting_detected` is true we surface a "RAOB <code>
confirms duct" badge — HRRR under-reads thin surface ducts that
soundings resolve, so the two signals together are the right
cross-check for live duct prediction.

Distance formatting: all km values on the path results (top Distance
summary, sounding "km from midpoint") now render as
`123 km (76 mi)`; under 10 km we keep one decimal so near-LOS paths
don't round to zero.
2026-04-18 10:50:24 -05:00
18d16b0ad6
feat(weather): nearest_sounding_to helper for path calculator
Spatial + temporal nearest-RAOB lookup (defaults to 300 km / ±3 h)
joining weather_stations → soundings. Returns the full sounding row
so callers can read the derived ducting_detected / min_refractivity_
gradient fields that HRRR's pressure-level product systematically
under-reads for thin surface ducts.

Prep for the path-calculator enrichment: a sounding within range at
the path midpoint gives an independent duct signal to cross-check
the nine HRRR samples already taken along the path.
2026-04-18 10:46:35 -05:00
622edee180
feat(propagation): per-contact mechanism classification
Classifies every contact's likely non-LOS propagation mechanism and
persists the result on contacts.propagation_mechanism. Mechanism is
determined in priority order:

  1. user_declared_prop_mode (ADIF PROP_MODE from the operator log)
  2. EME — moon-ephemeris check, ≥2m band, >1800 km path
  3. aurora — Kp≥5 + high-lat path, 50-432 MHz
  4. sporadic-E — foEs × 5 ≥ band_mhz, 400-2500 km path
  5. meteor_scatter — ±3 days of a shower peak, VHF/UHF
  6. rain_scatter — common-volume radar heavy rain, 5-11 GHz ≤800 km
  7. tropo_duct — HRRR native_best_duct ≥ band or ducting_detected
  8. line_of_sight — ≤50 km path
  9. troposcatter — default

Persisted via MechanismClassifyWorker (queue: :mechanism, unique on
contact_id). Submit-time enqueue path includes :mechanism by default;
BackfillEnqueueWorker cron now handles :mechanism alongside existing
types so prod continuously backfills any contact with
propagation_mechanism_status in (:pending, :queued, :failed). Also
added :radar to the cron's type list so common-volume radar backfill
runs automatically rather than only via `mix radar_backfill`.

New modules:
- Microwaveprop.Propagation.MoonEphemeris — Meeus low-precision moon
  position, accuracy ±1° — enough for the mutual-visibility EME test
- Microwaveprop.Propagation.MechanismClassifier — plug-in priority
  chain over the evidence map
- Microwaveprop.Workers.MechanismClassifyWorker — assembles inputs
  from HRRR / native profiles / common-volume radar / solar_indices /
  ionosonde + calls the classifier

ADIF importer now reads PROP_MODE into user_declared_prop_mode so
operator-tagged mechanisms (EME/ES/MS/RS/AS/AUR) become ground truth.
2026-04-18 10:42:08 -05:00
FluxCD
5cffbec165 chore: update prop image to git.mcintire.me/graham/prop:main-1776525807-f9af347 [skip ci] 2026-04-18 15:27:07 +00:00
f9af347485
chore(logs): skip HEAD requests in endpoint telemetry log 2026-04-18 10:22:48 -05:00
5fd37a17fd
feat(propagation): per-band weight calibration from full-corpus correlation
Ran recalibrate_algo.py against the full local prop_dev (81,994 contacts,
18.6M HRRR rows) and derived per-band composite weights for the nine
bands with >=200 matched contacts. Moisture (dewpoint/PWAT/surface N)
is consistently beneficial through 5.76 GHz, reverses at 24 GHz; rain
scales sqrt(rain_k) not linearly so 24 GHz gets 0.215 rain weight
instead of the linear-ratio 0.95. 10 GHz stays as the reference band
(defaults); 47+ GHz inherits defaults (n<200).

- BandConfig.weights/1 returns per-band override or default fallback
- @band_configs carries :weights on 222/432/902/1296/2304/3400/5760/24G
- Recalibrator.compute_factors/3 + fit(band_mhz:) band-aware fitting
- Scorer + ContactLive.Show pass band_config to weights/1
- algo.md Part 2d documents the 2026-04-18 analysis + derivation rule
- scripts/derive_band_weights.py turns correlations into weight maps
- report preserved at docs/algo-reports/2026-04-18-recalibration.md
2026-04-18 10:19:26 -05:00
FluxCD
41c9a5522f chore: update prop image to git.mcintire.me/graham/prop:main-1776523046-fc91f20 [skip ci] 2026-04-18 14:44:32 +00:00
fc91f204fd
feat(propagation): record per-forecast-hour chain step timings
Add a propagation_run_timings table so the wall-clock duration of each
(run_time, forecast_hour) step is queryable long after the run is over.
Keyed by (run_time, forecast_hour) with a status column that captures
whether the step succeeded or bailed out, and an error string on
failure.

PropagationGridWorker stamps every step (ok and failed) via
Propagation.record_run_timing/1. Timing inserts are wrapped in rescue +
changeset-error handling so the instrumentation can never brick the
chain.
2026-04-18 09:36:52 -05:00
154cb967a1
chore(scripts): source restore script config from .envrc, pin pg tools
Swap the SSH+scp-from-backup approach for a direct pg_dump against the
prod Postgres using PROP_PROD_DB_URL already in .envrc. Pin the
pg_dump/pg_restore/psql binaries to the @18 series so they match the
prod server version, and stream per-object progress via --verbose on
both tools.
2026-04-18 09:36:52 -05:00
fa0f2828a6
chore(db): rename dev database to prop_dev, add prod restore script
Rename the local dev database from microwaveprop_dev to prop_dev so
the name matches the production database. Add scripts/restore_prod_to_local.sh,
which pulls the latest pg_dump off the backup server, drops the local
prop_dev, recreates it, and pg_restores into it.
2026-04-18 09:36:52 -05:00
FluxCD
b675a4badc chore: update prop image to git.mcintire.me/graham/prop:main-1776519792-00aad3c [skip ci] 2026-04-18 13:46:47 +00:00
00aad3cb98
feat(auth): self-service password reset
Add a "Forgot your password?" flow off the login page. A 24-hour
reset_password token is emailed on request, the landing page lets the
user pick a new password, and all other tokens for the user are
revoked on success.

The request endpoint returns the same flash regardless of whether the
email matches a user so that attackers can't enumerate accounts.
2026-04-18 08:42:35 -05:00