Commit graph

99 commits

Author SHA1 Message Date
118ad4e8cc
WeatherMap: actually move the legend to top-right
The previous fix only updated the initial mount path. buildLegend gets
re-created on every layer switch via the swap at renderLayer, and that
code path still used the old isMobile-dependent bottomright position,
which clobbered the top-right placement as soon as the user clicked a
different layer.
2026-04-15 14:12:29 -05:00
33347f7fc2
WeatherMap: legend in top-right, description back in sidebar, play/stop, fix reload loop
Corrections to the previous weather-map pass:

* The "helper" that was supposed to live in the top-right was the
  color legend, not the layer description. Move the Leaflet legend
  control to `topright` (was `bottomright` on desktop).

* Put the layer description back in the desktop sidebar where it
  used to live, now with a "Group · Label" header above the body
  text so it matches the new style in the mockup.

* Add play/stop controls to the weather forecast timeline, ported
  from the propagation map: start plays "now" → end at 1s/step and
  loops; stop returns to the time closest to wall clock. A manual
  click interrupts playback the same way it does on /map.

* Fix the reconnect/reload loop I introduced by making mount do a
  synchronous ProfilesFile read + derive for the full 92k-point
  grid. That was pushing mount past the LV socket join timeout on
  a cold pod, which makes the LV client fall back to a full HTTP
  reload every ~20 seconds (no server-side error). Mount now uses
  the original `latest_weather_grid/1` cache-only hot path and only
  `weather_grid_at/2` (sync disk read) on user timeline scrubs.

* Clean up the playback timer in the hook's `destroyed` callback.
2026-04-15 14:11:53 -05:00
725a7e6bda
WeatherMap: forecast-hour timeline + top-right layer description
Two related UI changes driven by the same data:

* Move the selected-layer description out of the sidebar and into a
  dedicated top-right overlay on the map. The sidebar kept the same
  layer buttons but dropped the descriptive text; the new overlay
  shows group + layer name + description in one card. Mobile still
  shows the description in the collapsible panel since it has no
  top-right free real estate.

* Add a forecast-hour timeline bar at the bottom of the map, matching
  the propagation map. WeatherMapLive enumerates ProfilesFile on mount
  (the grid worker has been persisting f00..f18 on disk since
  commit 07ffcf5), pushes data-valid-times for the JS hook to render
  as buttons, and handles a new select_time event by reading the
  per-hour ProfilesFile on demand via Weather.weather_grid_at/2.

weather_grid_at/2 deliberately skips the GridCache write path for
non-latest hours — caching 18 × 92k rows would add ~300 MB per pod.
A ~2 MB ETF decode per scrub is fast enough for a click.

Subscribes to propagation:pipeline so the timeline picks up new
forecast hours live as they land, without waiting for the full chain
to finish.
2026-04-15 13:57:23 -05:00
a9fa1d8344
Pin propagation legend to top-right on desktop too
The forecast timeline at the bottom of the map now spans a lot of
hours horizontally, and on wide screens it ran into the legend
that used to anchor bottom-right — covering the later forecast
hours. Move the legend to top-right on every viewport (it was
already there on mobile for the same reason).
2026-04-15 10:06:37 -05:00
1a9df52102
Add play/stop controls to the propagation forecast timeline
New play and stop buttons live directly under the "Propagation
Forecast" label in the bottom timeline bar. Play starts iterating
one forecast hour per second from "Now" forward through every
loadable future hour, then loops back to "Now" and repeats. Stop
clears the interval and returns the map to "Now". A manual click
on any time button cancels playback so the user's selection isn't
overwritten by the next tick.

The existing click handler body moved into a shared
selectTimelineTime() so playback and manual clicks go through the
same cache-or-roundtrip path. A server-driven update_timeline event
(band change or new forecast hour) also tears the timer down before
overwriting timelineData underneath it.
2026-04-15 10:03:14 -05:00
92ef6c67fc
Defer beacon coverage estimate until the toggle is flipped on
Previously RangeEstimate.estimate/1 ran on every beacon page mount,
even though the cells data was only rendered after the user flipped
the coverage toggle — a wasted bbox grid pass per page load. Start
with estimate: nil, compute lazily in handle_event("toggle_coverage")
when flipping on, and push the cells payload to the Leaflet hook via
a new load_coverage event. The JS hook no longer reads data-cells on
mount and only builds the cellLayer when the server sends it. Cached
in the socket so re-toggling is instant.
2026-04-14 17:13:05 -05:00
2b323189d0
Add grid squares and weather radar toggles to the weather map
Ports the same two overlay toggles from /map to /weather:
- Maidenhead grid squares via the shared updateGridOverlay helper,
  redrawn on moveend when visible.
- ECCC GeoMet WMS radar (CONUS + Canada dBZ), lazy-created on first
  toggle with a 6-minute tile refresh timer.

Both toggles live in the desktop sidebar below the layer description
and in the mobile pulldown panel. Hook cleanup clears the radar
refresh timer on destroyed().
2026-04-13 16:31:54 -05:00
87119c33e1
Refresh viewshed on band change when a point is selected 2026-04-13 09:41:58 -05:00
a8d4d70abe
live_table polish: border color, control layout, row clicks, pagination
- Tailwind source(none) wasn't scanning deps, so shadcn tokens
  (border-border, bg-muted, text-foreground, ...) used by live_table
  and sutra_ui weren't generated, leaving `border` to fall back to
  currentcolor — a heavy dark border around every table in light mode.
  Add @source directives for deps/live_table/lib and deps/sutra_ui/lib.
- Drop the hardcoded width:100% on the outer .select wrapper so the
  per-page selector's Tailwind w-24 wins instead of stretching across
  the header and overlapping the search box.
- Make whole rows clickable: global click delegator in app.ts forwards
  tr clicks to the first <a href> in the row (the Actions "View" link),
  skipping inner interactive elements. cursor + hover highlight added.
- Replace the default Prev/Next footer with a daisyUI .join/.btn-based
  pager (MicrowavepropWeb.LiveTableFooter), wired up globally via
  :live_table defaults so it applies to every live_table at once.
2026-04-13 08:38:24 -05:00
810f5a22ce
Beacon list map, profile page polish, nav ordering fix
/beacons gains a Leaflet map above the table showing every approved
beacon as a circle marker with a popup that links to the detail page.
New BeaconsListMap JS hook (assets/js/beacons_list_map_hook.ts) walks a
data-beacons JSON payload, fitBounds over the marker set, and mirrors
the format_freq/1 integer-comma formatting used on the detail view so
the popups match the rest of the UI. Approved/on-air beacons render
green, off-air beacons render gray. BeaconLive.Index.mount/3 now
materializes the list into an assign so it can encode it into the data
attribute; the live PubSub path keeps the map in sync on
create/update/delete.

Profile (/u/:callsign) polish: drop the hero-radio, hero-signal, and
hero-information-circle icons per request, and replace the daisyUI
`avatar placeholder` wrapper with a plain flex-centered circle so the
initial letter actually lives inside the circle instead of anchored to
the top-left corner (daisyUI 5 removed the old placeholder-centering
behavior).

Navigation ordering: in the top navbar (layouts.ex) Contacts now sits
immediately before Contact Map instead of after it. All three vertical
sidebars already had them adjacent in the correct order, so only the
horizontal nav changed.
2026-04-12 16:23:09 -05:00
eedd91c6ee
Fix /map prod crash on reconnect and redraw reach polygon on band change
Two bugs on the propagation map:

