Commit graph

86 commits

Author SHA1 Message Date
ba0f1161a7 Beacon improvements: anon submit, notes, /map nav, admin backfill
- Anonymous users can submit beacons (held pending admin approval)
- Added notes field to beacons (textarea on form, shown on detail page)
- Added Beacons link to /map sidebar nav (mobile + desktop),
  removed stale Rover Planner link
- Moved /backfill to /admin/backfill under the admin live_session
- Beacon detail map zooms in two extra levels after fitBounds
2026-04-08 16:23:59 -05:00
f5b5af171e Add beacon keying field (on/off or FSK) 2026-04-08 16:13:35 -05:00
6476bc8045 Dedupe ERA5 fetch jobs by (lat, lon, valid_time)
The Era5FetchWorker now declares an Oban unique constraint on
{lat, lon, valid_time} for any job in available/scheduled/executing/
retryable, so two backfill runs targeting the same contact grid point
can no longer spawn parallel CDS requests for the same hour.

Because Oban OSS insert_all doesn't honor unique, ERA5 jobs are now
routed through Oban.insert/1 from ContactWeatherEnqueueWorker and the
era5_backfill mix task. Other worker types still use insert_all.
2026-04-08 15:52:53 -05:00
80f2725cd5 Beacon submission approval workflow
Non-admin users can now submit beacons but they are held in an
unapproved state until an admin approves them. Only approved beacons
appear in the public list; pending submissions are shown in a separate
admin-only section on /beacons with Approve and Delete actions. The
show page surfaces a pending badge and an admin-only Approve button
when viewing an unapproved beacon.

Also formats EIRP (mW) on the index page without scientific notation.
2026-04-08 15:42:32 -05:00
cda54e3514 Per-HRRR-cell beacon reception plot
Replace the idealised concentric-circle range rings with a realistic
per-HRRR-grid-cell reception map. For every 0.125° grid point within
the band's exceptional range, the estimator computes great-circle
distance, FSPL + atmospheric loss, and applies a cell-specific score
adjustment of (50 - score) * 0.3 dB — score 100 gives a 15 dB ducting
boost, score 0 a 15 dB penalty. Cells below the -145 dBm detection
floor are dropped.

The beacon map hook now renders each surviving cell as a 0.125°
filled rectangle with a tooltip showing tier, Rx dBm, distance, and
HRRR score, so the coverage footprint bulges where ducting conditions
are good and cuts off where they aren't, instead of being a perfect
circle. Falls back to score 50 for cells that don't have HRRR data
yet, so the plot is always useful.

Also fixes the prior all-grey-tiles regression by restoring an
explicit setView() at map creation and adding a post-mount
invalidateSize() for when the map is placed inside a flex container.
2026-04-08 13:43:01 -05:00
a5b3f1f3da Beacon detail map with range estimate and on_the_air flag
- Add on_the_air boolean to beacons (default true); surfaced as a
  checkbox on the form, a badge on the index, and in the detail list.
- Render a Leaflet map on the beacon show page with a marker at the
  beacon's lat/lon tooltipped with callsign + frequency. Marker is
  green when on air, gray when off.
- Compute and draw a reception-range estimate as concentric signal-
  strength rings. New Microwaveprop.Beacons.RangeEstimate solves a
  link budget (FSPL + O2/H2O absorption from BandConfig) at five RX
  thresholds (-100 to -145 dBm), then scales by 0.5 + score/100 from
  the latest Propagation.point_detail at the beacon's grid square, so
  current HRRR conditions shift the rings in or out.
- Re-enable the hourly PropagationGridWorker cron and freshness
  monitor in dev.exs so dev actually has HRRR-backed scores to feed
  the new estimator.
2026-04-08 13:29:58 -05:00
374f9b106a Beacon height in feet, auto-fill lat/lon from grid
- Rename beacons.height_m to height_ft; migration converts existing
  values (m * 3.28084). Form/list/show labels updated.
- Beacon changeset now fills lat/lon from the grid when they're blank
  (previously only the reverse worked). Lat/lon and grid inputs are no
  longer marked required in the form — enter one side and the other
  populates on blur via phx-change.
2026-04-08 12:55:10 -05:00
b656e015bf Beacon power in mW, settings tweaks
- Rename beacons.power_watts to power_mw (migration multiplies existing
  values by 1000); form/list/show all relabeled to "Power (mW)"
- Fix Add-monitor button alignment on /users/settings by rendering the
  label above and putting the input and button in a plain flex row so
  the input wrapper margin no longer offsets the button
