Skip to content

feat(psk-map): band conditions, lookback, hover card + cross-platform spot-delivery fixes#3635

Merged
NF0T merged 9 commits into
aethersdr:mainfrom
jensenpat:feat/psk-band-conditions
Jun 17, 2026
Merged

feat(psk-map): band conditions, lookback, hover card + cross-platform spot-delivery fixes#3635
NF0T merged 9 commits into
aethersdr:mainfrom
jensenpat:feat/psk-band-conditions

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

Summary

Builds on the PSK Reporter map (#3565) with a forecast/UX layer and, importantly, fixes the cross-platform "no spots" reports from macOS/Windows users. Adds an HF band-conditions row, a lookback selector, richer reception stats, an instant hover card, and a connection-health indicator — and hardens the PSK Reporter transport so spots actually arrive on shipped builds.

All read-only public data; no new API keys.

The bug users hit: no spots on macOS/Windows

Live (MQTT) is the default, and it connected over TLS (1884) trusting OpenSSL's default cert store. The app links Homebrew OpenSSL, so the baked-in CA path is the build machine's — absent on users' Macs, and Windows has no system-store handling at all. TLS verification failed → MQTT never connected → no live spots, while the website (and our Qt HTTPS path) worked fine.

Fixes:

  • Plain MQTT (1883) instead of TLS (1884). The feed is public, read-only, credential-less spot data, so TLS added nothing but the cert headache. Plain MQTT behaves identically on every platform with no CA handling. Isolated to PskReporterClient; the shared MqttClient is untouched.
  • HTTP fallback. If MQTT can't connect (port blocked, etc.) the client keeps polling retrieve.pskreporter.info every 5 min and stops once MQTT confirms connected.
  • gzip decompression. poll() manually set Accept-Encoding: gzip, which disables Qt's transparent decompression — so the gzipped reply was fed raw to QXmlStreamReader and parsed 0 reports every time. Removing the header fixes the backfill (verified in logs: now parses 100+ reports).
  • Deep backfill on open. start() resets lastSeqNo, so opening always backfills the lookback window (even in Live mode) instead of doing a stale incremental query that returns nothing on reopen.
  • Callsign lifecycle. RadioModel now emits callsignChanged; the dialog restarts the client on a late-arriving or edited callsign instead of only on reopen.

Features

  • HF band-conditions row (bottom-left): the four N0NBH/hamqsl band-group ratings (80-40 / 30-20 / 17-15 / 12-10m) as Good/Fair/Poor pills, day/night by local time, reusing the existing PropForecastClient feed (no new network source).
  • Lookback selector (right of Mode): 15m / 30m / 1h / 2h / 4h / 8h, persisted. Drives the HTTP backfill depth and the displayed window.
  • Reception stats (top-right): spot count, distinct bands heard, and farthest receiver (callsign + distance).
  • Instant hover card: QGeoView's mapMouseMove doesn't fire for plain hovering (the inner QGraphicsView viewport consumes move events), so the card never appeared once the delayed tooltip was disabled. MapView now event-filters the viewport's MouseMove and shows a persistent frameless card that stays up until the mouse leaves the marker (no QToolTip fade). SNR is rendered in dark orange for readability on the dark card.
  • Connection indicator (bottom-right): MQTT/HTTP (white) plus a status bullet — green = connected with data, yellow = connected/connecting but no data, red = no good connection.

Server politeness (PSK Reporter rate limits)

  • Lookback debounce (750 ms) so spinning the selector coalesces into one query.
  • No redundant re-queries: the client tracks the deepest window backfilled this session and only hits the network when you go deeper; narrowing or revisiting a covered window is a pure local display filter. Retention keeps spots back to the deepest fetched window so widening back is request-free. (Closes the 503s seen when stepping the selector.)
  • HTTP polling still floored at the 5-minute policy minimum; appcontact set; MQTT health summarized to the log every 5 min.

Testing

  • Builds clean on macOS (Qt 6) after rebasing onto latest main.
  • Verified on-radio via the test Mac across several iterations (logpull): MQTT connects on 1883 and streams live spots; HTTP backfill parses 100+ reports (gzip fixed, no XML errors); deep backfill populates on open; lookback narrowing/widening no longer re-queries; connection bullet reflects MQTT↔HTTP and data state.

Files

PskReporterClient.{h,cpp} (transport + lookback + connection state), PskReporterMapDialog.{h,cpp} (UI: band row, lookback, stats, connection bullet), map/MapView.{h,cpp} (instant persistent hover card), RadioModel.{h,cpp} (callsignChanged), MainWindow_DigitalModes.cpp (pass PropForecastClient).

💻 Generated with Claude Code (Fable 5.0) with architecture by @jensenpat

jensenpat and others added 9 commits June 16, 2026 21:18
Adds a small row at the bottom of the PSK Reporter window showing the
four N0NBH/hamqsl HF band-group ratings (80-40 / 30-20 / 17-15 / 12-10m)
as Good/Fair/Poor colored pills, reusing the existing PropForecastClient
feed (no new network source). Day vs night rating set is chosen by the
operator's local time. The dialog now takes the shared PropForecastClient
(nullable — row is hidden when absent), fetches the detailed forecast on
open (guarded against overlapping requests), and refreshes on
detailUpdated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Reception stats (spot count, farthest) now pin to the top-right corner
  (right-aligned), instead of sharing the row with the transient status
  text which pushed them off-center.
- Status/update text moves to the bottom-right; the HF band-conditions
  pills stay bottom-left — a clean bottom row.
- Stats enriched: distinct bands heard, and best SNR (who hears you
  loudest) alongside spot count and farthest receiver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a small inline sparkline (new reusable Sparkline widget) pinned to
the bottom-right of the PSK Reporter window. Each sample is the number of
receivers that heard us within the trailing 15 minutes, taken once per
minute over the past hour — a rising line flags a band opening. On open
it backfills the full hour from the cached spot timestamps so it's
useful immediately; a per-minute QTimer extends it live and stops when
the window closes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… not showing)

