Skip to content

Feature/ibp beacon integration#906

Merged
ceotjoe merged 11 commits intoaccius:Stagingfrom
ceotjoe:feature/ibp-beacon-panel
Apr 14, 2026
Merged

Feature/ibp beacon integration#906
ceotjoe merged 11 commits intoaccius:Stagingfrom
ceotjoe:feature/ibp-beacon-panel

Conversation

@ceotjoe
Copy link
Copy Markdown
Collaborator

@ceotjoe ceotjoe commented Apr 12, 2026

What does this PR do?

IBP Beacon Integration — Phases 1–3 (#878)

Adds full support for the NCDXF/IARU International Beacon Project — a network of 18 HF beacons transmitting sequentially on 14.100, 18.110, 21.150, 24.930 and 28.200 MHz in a fixed 3-minute/18-beacon cycle.

The schedule is fully deterministic — no server or network call is needed to compute it.


Phase 1 — IBP Panel (IBPPanel.jsx)

  • New dockable panel (ibp) under the Propagation group
  • Shows which beacon is transmitting on each of the 5 IBP bands right now
  • Per-slot countdown bar (10 s) and 3-minute cycle timer in the header
  • Bearing and distance to each beacon when QTH is configured
  • IBP panel is dockable-only; Modern and Classic layouts are not affected.

Phase 2 — Map Layer Plugin (useIBPLayer.js)

  • New map layer registered in layerRegistry.js
  • Pulsing animated markers for active beacons (band-coloured), dim circle markers for inactive
  • Great-circle arcs from the operator's QTH to each active beacon
  • Floating control panel: band filter, path toggle, inactive toggle, slot countdown
  • buildPopup() shows callsign, location, grid, active bands and seconds remaining

Phase 3 — RBN Cross-Reference (useIBPRBN.js)

  • New hook polls /api/rbn/spots for all 18 IBP beacon callsigns every 60 s
  • When a beacon has been heard by RBN skimmers in the last 5 minutes, a compact RBN N× +XdB indicator appears beneath its location in the panel
  • Shows skimmer count and best SNR — no indicator when propagation isn't there

Rig Control Integration

  • Clicking a band row in IBPPanel tunes the rig to band.mhz when rig control is enabled
  • Cursor and tooltip change to signal interactivity only when the rig bridge is active
  • Active beacon markers on the map also trigger tuneTo() on click
  • Rig callbacks stored in a ref to avoid re-creating markers on rig state changes

i18n

  • All 10 IBP keys translated into all 15 supported languages:
    ca, de, es, fr, it, ja, ka, ko, ms, nl, pt, ru, sl, th, zh

Files changed

File Change
src/utils/ibp.js Beacon data + deterministic schedule engine
src/hooks/useIBP.js 1-second hook, snaps on 10 s boundary
src/hooks/useIBPRBN.js RBN cross-reference hook (Phase 3)
src/components/IBPPanel.jsx Panel component with rig control + RBN indicator
src/plugins/layers/useIBPLayer.js Map layer plugin with rig control
src/plugins/layerRegistry.js IBP layer registered
src/App.jsx useIBP wired into layoutProps
src/DockableApp.jsx ibp panel factory added
src/lang/en.json + 15 others All IBP i18n keys

Testing

  • IBP panel appears in Dockable layout under Propagation
  • Band rows update every 10 s in sync with the NCDXF schedule
  • Bearing/distance columns appear only when QTH is set
  • Map layer shows pulsing markers for active beacons and dim circles for inactive
  • Great-circle arcs drawn from QTH to active beacons; toggle works
  • Clicking a band row tunes the rig (rig bridge must be connected)
  • Clicking an active map marker also tunes the rig
  • RBN indicator appears when a beacon has recent skimmer spots
  • Panel renders correctly in German, Japanese, Russian (spot-check i18n)

Checklist

  • App loads without console errors
  • Tested in Dark, Light, and Retro themes
  • Responsive at different screen sizes (desktop + mobile)
  • If touching server.js: caches have TTLs and size caps (we serve 2,000+ concurrent users)
  • If adding an API route: includes caching and error handling
  • If adding a panel: wired into Modern, Classic, and Dockable layouts
  • No hardcoded colors — uses CSS variables (var(--accent-cyan), etc.)
  • No .bak, .old, console.log debug lines, or test scripts included

Screenshots (if visual change)

IPB Panel:
Bildschirmfoto 2026-04-05 um 21 26 37

IBP map layer:
Bildschirmfoto 2026-04-05 um 21 35 08

ceotjoe and others added 7 commits April 5, 2026 00:17
Adds a live NCDXF/IARU International Beacon Project panel that shows
which of the 18 beacons is transmitting on each IBP band right now,
with a per-slot countdown and bearing/distance from the operator's QTH.
No server changes — the schedule is fully deterministic.

- src/utils/ibp.js: beacon list, getSchedule(), slot/countdown helpers
- src/hooks/useIBP.js: 1 s ticker, recomputes only on 10 s slot boundary
- src/components/IBPPanel.jsx: 5-row band table, progress bar, geo column
- Wired into App.jsx (useIBP hook) and DockableApp.jsx (factory + panel
  selector under Propagation group)
- i18n keys added to en.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a Leaflet map layer plugin for the 18 NCDXF/IARU IBP beacons.
No network requests — schedule is fully deterministic.

- Static dim markers for all 18 inactive beacon sites
- Pulsing highlighted marker (band-coloured + label) for each active
  beacon; updates every 10 s on slot boundary
- Dashed great-circle arcs from operator QTH to each active beacon,
  coloured by band, replicated across world copies
- Draggable/minimisable control panel: band filter, path toggle,
  inactive-station toggle, live slot countdown
- Registered in layerRegistry under Propagation category
- i18n name/description keys added to en.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
activeBandsByBeacon stores raw band objects {mhz, label, offset} but
buildPopup was accessing b.band.label (undefined), causing a TypeError
that aborted the entire marker-drawing forEach so no beacons appeared.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clicking a band row in IBPPanel or an active beacon marker on the map
now calls tuneTo(band.mhz) when rig control is enabled.  The cursor and
tooltip update to signal interactivity only when the rig is connected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Polls /api/rbn/spots for all 18 IBP beacon callsigns every 60 s.
When a beacon has been heard by RBN skimmers in the last 5 minutes,
a compact "RBN N× +XdB" indicator appears beneath its location in
the IBP panel, showing skimmer count and best SNR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds ibp.title, ibp.cycleCountdown, ibp.cycleCountdown.tooltip,
ibp.slotProgress.tooltip, ibp.footer, ibp.tune, ibp.rbn.heard,
ibp.rbn.tooltip, plugins.layers.ibp.name and .description to:
ca, de, es, fr, it, ja, ka, ko, ms, nl, pt, ru, sl, th, zh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
IBP beacons transmit in CW but getModeFromFreq() returns DATA/USB for
14.100, 21.150, 24.930 and 28.200 MHz. Pass 'CW' explicitly as the
second argument to tuneTo() in both IBPPanel and the map layer so all
5 beacon frequencies set the correct mode regardless of the band plan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@MichaelWheeley MichaelWheeley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had Mrs.W review src/lang/th.json
two minor changes made as comments

ceotjoe and others added 2 commits April 13, 2026 08:06
- th.json: improve ibp.rbn.tooltip and plugins.layers.ibp.description
  wording for more natural Thai
- useIBPRBN.js: apply Prettier formatting (fetch options object style)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolved conflict in th.json: kept IBP keys from feature branch and
Thai translations for band.conditions, contest, dxCluster, keybindings
and plugin layer strings from upstream/Staging.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ceotjoe ceotjoe requested a review from MichaelWheeley April 14, 2026 12:48
Copy link
Copy Markdown
Owner

@accius accius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — IBP Beacon integration

Excellent, thorough PR — deterministic schedule, clean separation of panel/layer/RBN hook, and complete i18n across all 15 languages. Couple of real issues plus some polish.

Likely bug

  1. useIBPRBN.js counts the wrong field. count is computed as new Set(json.spots.map((s) => s.callsign)).size, but every spot returned from /api/rbn/spots?callsign=<X> has the same callsign (the spotted station) — so count is always 0 or 1. I believe the intent is "number of distinct skimmers that heard the beacon" — that should be new Set(json.spots.map((s) => s.spotter)).size (or whatever the skimmer field is named in the RBN spot object).

Robustness

  1. window.deLocation is read directly inside useIBPLayer.js. That's a brittle global; the rest of the layer system receives config / observerLocation via useLayer({ map, enabled, config, … }). Please thread the operator QTH through config rather than off window.

  2. setTimeout(…, 100) / setTimeout(…, 150) to wire up event handlers and draggable behaviour on the Leaflet control. Brittle on slow first paint. A requestAnimationFrame inside onAdd (or controlRef.current.getContainer() immediately after addControl) is deterministic.

  3. useIBP interval keeps ticking once per second for the lifetime of the app regardless of whether the panel/layer is mounted. Cheap but unnecessary — consider gating by enabled, or accepting the cost as documented.

  4. 18 parallel HTTP requests / 60s from each user for the RBN cross-reference. For the current user-base it's fine, but consider a single multi-callsign endpoint (e.g. /api/rbn/spots?callsigns=a,b,c) to cut load by ~18× once this gets traction.

Consistency / checklist

  1. Hardcoded colors in inactive markers: fillColor: '#666', color: '#999' in useIBPLayer.js. Please use CSS variables per checklist.

  2. "Wired into all three layouts" — the PR description claims Modern, Classic, and Dockable wiring, but only DockableApp.jsx actually renders IBPPanel. App.jsx passes ibp into layoutProps, but neither ModernLayout.jsx nor ClassicLayout.jsx consume it. If IBP is intended as dockable-only, update the description; if it's meant for all three, add the panel.

  3. getUpcomingSchedule is exported and unused in this PR — happy for it to land for future timeline work, but consider a TODO or drop until needed.

Small nits

  1. IBP band offsets {0, 17, 16, 15, 14} — a quick sentence naming the math ("band N starts N slots earlier than 14.1 in the cycle, so offset is (-N) mod 18") in the comment would help future readers.

  2. elapsed computed but unused in the "Update countdown" effect.

  3. ibp.rbn.heard uses {{count}}× while some locales render that as {{count}}x visually — acceptable, just noting.

Checklist:

  • Dark / Light / Retro — please verify (panel + layer + control panel).
  • Mobile — floating control at topright + draggable tends to land off-screen on small viewports.

Looks good overall — address 1, 2, 6, and the layout-wiring claim and this is ready.

Copy link
Copy Markdown
Contributor

@MichaelWheeley MichaelWheeley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude did a pretty good job!

@ceotjoe
Copy link
Copy Markdown
Collaborator Author

ceotjoe commented Apr 14, 2026

Claude did a pretty good job!

@MichaelWheeley exactly. :-)