- Extend the settings page re-auth window from 10 minutes to 24 hours
2026-04-08 12:45:35 -05:00
ee9275e0b9 Add /beacons CRUD and /users admin page
Beacons:
- Scaffolded with phx.gen.live then reworked so reads are public
  and mutations go through a live_session gated by the new
  :require_admin on_mount hook in UserAuth
- Beacon schema stores frequency (MHz), callsign, grid, lat/lon,
  power (W), and height above ground (m); grid auto-derives from
  lat/lon when left blank via Maidenhead.from_latlon
- Adds Maidenhead.from_latlon/3 so we can compute grids locally
  instead of hitting an external API

Users admin page:
- /users and /users/:id/edit (admin-only) for listing, editing
  (callsign/name/email/is_admin), and deleting other users
- Adds Accounts.list_users, admin_update_user, delete_user, and
  a dedicated admin_changeset on the User schema
- Nav gains a "Users" link for admins and a "Beacons" link for
  everyone
2026-04-08 12:01:45 -05:00
eaef5b0178 Add is_admin flag, auto-grant to graham@mcintire.me
Adds a boolean is_admin column (default false) and has the
registration changeset auto-set it to true when the email matches
graham@mcintire.me (case-insensitive). The migration also backfills
the flag for an existing row with that email.
2026-04-08 11:43:52 -05:00
355bed178d Add beacon monitor registration and fix prod SMTP
Users can now register beacon monitors on the settings page. Each
monitor gets a unique random token the remote program will use to
authenticate its reports. Name is the only user-supplied field for
now; more will be added as the monitor protocol is defined.

Also adds :gen_smtp to deps. Swoosh's SMTP adapter depends on
:mimemail which lives in that package, and production was 500'ing
when trying to send confirmation emails without it.
2026-04-08 11:30:28 -05:00
27170b5139 Merge auth links into main nav and lower password min to 8
- Register/Log in (or callsign/Settings/Log out) now live next to
  the Map/Path/Rover links in the main header instead of a separate
  top menu bar
- Add on_mount in UserAuth to assign current_scope on LiveView mount,
  and pass current_scope through to Layouts.app from every LiveView
- Drop the old top <ul> from root.html.heex
- Password minimum lowered from 12 to 8 characters
2026-04-08 10:39:51 -05:00
1d86e287b2 Add password auth with callsign + email confirmation
Generated Accounts context, User schema, and controllers via
phx.gen.auth. Adapted it for password-only login with required
email confirmation:

- Users have callsign (unique, uppercased), name, email, password
- Registration form fields: callsign, name, email, password, confirm
- Magic-link login path removed; login is email + password only
- After register, a confirmation email is sent and login is blocked
  until the account is confirmed via the token URL
- Confirmation link logs the user in on first use
- SMTP2GO configured as the outgoing mailer in k8s prod
2026-04-08 10:21:40 -05:00
75fd05a6d1 Simplify rover planner: remove grid toggle and coverage calculation 2026-04-08 09:06:09 -05:00
cfaef287e4 Add 902-5760 MHz bands, centralize band options
New BandConfig entries for 902, 1296, 2304, 3456, 5760 MHz:
- All beneficial humidity effect (like 10 GHz)
- Near-zero gaseous absorption and rain attenuation
- Ranges: 902 MHz typical 400 km, 5760 MHz typical 220 km
- Same seasonal curves as 10 GHz (ducting-driven)

BandConfig.band_options/0 generates dropdown options from configs.
All pages (path, rover, submit) use centralized band_options instead
of hardcoded lists. Map page already used BandConfig.all_bands().
10 GHz remains the default on all pages.
2026-04-07 16:39:25 -05:00
6aec3eeea4 Extract Coverage module, fix computation, add tests
- Extract scoring logic to Microwaveprop.Rover.Coverage for testability
- Two-phase computation: fast pass (distance + propagation) for all grids,
  then SRTM terrain analysis for top 20 candidates only
- Cap search radius to 300 km to keep candidate count reasonable
- Run terrain analysis in Task with rescue/fallback for resilience
- Background Task doesn't block LiveView process
- 5 tests covering: ranked results, empty inputs, station details,
  band range differences, field presence
2026-04-07 16:08:03 -05:00
3cdd132f24 Data-driven algorithm refinements from full dataset analysis
Analyzed 37,925 HRRR-matched contacts from 58,367 total.

