Commit graph

38 commits

Author SHA1 Message Date
109c4e141f
feat(eme): 3D WebGL Earth-Moon globe with lazy-loaded Three.js
Replaces the SVG schematic on /eme with a textured WebGL scene: NASA
Blue Marble Earth, real-size Moon, outbound beam, dashed return ray
to the sub-lunar point, and a red shading on the anti-moon hemisphere
that marks who can't see the Moon (outside the bounce footprint).

Three.js (~680 kB) now lives in its own chunk; esbuild --splitting
keeps it out of the main bundle so it only loads when a user hits
/eme. Main app.js drops from 1.6 MB to 922 kB.

Also swaps the Elevation/SNR rows so the badge comes before the number.
2026-04-23 17:00:53 -05:00
bd107ee747
feat(eme): add /eme Earth-Moon-Earth calculator
New /eme page — antenna aiming + link budget for moonbounce.

Given a station (callsign / grid / lat,lon), TX power, antenna gain,
detection bandwidth, and system Tsys, the page shows in real time:

- Moon azimuth / elevation from the observer (ticks every 10 s)
- Slant range to the Moon
- EIRP breakdown: TX(dBm) + gain(dBi) = EIRP(dBm)
- Path-loss decomposition (Earth → Moon → Earth):
    free-space spreading (×2)
    − moon reflection gain 10·log(4π σ / λ²) where σ = ρ·π·R²
    = round-trip path loss
- Band-dependent lunar albedo ρ (VHF ≈ 0.065, microwave ≈ 0.08,
  mm-wave ≈ 0.02) surfaced as a named line item with the current
  ρ shown to three decimals
- Received power, thermal noise floor, and SNR with a badge that
  classifies the margin (strong / marginal / below noise)
- Self-echo Doppler shift with its radial-velocity driver

New modules:

- `Microwaveprop.Moon` — low-precision lunar ephemeris
  (Astronomical Almanac Sec. D.4 truncated series). Provides
  `julian_day/1`, `geocentric/1`, `observer_position/3`, and
  `radial_velocity_km_s/3`. Accurate to ~0.3° position / ~200 km
  distance — well inside any amateur EME-dish beamwidth.
- `Microwaveprop.Propagation.Eme` — path-loss helpers:
  `moon_albedo/1` (band lookup), `moon_rcs_m2/1`,
  `fspl_round_trip_db/2`, `moon_reflection_gain_db/2`,
  `path_loss_db/3`, `received_power_dbm/1`, `noise_floor_dbm/2`,
  `snr_db/1`, `doppler_shift_hz/2`.

Wired into the main nav ("EME" button) and the router between
/path and /rover.

Tests: 3 properties + 41 unit tests cover JD conversion, moon
position envelope, band-dependent albedo lookup, decomposition
identity, linearity/monotonicity of path loss and Doppler, and a
LiveView integration test against the rendered DOM.

Full suite: 2,460 tests + 170 properties; credo strict clean.
2026-04-23 16:18:20 -05:00
13088a6906
feat(nav): add Status link to admin dropdown 2026-04-23 12:49:48 -05:00
0c98707091
feat: harden /map analysis breakdown + move Plausible to root layout
Two fixes cover the blank "analysis breakdown" panel the user reported:

1. Bound the point_detail fallback lookback to 24h. Previously
   factors_from_fallback_profile would happily use a week-old analysis
   profile when no current one existed — now anything older than 24h
   returns an empty factor map so the UI can surface "breakdown
   unavailable" instead of silently misattributing.

2. Move the Plausible analytics snippet from Layouts.app into
   root.html.heex. Full-bleed LiveViews (/map, /contacts/map,
   /weather, home) bypass Layouts.app, so the snippet only loaded on
   the navbar-wrapped pages. root.html.heex is loaded once per HTTP
   document so coverage is now universal.

Added ~30 tests locking both down:
  • point_detail fallback: exact-time wins, 24h boundary accepted,
    30h-stale rejected, multi-band coverage, missing-cell degrades to
    empty factors, default-valid_time path
  • analytics_test.exs: Plausible script present on 12 representative
    pages, exactly once, loaded async, surviving a login round-trip

Also fixed pre-existing credo issues per standing rule: shortened the
map_live_test ETS match_delete call, extracted a function from the
deeply-nested hrrr_point_enqueuer.enqueue_for_contacts/1 cond, and
swapped Mix.Tasks.Rust.Golden's IO.inspect for Mix.shell().info.
2026-04-21 15:56:30 -05:00
d0dbb5c5b4
style(layouts): split plausible script tag onto two lines for heex 2026-04-21 15:30:51 -05:00
48d30f1688
feat(web): add Plausible analytics snippet to the shared layout footer
Self-hosted Plausible instance at a.w5isp.com. No cookies, no personal
data; drops one small JS file per page view.
2026-04-21 14:12:54 -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
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
b8b78f7e57
feat(design): apply NTMS design system tokens
Adopt the handoff design system: navy-slate surfaces with indigo-violet
accent (#6366F1 dark / #5458E3 light), Inter as the UI font, hairline
1px borders, softer corner radii. Add propagation-scale and amateur-band
CSS vars so JS hooks can pull theme-aware colors via getComputedStyle.

Polish: drop table zebra striping for hover-on-base-200 rows with
uppercase small-caps headers, bump .header to text-2xl, tighten nav
spacing with a hairline divider, fix broken text-brand -> text-primary
on the auth screens.

Wire --band-* into contacts_map_hook: resolve once at mount, re-resolve
on data-theme mutation so polylines recolor when the theme toggles.
2026-04-17 17:34:15 -05:00
b6caffea3e
feat(nav): show last-deployed timestamp
Docker final stage bakes BUILD_TIMESTAMP into /app/BUILD_TIMESTAMP via
build-arg (only consumed after mix compile, so earlier layer caching is
preserved). Application.build_timestamp/0 reads the file, falls back to
DEPLOY_TIMESTAMP env, then to compile time. Navbar renders a compact
"Deployed Xm ago" with the full UTC time in the tooltip.
2026-04-17 13:08:51 -05:00
c3616b31c7
Surface HRRR weather details on /path
Adds an Atmospheric Profile section to the link calculator that shows
the HRRR data actually feeding the prediction: valid/run time, a
per-point table (src/mid/dst) with surface temp, dewpoint, pressure,
HPBL, PWAT, surface refractivity and dN/dh, plus a skew-T log-P chart
for the midpoint vertical profile.

Pulls the skew-T chart function out of contact_live into a shared
component so both LiveViews can render it without duplication.
2026-04-15 11:57:51 -05:00
f3d75373dd
Admin-only nav dropdown
Collapse the three admin-only links (Users, Contact edits, Oban) into
a single "Admin" dropdown in the horizontal nav so the top bar isn't
cluttered with admin controls for the one admin account. Non-admins
see nothing extra.
2026-04-12 17:07:07 -05:00
ad041ccf4a
Nav reorder + path calc link from beacon detail
Move Scoring Algorithm directly above About in map and weather-map
sidebars; reorder the horizontal nav so Submit sits before Contacts
to match.

Add a "Plot path to beacon" button on the beacon detail page that
navigates to /path with destination, band (snapped via BandResolver),
and dst_height_ft prefilled from the beacon.
2026-04-12 16:33:09 -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
fc245367e3
User profiles at /u/:callsign, flexible band input, assorted UX cleanup
Profile page:
  * New MicrowavepropWeb.UserProfileLive at /u/:callsign is a public
    page showing a user's contacts and beacons. Resolves case-
    insensitively so /u/w5isp and /u/W5ISP are the same thing; unknown
    callsigns redirect to /. Uses daisyUI card / stats / table
    components with an avatar-placeholder initial and hero icons.
  * Accounts.get_user_by_callsign/1 (case-insensitive) plus
    Radio.list_contacts_for_user/1 and Beacons.list_beacons_for_user/1
    back the page. Beacons list includes both approved and pending so
    owners see their drafts.
  * The top nav bar and the three LiveView sidebars (MapLive,
    WeatherMapLive, ContactMapLive) now render the logged-in callsign
    as a navigate link to /u/:callsign instead of a static label.
  * Nine new tests cover the lookup, the LiveView render, and the
    ownership-scoped queries.

Flexible band input:
  * New Microwaveprop.Radio.BandResolver module converts any of:
    ADIF wavelength labels ("33cm", "1.25cm", "6mm", case/whitespace
    insensitive), numeric frequency strings ("903.100", "10368.000"),
    and canonical MHz integers into the one of the site's known bands.
    Returns the nearest allowed band for numeric inputs >= 900 MHz,
    nil otherwise.
  * 902 MHz is added to Contact.@allowed_bands, ContactEdit.@allowed_bands,
    AdifImport.@allowed_bands, and the BandResolver list so "33cm"
    round-trips end-to-end.
  * AdifImport and CsvImport now delegate band resolution to
    BandResolver, and Radio.create_contact/2 normalizes the :band attr
    on the way in so the manual form and any API callers benefit too.
    CsvImport's "invalid band" tests previously used 99999 MHz which
    the new resolver snaps to the nearest allowed band; swapped to
    "notaband" which is truly unresolvable.

Contacts and beacons list UX:
  * Remove the "Submitted" column from /contacts — it duplicated info
    already visible on the detail page and was pushing the real
    columns off narrow viewports. submitted_cell/1 and its three
    column-specific tests go with it.
  * Hide the Lat / Lon columns from /beacons — six decimal places of
    coordinates weren't useful next to the grid square and took a
    disproportionate amount of row width.
2026-04-12 16:13:08 -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
91fb5cda22 Delay disconnect alerts by 3s to avoid flashing on brief hiccups 2026-04-12 10:40:42 -05:00
7fb340bc35 Fix all remaining credo --strict issues (0 issues)
Aliases: add module aliases for 9 nested module references
Apply: replace apply/3 with direct module attribute calls
Line length: break 1 long spec line
Refactoring: extract helpers to reduce complexity and nesting
in show.ex, radio.ex, weather workers, terrain, duct detection,
backfill dashboard, contact map, and mix tasks
2026-04-12 10:26:53 -05:00
aba4ca538d Add PayPal donate link to footer and about page 2026-04-12 09:34:01 -05:00
a66d3094ca Add contact edit approval system with admin review queue
Registered users can suggest edits to any contact's core fields
(callsigns, grids, band, mode, timestamp). Edits enter an admin
approval queue with field-by-field diff view. On approve, changes
are applied and enrichment re-enqueued if grids/band changed.
Users receive email notification on approve or reject.

Also updates dependabot.yml for mix ecosystem.
2026-04-11 16:15:49 -05:00
caad7d90a7 Mount Oban Web dashboard at /admin/oban
Vendor oban_web 2.12.1 and oban_met 1.1.0 (taken from the
towerops-web2 vendor tree), bump oban to ~> 2.21, and mount the Oban
dashboard at /admin/oban behind the existing require_admin on_mount
hook. Adds an admin-only "Oban" nav link and extends the Dockerfile
to copy vendor/ before deps.get so production builds pick up the
path dependencies.
2026-04-09 14:07:33 -05:00
8007346d79 Trim footer and privacy copy
Drop the trailing period on the footer NTMS link, remove the privacy
page's removal-request section, and tidy a stray blank line on About.
2026-04-09 12:30:10 -05:00
442d1d1bdc Add /privacy page with shared footer across non-map pages
New PrivacyLive page explains that collected/submitted data is used
only for propagation research and will never be sold, traded, or
disclosed. Layouts.app now renders a small footer with the NTMS
attribution and a Privacy link on every page that uses it, so the
full-screen map pages are untouched. The duplicate NTMS credit on
the About page is removed since it now lives in the shared footer.
2026-04-09 09:54:54 -05:00
abe0aa9ec6 Pin referrer policy so OSM tile requests always send a Referer
OSM's tile usage policy requires a Referer header identifying the site.
strict-origin-when-cross-origin is the current browser default and sends
the origin on HTTPS→HTTPS cross-origin requests; pinning it explicitly
prevents silently losing the header if a browser default ever changes.
2026-04-09 09:30:23 -05:00
f8763feb38 Add About page, hide Rover from menu 2026-04-08 15:42:32 -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
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
8a7719b953 Add /rover planner for microwave contest rovers
Interactive map-based tool for planning rover operating positions.
Rovers enter stationary stations they want to work, then click the
map to evaluate candidate operating grids.

Features:
- Add stationary stations by callsign or grid square
- Click map to add operating positions (snaps to Maidenhead grid)
- Async SRTM terrain analysis: each stop computes terrain profile
  to every station, shows CLEAR/BLOCKED verdict + diffraction loss
- Terrain lines on map: green=workable, red=blocked, dashed=marginal
- Propagation score and 18-hour forecast sparkline per stop
- Route summary: driving distance, unique grid-station pairs
- Band selector affects range limits and propagation scores
- Numbered waypoint markers with score-colored backgrounds

New files:
- Geo module (shared haversine/bearing, used by PathLive too)
- RoverLive with fullscreen map + sidebar
- RoverMap JS hook with grid overlay, station/stop markers, route line
- Nav links in header and map control panel
2026-04-07 14:17:48 -05:00
95351a2358 Add /path calculator page
LiveView page for analyzing microwave propagation between two points.
Features:
- Source/destination input: callsign (via gridmap.org API) or Maidenhead grid
- Band selector for all 8 microwave bands (10-241 GHz)
- Leaflet map showing path line (reuses ContactMap hook)
- Terrain elevation cross-section (reuses ElevationProfile hook)
- Link summary: distance, bearing, terrain verdict
- Loss budget: FSPL + O2 + H2O + rain + diffraction
- Propagation score with 10-factor breakdown
- Current atmospheric conditions from path-integrated HRRR

New CallsignClient for gridmap.org location lookup.
Navigation links added to main navbar and map control panel.
2026-04-07 13:25:04 -05:00
cd8d7a0f52
Add contact detail map, elevation profile chart, and UI improvements
- Leaflet map on contact detail pages showing both station locations
- SRTM elevation profile with Chart.js: terrain, LOS, Fresnel zone
- Path info: distance, azimuth, elevation angle in both directions
- Compact 4-column grid layout for contact details
- Contact map page with all contacts plotted
- Rename QSOs to Contacts in page header
- Wrap algo page in standard layout
2026-04-01 12:34:03 -05:00
ea96c93e2d
Rename QSO to Contact in UI, add higher bands, improve submit page
- All menus and UI text now say Contacts instead of QSOs
- Add 68, 122, 134, 241 GHz bands to submit form and validation
- Add info box on submit page explaining why contacts matter
- Larger submit button with icon
- Make HRRR partition migration idempotent for partial re-runs
2026-04-01 12:28:24 -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
a18cf48493
Add regional analysis to algo.md, update site title to NTMS
- Document Finding 11: regional QSO performance by Maidenhead field
- Explain why regional weight adjustments are not justified
- Fix algo.md table formatting (blank lines before tables for Earmark)
- Change page title from Phoenix Framework to North Texas Microwave Society
2026-03-31 10:47:39 -05:00
2f2f3388cd
Rename 'How this works' to 'Scoring Algorithm' 2026-03-31 10:24:04 -05:00
5a0113aac2
Add Leaflet map integration with propagation score display
Vendor Leaflet 1.9.4 (JS, CSS, marker images) and wire it into
the esbuild/Tailwind asset pipeline. Create MapLive with band
selector buttons, auto-refresh, and a colocated JS hook that
renders propagation scores as color-coded circle markers with a
legend. Stub Propagation context and BandConfig modules provide
the data interface for the scoring pipeline.

Add nav bar with links to Map, QSOs, and Submit pages.
2026-03-30 17:11:00 -05:00
6f16395f44
Add QSO import, solar indices, Oban workers, LiveView UI, and parallel weather import
- Radio context with QSO schema and CSV import script (58K contacts)
- Solar index schema, GFZ client, and daily Oban cron worker
- LiveView dashboard with QSO table and weather correlation views
- Parallelize weather import script using Task.async_stream (10 concurrent workers)
- Replace sequential rate_limit sleeps with concurrency-based backpressure
- Atomic progress counter for interleave-safe reporting
2026-03-29 13:04:55 -05:00
bc674f0d54
initial 2026-03-28 11:28:47 -05:00