Clarify & correctness:
- useIBPRBN: add comment clarifying spot.callsign = skimmer, spot.dx = beacon
- ibp.js: rewrite band offset comment with (-N mod 18) math explanation
- ibp.js: add TODO note on getUpcomingSchedule (Phase 4 timeline)
- useIBP: document intentional unconditional interval

Robustness:
- useIBPLayer: thread deLat/deLon via PluginLayer props, drop window.deLocation
- PluginLayer: forward deLat/deLon to useLayer hook
- WorldMap: pass deLocation.lat/lon as deLat/deLon to PluginLayer
- useIBPLayer: replace setTimeout(100) with direct div.querySelector wiring
- useIBPLayer: replace setTimeout(150) with double-rAF for makeDraggable

Consistency:
- useIBPLayer: replace hardcoded hex (accius#666/#999/#aaa/accius#888) with CSS variables;
  popup HTML uses var(--text-muted) inline; circleMarker options resolved via
  getComputedStyle at render time
- useIBPLayer: remove unused elapsed variable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ceotjoe
Copy link
Copy Markdown
Collaborator Author

ceotjoe commented Apr 14, 2026

@accius

Thanks for the thorough review — all points addressed in commits 5f894e4 and earlier. Notes below.


Likely bug — useIBPRBN skimmer count

The field is correct as written. In the RBN spot object produced by the server:

  • spot.callsign = the skimmer (the station that heard the beacon)
  • spot.dx = the beacon callsign (same for every spot in a mode=dx query)

So new Set(spots.map(s => s.callsign)).size does count distinct skimmers. The naming is confusing because the field that identifies the hearing station is called callsign — I've added a comment to make the intent explicit.


window.deLocation

Fixed. deLat/deLon are now threaded as props through WorldMap → PluginLayer → useLayer. window.deLocation is gone from useIBPLayer. (useRBN uses the same global — happy to clean that up in a follow-up if preferred, or I can include it here.)


setTimeout for event-handler and draggable wiring

Fixed. Event listeners are now attached directly to the in-memory div inside onAdd (no timeout needed — the element exists before Leaflet inserts it). The makeDraggable call is now wrapped in a double-requestAnimationFrame which is deterministic: first frame Leaflet inserts the element, second frame it's painted.


useIBP interval always ticking

Left as-is with a comment. The hook is consumed by both IBPPanel and useIBPLayer simultaneously and a single shared 1-second tick is negligible. Gating it would require either an enabled prop (adds complexity) or moving the hook inside the panel (breaks the map layer). Happy to revisit if this is a firm requirement.


18 parallel RBN requests

Acknowledged. A multi-callsign endpoint (/api/rbn/spots?callsigns=a,b,c) is the right long-term fix. Implemented.


Hardcoded colours

Fixed. Popup HTML now uses var(--text-muted) inline. For Leaflet circleMarker options (SVG attributes, can't accept CSS variables), colours are resolved via getComputedStyle(document.documentElement) at render time.


Layout wiring

IBP is intentionally dockable-only — there's no natural fixed slot for it in the Modern or Classic grid. PR description updated to reflect this.


getUpcomingSchedule exported but unused

Added a // TODO: consumed by Phase 4 (listening log timeline) comment. Happy to remove the export entirely if you'd prefer to keep the tree clean until it's needed.


Band offset math comment

Rewritten to show the (-N mod 18) derivation with a per-band table.


elapsed computed but unused

Removed.


ibp.rbn.heard× rendering

Noted. The × (U+00D7) renders correctly in every tested locale/font. Acceptable as-is.


Mobile — floating control off-screen

makeDraggable.js already saves positions as viewport percentages and calls clampToViewport on both restore and window resize, so a panel dragged near the edge on desktop won't jump off-screen on a smaller display.

The real gap is that makeDraggable has no touch event handlers — on mobile the panel can't be repositioned at all. The fix belongs in makeDraggable.js (adding touchstart/touchmove/touchend alongside the mouse handlers, reading e.touches[0]), which would benefit all six draggable panels in one change. I'd suggest a dedicated follow-up PR for that rather than broadening the scope here.

Add a multi-callsign branch to GET /api/rbn/spots: when ?callsigns=a,b,c
is present the handler resolves all callsigns in one request (capped at 30),
reusing the existing per-callsign rbnApiCaches entries, and returns a single
{ results: { [callsign]: { count, spots[] } } } object.

useIBPRBN now issues one fetch per 60 s instead of 18, cutting per-user
HTTP load by ~18× as adoption grows.  The original ?callsign= (singular)
route is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ceotjoe ceotjoe merged commit b81b510 into accius:Staging Apr 14, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants