For a 1 W / 432 MHz beacon, RangeEstimate.estimate/1 was returning
~126 k cells because free-space loss dominates over atmospheric
loss at low UHF, letting the rx_dbm filter pass almost the whole
bbox. Jason-encoding that payload into the map's data-cells
attribute was locking up browsers on the beacon detail page for
low-band beacons even though the coverage toggle is already
disabled for them. Return an empty estimate below the same 5760
MHz threshold the UI already enforces.
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.
- Add belongs_to :user on Beacon schema, preload in get_beacon!/1
- Show "Submitted by {callsign}" on beacon detail page
- Add NTMS donation link on the about page
- Change grid preference text to 8 characters (e.g. EM12kp37)
- Round grid-derived lat/lon in maybe_fill_latlon changeset step
- Format lat/lon to 6 decimal places on index table
- Migrate existing beacon data to 6 decimal precision
- Add comma separators to EIRP mW display (e.g. 10,000 mW)
- Extract shared add_commas helper for format_freq and format_mw
Height in feet doesn't need decimal precision. Migrates the DB
column, updates schema type, and strips trailing .0 from form
input so the integer cast succeeds.
- Add CommaNumber JS hook for live comma formatting while typing
frequency MHz in the beacon form
- Strip commas server-side before changeset validation
- Order beacon list by most recently added (desc inserted_at)
- Round lat/lon to 6 decimal places in changeset and display
- Round height_ft to integer in changeset and display
- Display coords at 6 decimal places on show page
- Beacon map defaults to just the marker at zoom 11. A daisyUI toggle
switch shows/hides the estimated-coverage cell layer; the coverage
info line and tier legend are hidden unless the toggle is on.
- Beacon detail list replaced with a compact dl grid (2-4 columns).
Lat/lon rounded to 5 decimals, power/height/beamwidth formatted via
Beacon.format_mw/1 so no more "2.5e3" scientific notation or "280.0".
format_mw/1 hoisted onto the schema so index and show share it.
- About page no longer uses prose classes (this project doesn't load
@tailwindcss/typography). Headings, lists, and paragraphs now use
explicit utility classes so they render as intended.
Beacons can now record a directional bearing ("omni" or 0-360 degrees)
and a beamwidth in degrees. The keying list grows from {on_off, fsk}
to include FM Voice, WSPR, and the full Q65A/B/C/D/E × 15/30/60/120
family, grouped via optgroups in the submission form.
- 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
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.
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.
The power_mw field represents the beacon's effective radiated power,
not just transmitter output. Relabel it as "TX power (EIRP)" on the
form and detail list, "EIRP (mW)" in the index, and drop the now-
meaningless tx_gain_dbi constant in RangeEstimate since the stored
value already includes antenna gain.
- 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.
- 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.
- 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
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