Root cause of cross-platform 'no spots': Live mode (default) connected to
the PSK Reporter MQTT broker over TLS (1884) and relied on OpenSSL's
default cert store. Our app links Homebrew OpenSSL, so the baked-in CA
path is the build machine's — absent on users' macOS boxes, and Windows
has no system-store handling at all. TLS verification failed, MQTT never
connected, and there was no fallback, so shipped users saw no live spots
even though the website (and our Qt HTTPS path) worked fine.

Fixes:
- Connect to the broker over plain MQTT (1883) instead of TLS (1884).
  The feed is public, read-only, credential-less spot data, so TLS adds
  nothing — and plain MQTT behaves identically on every platform with no
  CA handling. Shared MqttClient is untouched (no risk to other users).
- HTTP fallback: if MQTT can't connect (port blocked, etc.) keep polling
  retrieve.pskreporter.info every 5 min; stop once MQTT confirms connect.
- RadioModel now emits callsignChanged; the PSK dialog restarts the
  client on a late-arriving or edited callsign instead of only on reopen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…I cleanup

- Lookback dropdown (15m/30m/1h/2h/4h/8h) right of Mode, driving the
  HTTP backfill depth and the retained/displayed window (replaces the
  fixed 24h tombstone); persisted.
- Removed the activity sparkline; replaced with a bottom-right connection
  indicator: 'MQTT'/'HTTP' + a status bullet (green = connected with
  data, yellow = connected/connecting but no data, red = no good
  connection). Driven by new PskReporterClient connection-state API.
- Band-condition pills now snap to the bottom-left corner — dropped the
  near-invisible palette(mid) 'HF bands:' title that was pushing them off
  the corner; day/night context moved into each pill's tooltip.
- Removed the 'best SNR' stat from the top-right line (kept count, bands,
  farthest).
- Instant hover card: QGeoView's mapMouseMove doesn't fire for plain
  hovering (inner QGraphicsView viewport consumes move events), so the
  hover card never showed once the delayed tooltip was disabled. MapView
  now event-filters the viewport's MouseMove and shows the card
  immediately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…istent hover card, orange SNR

- Opening the window now always does a deep HTTP backfill, even in Live
  (MQTT) mode: start() resets lastSeqNo so the seed query fetches the full
  lookback window instead of an incremental (empty) query left over from a
  prior session. Previous spots now populate immediately on open.
- Connection bar shows 'MQTT'/'HTTP' in the normal (white) label color;
  only the status bullet is colored.
- Hover card no longer uses QToolTip (which fades on a timer). MapView now
  shows a persistent frameless child label that stays up until the mouse
  leaves the marker (or the viewport), and follows the cursor.