- Remove shallow BL bonus (score 82 for HPBL < 300m): data shows
  medium BL (1000-2000m) produces longest contacts (222 km avg),
  not shallow (210 km). Refractivity fallback now uses default score.

- Refine pressure scoring bins: add <980 mb tier (score 88), steeper
  gradient from low to high. Contacts at <970 mb avg 242.7 km vs
  184.1 km at 990-1000 mb.

- Update algo.md calibration stats (58,367 contacts, 41M HRRR
  profiles, 58,361 terrain profiles) and document dataset bias
  (99.5% Aug-Sep).

- Create updates.md with full binned analysis tables for all factors.
2026-04-07 11:15:49 -05:00
646467cf4d Store stub records on empty weather fetches to prevent infinite backfill retries
When ASOS/RAOB/IEMRE APIs return empty data, store a stub record so
dedup checks (has_surface_observations?, has_sounding?, has_iemre_observation?)
see coverage and don't recreate the same jobs on future backfill runs.
2026-04-06 16:10:39 -05:00
e99e585b66 Add type filter to backfill enqueue
Backfill dashboard now has checkboxes to select which enrichment types
to process (HRRR, Weather, Terrain, IEMRE). Prevents weather jobs from
flooding the queue when only HRRR needs work.
2026-04-06 11:42:26 -05:00
c98aabfc15 Add BackfillEnqueueWorker tests (0→100% coverage) 2026-04-06 10:15:38 -05:00
62c9600f67 Add Tier 2 test coverage: SolarIndexWorker, HrrrFetchWorker, IemClient
SolarIndexWorker 36→100% (date-arg clause), HrrrFetchWorker 38→42%
(batch skip, backoff), IemClient 65→74% (fetch_current_asos).
2026-04-06 10:15:38 -05:00
7e52c6660d Add Tier 1 test coverage: EnrichmentStatus, Markdown, health, RemoteIp, Maidenhead
EnrichmentStatus 0→100%, HealthController 0→100%, Maidenhead 74→96%,
RemoteIp 56→94%, Markdown 0→92%. Total coverage 53→55%.
2026-04-06 10:15:38 -05:00
16883591b4
Database performance fixes and async backfill enqueue
- Fix score_pressure crash on nil pressure_mb (coastal HRRR points)
- Set 10-min timeout on grid score upsert transaction (was :infinity)
- Single DELETE for prune_old_scores instead of N queries in a loop
- Remove dead load_hrrr_refractivity that loaded 95k rows into nil map
- Pass selected_time to point_detail to skip latest_valid_time sub-query
- Batch station existence checks (1 query per path point, not per station)
- Batch solar index upserts via insert_all in chunks of 500
- Batch backfill_distances via single UPDATE FROM VALUES statement
- Add is_grid_point boolean + partial index to hrrr_profiles (replaces
  non-sargable modular arithmetic filter on every weather map query)
- Add partial index on contacts(qso_timestamp) WHERE pos1 IS NOT NULL
- Move backfill enqueue to Oban worker so UI returns immediately
2026-04-04 19:19:18 -05:00
dc66252fdc
Reduce dead tuples by using conditional upserts and skip-on-conflict
- HRRR, IEMRE, terrain: on_conflict: :nothing (immutable data)
- Surface obs, soundings, solar: conditional WHERE (only update when values differ)
- set_enrichment_status!: skip rows already at target status
2026-04-04 17:32:57 -05:00
9cbf8b91f4
Derive upper-air weather layers from HRRR profiles in weather grid queries 2026-04-03 16:52:01 -05:00
489a886b0e
Add WeatherLayers module for deriving upper-air map fields from HRRR profiles 2026-04-03 16:49:17 -05:00
0dd695c574
Add weather grid query functions for /weather map page 2026-04-03 15:08:21 -05:00
bdc4d08111
Rename remaining qsos variable names to contacts in tests 2026-04-02 15:56:31 -05:00
da75977a55
Rename qsos table to contacts, qso_id to contact_id
- Migration renames table, column, all indexes, and FK constraints
- Contact schema now references "contacts" table
- TerrainProfile foreign key renamed from qso_id to contact_id
- All code and tests updated to use contact_id
2026-04-02 15:52:19 -05:00
d275e9e7c4
fix enrichment 2026-04-02 15:30:41 -05:00
79af4e4959
Ensure positions computed from grids before any enrichment
- Radio.ensure_positions!/1 computes pos1/pos2/distance from grids
- Called in contact show page mount and enqueue_for_contact
- Migration backfills positions for existing contacts missing them
- Migration corrects enrichment statuses based on actual data presence
- Backfill dashboard includes contacts with grids but no positions
2026-04-02 12:28:48 -05:00
93f2c32971
Remove timex, earmark, dns_cluster deps; add local implementations
- Replace Timex with regex-based timestamp parser (broader format support)
- Replace Earmark with local Markdown.to_html! (headings, code blocks,
  tables, lists, inline formatting, links, horizontal rules)
- Remove dns_cluster (unused, single-instance deployment)
- Add stream_data property tests for timestamp parsing (5 properties
  covering ISO, US dates, AM/PM, format roundtrips, garbage rejection)
- Removes transitive deps: combine, tzdata, hackney, certifi, metrics,
  mimerl, parse_trans, ssl_verify_fun
2026-04-02 12:17:37 -05:00
55a51f69ab
Replace boolean enrichment flags with enum status fields
- New fields: hrrr_status, weather_status, terrain_status, iemre_status
  with values: pending, queued, processing, complete, failed, unavailable
- EnrichmentStatus module defines valid state transitions
- Migration backfills: queued=true → complete, false → pending
- Workers set status on transitions (queued on enqueue, complete on finish)
- Show page reads status from contact struct, not computed dynamically
- Backfill dashboard queries status fields for accurate progress counts
- Remove cron-scheduled ContactWeatherEnqueueWorker (enrichment only on
  submission, page view, or manual backfill)
- Partial indexes on status != 'complete' for fast unprocessed lookups
2026-04-02 12:07:51 -05:00
fd00de7bc6
Never download SRTM tiles during page load, only in Oban workers
- Srtm.lookup no longer auto-downloads by default, requires download: true
- TerrainProfileWorker passes download: true for background tile fetch
- Contact detail page enqueues TerrainProfileWorker if profile missing
- Page load gracefully handles missing tiles via API fallback or nil
2026-04-02 08:07:26 -05:00
ec388e4bf5
Use Timex for flexible CSV timestamp parsing with tests
- Add timex ~> 3.7, downgrade gettext to ~> 0.26 for compatibility
- Parse ISO 8601, US dates (M/D/YYYY), AM/PM, 24h, compact formats
- 22 tests covering all accepted timestamp formats and edge cases
- Integration tests for CSV import with US date and space-separated formats
2026-04-01 15:39:02 -05:00
18a291555c
Add CSV bulk upload to /submit page
Adds a tabbed UI with single-contact form and CSV upload. Users can
download a sample CSV, upload their file, and get partial import with
per-row error reporting. Enrichment jobs enqueue for each imported contact.
2026-04-01 15:05:51 -05:00
254e64dedc
Rename qsos to contacts throughout codebase, keep DB table name
Rename all modules, functions, variables, routes, and UI text from
qso/qsos to contact/contacts. Database table stays as "qsos" to avoid
migration. Add /qsos -> /contacts redirects for old URLs.
2026-04-01 11:25:04 -05:00
bc529af392
Update algo.md Finding 10: corrected mode advantage, no SSB on rainscatter 2026-04-01 11:10:11 -05:00
b75516ff9f
Partition hrrr_profiles by valid_time for query performance
54M row table partitioned into date-range partitions. Queries with
valid_time filter only scan relevant partitions. Prune now uses
partition-aware DELETE. Fix tests for partitioned constraint behavior
and removed sort-click tests (grouped table uses URL params).
2026-04-01 11:03:33 -05:00
02cb4fd67b
Integrate ML model into grid worker, QSO search, and UI improvements
ML Integration:
- Load trained model at app startup, cache compiled predict fn in persistent_term
- Grid worker uses batched ML prediction (10K chunks) when model loaded,
  falls back to algorithm scorer when not
- ML score replaces composite, algorithm factor scores preserved for detail view
- Fix process explosion: single EXLA call per chunk instead of per-grid-point

QSO Features:
- Callsign search (ILIKE on station1/station2) with trigram indexes
- Reciprocal QSO grouping (same pair, same band, same hour)
- Wider layout (max-w-7xl) for data table pages
- QSO Training Data link on map page

Infrastructure:
- Re-enable hourly propagation grid worker in dev
- Track ML model weights in git for Docker builds
- Add btree indexes on qsos (timestamp, band, distance_km)
- Remove nav icons from layout header
2026-04-01 10:14:22 -05:00
9537c97d1d
20-feature model with solar indices, sounding stability, and ducting
Add SFI, Kp max (solar), K-index, lifted index (sounding stability),
and ducting_detected (HRRR) as model features. Training now joins to
solar_indices and nearest sounding (within 6 hours) for both phases.
Model can learn solar/geomagnetic effects if they exist in the data.
2026-04-01 09:31:54 -05:00
07558d17eb
Two-phase training: pretrain on algorithm scores, fine-tune on QSOs
- 15 features: add surface_refractivity and latitude
- Bigger network: 128→64→32 (3 hidden layers)
- Phase 1: pretrain on 500K stratified algorithm scores (all seasons/locations)
- Phase 2: fine-tune on 57K real QSO-HRRR matched data (percentile target)
- Lower LR (0.0003) for fine-tuning to preserve pretrained knowledge
- Model.train accepts :initial_state option for transfer learning
2026-04-01 09:27:27 -05:00
08e4b9abdd
Bigger network (128→64→32) and percentile-based training target
- 3 hidden layers instead of 2 for better feature interaction learning
- Target is within-band distance percentile (0-1) instead of raw
  normalized distance — reduces noise from operator/equipment variation
2026-04-01 09:17:36 -05:00
69b5caf876
Normalize ML features to prevent NaN gradient explosion
Raw features had vastly different scales (pressure ~1013, sin/cos ~[-1,1])
causing gradient explosion. Normalize all atmospheric features to ~[0,1]
using known physical bounds. Add Polaris dep for optimizer.
2026-04-01 09:08:33 -05:00
c12f8cf5ed
Use local solar time for time-of-day scoring, add PWAT factor and pressure refinements
Score time-of-day per grid point using longitude/15 solar offset instead of
hardcoded CST/CDT. Add PWAT as 10th scoring factor. Refine pressure thresholds.
Update ML model and training pipeline to use local solar time.
2026-04-01 08:58:21 -05:00
8949920b7f
Add Nx/Axon/EXLA ML model skeleton for propagation prediction
13-feature feed-forward network (atmospheric + temporal + frequency).
Includes build, init, predict, encode_features, save/load to disk.
Model weights saved to priv/models/propagation_v1.nx (gitignored).
Not yet trained — scaffolding only.
2026-03-31 16:26:34 -05:00
c9112b9280
Recalibrate refractivity thresholds for HRRR gradient distribution
Previous thresholds (-500 to -60) were calibrated for radiosonde data.
HRRR profiles have coarser vertical resolution, with gradients clustering
between -40 and -130 N/km (median -70). Nearly all grid points were
falling through to the default score of 42, wasting the refractivity
factor. New thresholds (-200 to -40) spread across HRRR percentiles.
2026-03-31 16:08:15 -05:00
49cbe6789c
Implement ITU-R P.526-16 terrain diffraction model
- Replace piecewise knife-edge loss with P.526-16 Eq. 31 single formula
- Fix diffraction parameter ν to use standard formula instead of ad-hoc approximation
- Implement Deygout 3-edge method for multiple obstacle diffraction
- Add dynamic k-factor from HRRR refractivity gradient (falls back to 4/3)
- Terrain worker now looks up nearest HRRR profile for atmospheric correction
- Update algo.md with P.526-16 methods and k-factor table
- Fix pre-existing map_live_test antenna height default (33 ft, not 8)
2026-03-31 16:04:23 -05:00
5eaa55448e
Add terrain-aware viewshed on map click
Replace generic range circles with actual LOS coverage polygon computed
from SRTM elevation data. Casts 180 rays (every 2 degrees) from the
clicked point, runs Fresnel/diffraction analysis on each, and renders
the reachable area as a Leaflet polygon.

- Viewshed module with haversine forward, terrain sweep, async compute
- Antenna height control (default 8 ft) in map panel
- LiveView start_async/handle_async for non-blocking computation
- Remove signal icon from band selector
2026-03-31 13:06:01 -05:00
40d1fa03aa
QSO submission triggers enrichment directly, fix prod badarith crash
- Add enqueue_for_qso/1 to directly enqueue weather/HRRR/terrain/IEMRE
  jobs for a single user-submitted QSO (no cron, no bulk processing)
- Submit flow calls enqueue_for_qso instead of generic enqueue worker
- Add enrichment queues to prod config for on-demand processing
- Guard against HRRR fill values in store_hrrr_profiles (fixes badarith)
- Filter QSOs without pos2 in build_terrain_jobs
2026-03-31 12:29:39 -05:00