1. LiveStash reconnect path crashed the LiveView with KeyError on
   :initial_scores_json. stash_assigns/2 only persists selected_band and
   selected_time, but the recovered-socket branch returned early without
   rebuilding any of the other mount-time assigns (bands, bounds, the
   initial score JSON payload, grid/radar toggles, antenna height).
   Users on the Phoenix longpoll fallback — or anyone whose websocket
   reconnected after a brief network blip — saw a blank page and a
   server-side 500. Refactor mount/3 to always compute ephemeral assigns,
   using recovered selected_band / selected_time when present and
   falling back to defaults otherwise.

2. The propagation reach polygon shown after clicking a point never
   redrew when the user changed bands or scrubbed the forecast timeline.
   The redraw logic only ran inside the point_detail handler and was
   gated on `hull.length >= 3`, so the stale polygon from the old band
   stuck around whenever the new band had no coverage at the clicked
   point. Extract a redrawReachPolygon/0 helper on the Leaflet hook that
   always clears the previous polygon first and pulls the tier color
   from the current score at the clicked location, then call it from
   update_scores (band change + server time scrub) and the cached
   timeline-scrub path. The point_detail handler delegates to the same
   helper so the polygon stays in sync with both the score grid and the
   detail panel.
2026-04-12 15:42:45 -05:00
85036eff21
Add toggleable weather radar overlay, disable rain-scatter display
New "Weather radar" toggle in /map's control panel (mobile + desktop)
layers an ECCC GeoMet WMS composite dBZ mosaic over the propagation
heatmap. The Radar_1km_dBZ-Extrapolation layer covers CONUS + Canada at
1 km resolution, updated every ~6 minutes. Tiles are lazy-created on
first toggle so visitors who never open the panel never hit the WMS
service. Refresh is driven by a setInterval that calls setParams with a
cache-busting timestamp, matching ECCC's publish cadence.

Rain-scatter display (NEXRAD cell markers + "Rain Scatter" section in
the point-detail panel) is disabled for now. The live radar overlay
answers "is there rain near this point?" more directly, and the scatter
propagation feature needs more work before it's useful. The Elixir
start_async, handle_async, and fetch_rain_scatter helpers are removed.
The JS drawScatterMarkers / buildScatterBlock paths remain as harmless
dead code since they're guarded by a `rain_scatter` field the server no
longer sends — easy to re-enable later by restoring the payload key.
2026-04-12 15:36:29 -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
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
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
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
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
91fb5cda22 Delay disconnect alerts by 3s to avoid flashing on brief hiccups 2026-04-12 10:40:42 -05:00
5f803e4c0d Add 6-char subsquare grid overlay and fix map edge rendering
- Show subsquares (5' x 2.5') at zoom >= 11 with lighter styling
- Pad map bounds by 10% so edge tiles always have score data
- Re-send bounds after 500ms to catch late container layout changes
2026-04-11 18:08:18 -05:00
58e4f55ba8 Curved earth surface in elevation profile with ducts following curvature 2026-04-11 17:10:38 -05:00
1f368f9b61 Convert all JavaScript to TypeScript with type annotations
- Rename all .js files to .ts in assets/js/
- Add interfaces for hook state, data structures, and event payloads
- Add type annotations to function parameters and return types
- Create type declarations for vendor deps (Leaflet, Chart.js, Phoenix, topbar)
- Update tsconfig.json for strict TypeScript checking
- Update esbuild entry point to app.ts
2026-04-11 16:59:28 -05:00
3d550f24c6 Defer map init and sendBounds to requestAnimationFrame in all map hooks 2026-04-11 16:49:17 -05:00
18e920fe54 Fix backfill page missing partitioned table stats, fix contacts map init
Backfill page now sums child partition stats for partitioned tables
like hrrr_profiles (relkind 'p') which were excluded by the relkind
'r' filter. Contacts map defers Leaflet init to requestAnimationFrame
so the flex container has dimensions before map initialization.
2026-04-11 16:34:15 -05:00
77656ba023 Move contacts map controls to sidebar matching main map layout
Replace the JS-built Leaflet control panel with a server-side
sidebar (desktop) and mobile floating controls. Band checkboxes,
callsign filter, and nav links now match the main propagation map
layout. Filter state managed by LiveView, pushed to JS via events.
2026-04-11 16:21:04 -05:00
2aa76727f9 Add callsign filter and band counts to contacts map
- Callsign input filters contacts where either station matches
  (case-insensitive substring match, debounced 300ms)
- Band checkboxes now show per-band count of visible contacts
- All/None buttons for quick band selection
- Header count updates dynamically with filters
- Complete rewrite: rebuilds map on any filter change instead of
  toggling individual layers (simpler, handles cross-filter correctly)
2026-04-11 15:26:57 -05:00
e8ae407be9 Add rain scatter prediction to map detail panel
When clicking a grid point, fetches the latest NEXRAD composite
reflectivity and identifies rain cells within 300 km that could
enable rain scatter contacts. Shows:
- Scatter classification (excellent/good/marginal/none)
- Top 3 cells with dBZ, distance, bearing, and relative signal
- Colored circle markers on the map at rain cell locations
- Markers sized by reflectivity, colored by intensity

Uses simplified bistatic radar equation accounting for reflectivity,
frequency-dependent scattering (Rayleigh/Mie), and R^4 path loss.
NEXRAD cells sampled every ~5 km within bounding box for efficiency.
2026-04-11 15:19:19 -05:00
9fea93f59c Show earth curvature in contact elevation profile
Terrain elevations now include the earth bulge correction, so the
profile visually humps up in the middle relative to the straight
LOS beam — showing how the curved earth rises into the signal path.
Uses actual k-factor from HRRR refractivity when available, otherwise
standard 4/3. For a 300 km path at k=4/3, the midpoint bulge is
~2,650 m (8,700 ft).
2026-04-11 14:55:21 -05:00
ab8773acd0 Add per-band filter checkboxes with All/None to contacts map 2026-04-11 14:55:21 -05:00
ff1b2d0cbe Add Maidenhead grid overlay toggle to contact map 2026-04-11 14:55:21 -05:00
13782ea7d4 Increase max zoom on main map from 10 to 13 2026-04-11 14:55:21 -05:00
bcdbf0b53a Show per-duct layer heights in map detail panel
When clicking a grid point with ducting, the panel now shows each
duct layer with base-top height in feet, thickness in meters, and
minimum trapped frequency. Data flows from Duct.analyze through
the scoring factors as a ducts array.
2026-04-11 14:13:24 -05:00
58a99de3cd Fix map detail panel: update weights, add PWAT, show duct info
- Update all 9 hardcoded weights to match recalibrated values
- Add missing PWAT factor (11.3% weight) to FACTOR_META and FACTOR_ORDER
- Reorder factors by weight (highest first)
- Fix pressure explanation (low pressure is better, not rising)
- Add PWAT factor explanation (beneficial vs harmful by band)
- Show duct info panel when native data detects ducting layers
  (count, thickness, minimum trapped frequency)
2026-04-11 13:49:24 -05:00
0059317043 Beacon page improvements: comma formatting, sorting, rounding
- 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
2026-04-11 09:41:51 -05:00
ac36441102 Remove grid squares toggle from rover planner page
The rover page now shows only: station markers, band selector,
propagation heatmap, and URL sharing. Grid overlay remains on
the main /map page.
2026-04-10 17:31:23 -05:00
a95d2ada52 Compact beacon detail page, toggleable coverage, About page formatting
- 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.
2026-04-09 09:12:32 -05:00
e327ff584e Map UX polish and populated About page
- /map now centers on the visitor's Cloudflare geolocation when the
  cf-iplatitude/cf-iplongitude headers are present, falling back to DFW.
  Initial HRRR score bounds track the chosen center.
- Added /about link to the /map side navigation.
- Renamed "Propagation Map" label to "Propagation Prediction Map" in
  the /map page title, sidebars, and the /weather back-link.
- Populated /about with mission, approach, stack, roadmap, and a live
  stat grid fed from DB counts (estimates via pg_class.reltuples for
  the multi-million-row tables).
2026-04-08 17:06:53 -05:00
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
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
e4fb422402 Dock map controls to right sidebar with collapsible panel
- Move controls (band selector, grid toggle, nav links) to a docked
  right sidebar on desktop, keep floating overlay on mobile
- Point detail panel floats over the map bottom-left instead of inside
  the narrow sidebar
- Add collapse/expand toggle for the sidebar
- Add auth links (login/logout/settings) to sidebar
- Wrap public live routes in live_session with UserAuth on_mount
- Use daisyUI menu lists for sidebar navigation
2026-04-08 12:35:00 -05:00
e6aab62ea8 Show grid squares by default on rover planner 2026-04-08 10:21:40 -05:00
27deb02bc7 Add grid squares toggle to rover planner
Extract shared Maidenhead grid overlay into maidenhead_grid.js so both
the main map and rover planner use the same implementation. Wire up the
toggle_grid event and checkbox on the rover page matching the main map.
2026-04-08 09:06:09 -05:00
75fd05a6d1 Simplify rover planner: remove grid toggle and coverage calculation 2026-04-08 09:06:09 -05:00
1bee565d04 Add HRRR propagation overlay to rover planner map 2026-04-07 17:39:03 -05:00
941182ce7b Render rover grid overlay above coverage using custom pane 2026-04-07 17:27:09 -05:00
cdbb47bf1b Show 'Press ESC to close' hint on map and weather pages
Subtle hint appears in top-right corner (desktop only) when the
detail panel is open. Fades in when panel shows, fades out on ESC.
Hidden on mobile where the X button is more appropriate.
2026-04-07 17:05:15 -05:00
efe7103fe1 Use proper Maidenhead grid overlay on rover page
Extract GridSquare class and updateGridOverlay from propagation_map_hook
into shared maidenhead_grid.js module. Rover map now shows the same
zoom-adaptive Maidenhead grid overlay as the main /map page — red
rectangles with labels that switch between field (2-char) and square
(4-char) based on zoom level. Updates on pan/zoom.

Toggle button hides/shows the grid overlay.
2026-04-07 16:58:03 -05:00
caa03fed53 Add grid toggle, use 0.25deg resolution for coverage scoring
- Toggle button for Maidenhead grid overlay (on by default)
- Coverage candidates now at 0.25° resolution (~28 km) instead of
  2°×1° Maidenhead grids, matching HRRR propagation grid density
- Coverage rectangles render as 0.25° cells for finer detail
- Grid overlay is separate from coverage coloring
2026-04-07 16:31:39 -05:00
70521c37f2 Rover planner: auto-compute coverage areas with terrain + propagation
Rewrites rover planner to automatically color the map after stations
are entered. No manual stop selection needed — the system evaluates
every Maidenhead grid in range and scores them by:

- SRTM terrain analysis to each station (diffraction loss, LOS verdict)
- HRRR propagation forecast (ducting, refractivity, atmospheric conditions)
- Distance to stations vs band range limits
- Propagation boost: good conditions (score 80+) make blocked paths
  viable through ducting, matching the app's core beyond-LOS prediction

Coverage score formula: boosted path quality 30% + propagation 30%
+ distance 20% + station count 20%. Colored grid squares on the map
show best (green) to worst (red) operating positions.

Sidebar shows top 10 grids ranked with: workable station count,
propagation score, best operating hour, 18-hour forecast sparkline.
Click a grid for per-station detail: distance, terrain verdict,
diffraction loss.

URL stores stations + band for sharing: /rover?stations=W5ISP,K5TR&band=10000
2026-04-07 15:43:26 -05:00
43b51fdb98 Throttle GPS updates on path page to once per minute
Switch from getCurrentPosition (which looped via push_patch) to
watchPosition with a 60-second throttle. Break the server-side
request_gps loop by skipping re-request when coords already exist.
2026-04-07 14:44:34 -05:00