- SNR in the hover card is always dark orange (#ff8c00) for visibility;
  card retuned for its dark background (muted text lightened).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The HTTP query set 'Accept-Encoding: gzip' by hand, which disables Qt's
transparent decompression — so the gzipped reply was fed raw to
QXmlStreamReader and failed with 'incorrectly encoded content', parsing 0
reception reports every time (confirmed in logs). Removing the header lets
QNetworkAccessManager negotiate and decompress automatically, so the
backfill now yields real spots. MQTT live was unaffected and already
healthy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Changing Lookback fires an immediate deep HTTP backfill; spinning through
options issued several queries within seconds and tripped PSK Reporter's
rate limiter (503). The selector now persists the choice immediately but
defers the client update behind a 750ms single-shot timer, so rapid
toggling coalesces into one query.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Changing Lookback re-fired a deep HTTP query every time — even when
narrowing or revisiting a window already covered — which tripped PSK
Reporter's 503 rate limiter during quick stepping. The client now tracks
the deepest window backfilled this session and only issues a network query
when the new lookback exceeds it; narrowing/revisiting is a local display
filter (the dialog filters spots() by the current lookback). Retention
keeps spots back to the deepest fetched window so widening back is instant
and request-free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jensenpat jensenpat marked this pull request as ready for review June 17, 2026 04:20
@jensenpat jensenpat requested a review from a team as a code owner June 17, 2026 04:20

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for this, @jensenpat — really thorough work, and the writeup made review easy. I traced the main flows and the code holds up well: the deeper-only fetch gate in setLookbackSeconds, the pruneOldSpots() retention to qMax(m_lookbackSec, m_fetchedLookbackSec), and spotsUpdated → rebuildMarkers filtering display by the current lookback compose correctly, so narrowing/widening within a covered window stays local and request-free as described. Null-guarding on m_propForecast is consistent (constructor, updateBandConditions, bottom-row build), the PropForecastDetail API matches usage, settings go through the nested-JSON pskSettings()/writePskSetting() blob (Principle V), and Qt parent-ownership keeps the new m_hoverCard/timers leak-free. CI is green on all three platforms.

The three transport fixes are the high-value part and each is well-reasoned:

  • gzip header removal — correct; manually setting Accept-Encoding: gzip does suppress Qt's transparent decompression, so this is a real fix, not a guess.
  • deep backfill on (re)open — resetting m_lastSeqNo/m_fetchedLookbackSec in start() is the right place.
  • flowStartSeconds=-fetchDepth initial query — bounded well under the 24h API cap.

A couple of non-blocking notes:

  1. TLS → plain MQTT (1884 → 1883) is a transport-security downgrade, not just a CA-path fix. The diagnosis (Homebrew OpenSSL's baked-in CA path is absent on users' machines) is sound and plaintext is a defensible trade-off for public, credential-less, read-only spot data. Worth stating explicitly that the accepted consequence is the feed is now unauthenticated and unencrypted — an on-path attacker could inject bogus spots/markers. For a hobbyist map layer that's low-impact, so I'd merge as-is; just flagging it so the decision is on the record rather than implicit. (A more surgical alternative — keep TLS but point at a bundled/system CA — is more work and probably not worth it here.)

  2. Minor / optional: MapView::eventFilter uses static_cast<QMouseEvent*>(event)->pos(), which is deprecated in Qt 6 in favor of position().toPoint(). Builds clean today (no -Werror on first-party code), so purely a forward-compat nicety — take it or leave it.

Nothing blocking from me. Nice incremental hardening of the PSK Reporter path.


🤖 aethersdr-agent · cost: $7.4882 · model: claude-opus-4-8

@NF0T NF0T self-assigned this Jun 17, 2026
@NF0T NF0T enabled auto-merge (squash) June 17, 2026 16:54

@NF0T NF0T left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed independently — transport fixes, feature additions, and code conventions all checked.

Transport fixes are real and correctly diagnosed. The gzip Accept-Encoding header removal is the right fix: manually setting the header tells QNetworkAccessManager to hand decompression back to the caller, so Qt was delivering raw gzip bytes to QXmlStreamReader and silently parsing zero spots. Removing the header restores transparent decompression. The TLS→plain MQTT change is a defensible tradeoff — libssl on macOS/Windows can't resolve the Homebrew-baked CA path at runtime; for public read-only spot data the security exposure is minimal and the comment captures the reasoning. The m_lastSeqNo = -1 / m_fetchedLookbackSec = 0 reset in start() correctly ensures the HTTP seed query covers the full lookback window on first open.

Feature implementation is solid. The three-layer data architecture (fetch deep / retain qMax(m_lookbackSec, m_fetchedLookbackSec) / display current window) is well-constructed. The "deeper-only" fetch gate prevents unnecessary network hits when narrowing the window. Null guards on m_propForecast are consistent across all call sites. callsignChanged signal guards with != m_callsign before emitting — correct Qt pattern.

One minor finding (non-blocking): me->pos() in MapView::eventFilter is deprecated in Qt 6.0 in favor of position().toPoint(). Adds one entry to the warning cleanup tracking list but doesn't block merge — no -Werror on CI.

Principle V confirmed. All settings through pskSettings()/writePskSetting().

CI is 6/6 green across all platforms. All paths are Tier 3.

@NF0T NF0T merged commit 60a4862 into aethersdr:main Jun 17, 2026
6 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.

2 participants