Repository navigation
feat(gui): the PSK Reporter map is AetherMap, a CanonWindow workspace, and every close path stops its clients and an armed beacon. Principle VI. - #6340
Conversation
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
ten9876
left a comment
There was a problem hiding this comment.
Issue fit: #6239 explicitly selects AetherMap as a canon workspace; this incremental PR implements that selection and fixes the hidden-window shutdown path. Reviewed head 6ed096b2f against its declared base 8f53d2b76 (#6339). The rename, header, scoped styling, geometry-key retention, and move from closeEvent() to done() match the stated intent. Actual hardware unkeying was not tested.
Scope
| File/group | Claimed? | Verdict |
|---|---|---|
| PskReporterMapDialog.cpp/.h | Yes | Workspace styling and shared shutdown path |
| MainWindow_DigitalModes.cpp / MainWindow_Menus.cpp | Yes | Remove legacy chrome wiring and rename action |
| Three style guides | Yes | Document approved canon surface |
| Ten user-guide pages | Yes | Rename links/menu paths and describe closing |
One signed incremental commit; all changed files belong to the disclosed scope. No new settings key, protocol surface, dependency, workflow, test, or transmit-initiation path. Stable snapshots are untouched. The docs checklist has the screenshot gap below.
Blockers
None found.
Nits — non-blocking
-
Repair the screenshot recipe and refresh/disclose stale captures.
docs/user/screens.json:3822still invokesPSK Reporter...;src/gui/MainWindow_Menus.cpp:1327now registersAetherMap.... Reproduced through the bridge:{"error":"widget or menu action not found: PSK Reporter...","ok":false} {"action":"trigger","class":"QAction","ok":true,"target":"AetherMap...","text":"AetherMap..."}Replace the recipe target with:
"target": "AetherMap...",
Update the manifest caption at
docs/user/screens.json:3838too; otherwise embedding can restore the old wording. Refresh the sidebar/Tools screenshots or identify the stale captures in the PR body, as AGENTS.md's user-documentation workflow requests. -
Retain regression coverage for the shutdown path.
src/gui/PskReporterMapDialog.cpp:2894has no committed test of its new cleanup placement. The throwaway harness is useful evidence, but cannot prevent a future move back tocloseEvent(). Preserve a behavioral test of the actual cleanup seam, using injected client/beacon state or cleanup callbacks, with no synthetic firmware peer or transmitter. Test Escape, Ctrl+W, corner close, and reopen, and mutation-check removal of cleanup. This is a non-blocking follow-up to the demonstrated fix, not a request to transmit or add network peers.
Evidence and attacks
- Exact head builds on Linux/Qt 6.12. All four claimed tests pass:
canon_window_test,modem_chrome_test,window_show_state_test,theme_manager_test. - Independently linked a throwaway probe against the app objects. It calls the actual dialog's cleanup with inert injected client-running and beacon flags; show-time transport startup is suppressed, and a socket shim rejects all opens. Escape, Ctrl+W,
close(),reject(), and the corner button all yieldvisible=0 client=0 global=0 started=0 armed=0 transmitting=0 clean=1. Removing thedone()cleanup yields running/armed flags still set andclean=0on all five paths. There was one rejected incidental socket attempt; no socket opened. This proves local cleanup dispatch, not real producer ownership or radio unkeying. - Drove exact head through explicit socket
aethersdr-pr6340-review, fresh scratch settings, offscreen, TX disabled, and onlyDEMO-0001. Renamed action opens AetherMap; whole-window/sidebar captures render; close/reopen works. An 850×650 resize survives process restart with the same geometry key. All owned review app instances were closed. - Pending lookback debounce cannot restart stopped clients:
PskReporterClient::setLookbackSeconds()gates polling onm_running. Source audit also preserved destructor cancellation, reentrancy guards, and generator ownership checks. No live radio, WSPR transmission, native taskbar/Wayland edge behavior, or macOS/Windows build was exercised. - Engine-boundary, capability-record, command-plane checks retain their existing baselines; touchpoint manifest and generated references are current. No
code-reviewskill is available; the manual audit was supplemented by independent lifecycle and canon/scope passes.
Recommendation
Approve with nits. The close-path fix survived state-injection and mutation checks; no incremental correctness or scope blocker was found. This remains stacked on #6339: land that base, rebase/retarget to main, then obtain required build, check-windows, and check-macos results. Current head has passing Docs and Static Checks only because ci.yml restricts PR builds to main. Those runs were created at 05:42:03 UTC, after current main's 02:35:53 UTC tip; there is no current timing gap, but they validate the stacked merge rather than the eventual main candidate.
There was a problem hiding this comment.
1. Issue fit
Partially — the code half is solid; the docs half leaves one committed artefact behind.
#6239's "Picked" list carries the bullet this PR implements ("AetherMap (PskReporterMapDialog, formerly 'PSK Reporter'), a workspace; every close path now cancels an armed WSPR beacon"), and the diff maps to it item for item: CanonWindow(…, Kind::Workspace) at src/gui/PskReporterMapDialog.cpp:454, the geometry key reused at :466, FramelessResizer::install() at :469, the canon scope at :464, and the teardown moved from closeEvent() to done() at :2893.
The close-path claim is real, not theoretical. tests/canon_window_test.cpp:125-152 already pins that Escape on a CanonWindow goes through QDialog::reject() and hides the window without a close event — the test comment says so in as many words — so on main Escape did leave both PSK Reporter clients polling and an armed beacon armed behind a hidden window. The new funnel is complete: m_close → QDialog::close (CanonWindow.cpp:81) → closeEvent → reject() → done(); the Ctrl+W/⌘W shortcut → reject() directly (CanonWindow.cpp:86); Escape → QDialog::keyPressEvent → reject(). I grepped the dialog for internal accept()/reject() calls that could now fire the teardown unexpectedly — there are none.
What is not addressed: docs/user/screens.json, which the rename invalidates in three ways (blocker 1).
2. Scope
| File / group | What it changes | Claimed in body? | Verdict |
|---|---|---|---|
docs/style/{aethersdr-style-guide,dialog-patterns,theme-style-guide}.md |
Adds AetherMap to the canon-window and canon/* scope lists |
Yes | In scope |
docs/user/docs/psk-reporter-map.md |
Title/description → AetherMap, new chrome paragraph, caption reworded | Yes | In scope — but the caption edit is what breaks the embed check (blocker 1) |
docs/user/docs/{aetherclock-and-gps,before-you-transmit,docs-style-guide,index,menu-reference,spothub,troubleshooting,tx-controls,wsjt-x-integration}.md |
Link text and menu-path renames | Yes | In scope |
src/gui/MainWindow_Menus.cpp:1327 |
Menu label → AetherMap... |
Yes | In scope. The action is re-parented to Tools at :1563, so the docs' Tools → AetherMap… is correct |
src/gui/MainWindow_DigitalModes.cpp:1631-1635 |
Drops setFramelessMode() and trackPersistentDialog() |
Yes | In scope and correct — m_persistentDialogs is used for nothing but frameless propagation (MainWindow.cpp:10202), which CanonWindow deliberately ignores |
src/gui/PskReporterMapDialog.{h,cpp} |
Base swap, canon sheet, statusHex(), done() override, min size 480→520, sidebar objectName |
Yes | In scope |
No unrelated files, no CHANGELOG.md entry, no new AppSettings keys (the existing PskReporterMapGeometry is reused), no new public/protocol surface. Nothing in the diff is unexplained by the issue.
One governance item for the maintainer rather than a blocker: the product rename of a long-standing window is the one user-facing piece whose authority I could not locate. #6239's thread has no maintainer comment naming AetherMap — the AetherRX/AetherTX entries say "Picked by the maintainer" in prose, while the AetherMap bullet lives in the tracker body you maintain and the issue's updated_at (2026-10-10T05:42:16Z) is the minute this PR opened. The CanonWindow conversion itself follows the #6334/#6339 precedent cleanly; it is specifically "PSK Reporter" → "AetherMap" across the menu, the window title and the docs page title that I think wants a one-line maintainer confirmation.
3. Blockers
1. docs/user/screens.json was not updated for the rename — the embed check now fails, and the capture recipe for psk-reporter-sidebar can no longer run. (docs/user/docs/psk-reporter-map.md:23, docs/user/screens.json:3822, :3838, :3839)
Three separate consequences, one root cause:
-
The embed check fails. The PR hand-edits the caption on the page:
*AetherMap's sidebar: WSPR beacon, report filters, band conditions and map options. The grid square is blacked out.*while
screens.json:3838still reads"The PSK Reporter window's sidebar: …".tools/docs/embed_screenshots.pywrites each caption from screens.json and is idempotent, so the next run reverts the hand edit. Run on your head checkout:$ python3 tools/docs/embed_screenshots.py --check out of step: docs/user/docs/psk-reporter-map.md # exit 1and on
main:screenshots are embedded (119 shots), exit 0. So this is PR-introduced.AGENTS.md:185-189is the rule: "Screenshots (docs/user/screens.json): if your UI change makes a shot stale, re-shoot it withtools/docs/capture_screenshots.py --only <id>… then place it withtools/docs/embed_screenshots.py(--checkreports a page out of step), or say in the PR which shot is stale." The body does neither, and ticks "Documentation updated". Note--checkis in no workflow (onlyAGENTS.mdanddocs/user/README.mdcite it), so the green Build docs site does not cover it — that is why I am naming it rather than letting CI do it. -
The capture recipe is un-runnable.
screens.json:3822:{ "cmd": "invoke", "target": "PSK Reporter...", "action": "trigger" }AutomationServer::doInvokefalls through toresolveMenuBarAction(src/core/AutomationServer.cpp:1203), which matches the action by its display text;pskMapActionhas noobjectName, so afterMainWindow_Menus.cpp:1327this target resolves to nothing and the step fails with "widget or menu action not found". Re-shooting this page is blocked until it is updated. -
The alt text is now wrong (
screens.json:3839, embedded atpsk-reporter-map.md:20): "PSK Reporter sidebar…" for a window whose title is AetherMap.
Mechanical fix in docs/user/screens.json, entry psk-reporter-sidebar:
{
"cmd": "invoke",
"target": "AetherMap...",
"action": "trigger"
},plus "caption": "AetherMap's sidebar: WSPR beacon, report filters, band conditions and map options. The grid square is blacked out." and "alt": "AetherMap sidebar. WSPR beacon: …" (the rest of the alt unchanged), then python3 tools/docs/embed_screenshots.py to place them. Both shots (psk-reporter-sidebar, menu-tools) are also visually stale now — the window has lost its title bar and the menu label changed — so either re-shoot them or say so in the body, per the AGENTS.md sentence above.
4. Nits (non-blocking)
docs/user/docs/menu-reference.md:64— the Tools-menu screenshot alt still listsPSK Reporter...among the items (it comes fromscreens.json:4064). Same root cause as blocker 1; worth fixing in the same pass since it is a11y text.src/gui/PskReporterMapDialog.cpp:1084—controls->setObjectName(QStringLiteral("pskReporterSidebarContent"));is fair enough as a bridge handle (the body says you grabbed it), butscreens.json:3831-3836still crops the union ofpskReporterSidebar(theQScrollArea, named at:894). If the new name is the one the shot should use, point the crop at it; otherwise a word in the body on why both exist.src/gui/PskReporterMapDialog.cpp:2474-2482—statusHex()bakes the dot colour into rich text, andupdateConnectionIndicator()is only reached from client-status signals (:1434-1437,:2446,:2845).applyCanonSheet()rebuilds the sheet onthemeChanged, but the dot keeps the old hex until the next status change. Self-heals within a poll cycle; athemeChanged→updateConnectionIndicatorconnection would make it immediate.src/gui/PskReporterMapDialog.cpp:197-349—kAetherMapStyleis file-local, so nothing can do to it whatmodem_chrome_test'severyThemeTokenInTheSheetResolves()does forModemChrome: an unresolved{{token}}in a canon sheet fails silently. I checked by hand instead — all 11{{color.canon.*}}placeholders plus the three status tokens (color.accent.danger/warning/success) resolve in bothdefault-dark.jsonanddefault-light.json. If this sheet grows, the ModemChrome shape earns its keep.src/gui/PskReporterMapDialog.cpp:454—CanonWindow(tr("AetherMap"), …): a brand name intr()invites translators to translate it. AetherModem usesQStringLiteral("AetherModem")(Ax25HfPacketDecodeDialog.cpp:885).src/gui/PskReporterMapDialog.cpp:4—ThemeManageris used seven times but only reaches the TU throughCanonIndicators.h; a direct#include "core/ThemeManager.h"would survive that header changing.- Not a code nit, just a status note for the maintainer: the base is
canon-aether-modem, andci.ymlispull_request: branches: [main], so no build or test lane has run for6ed096b— the four green checks are Static checks, Build docs site, Sanitizer-configure and a skipped PDF job. The compile evidence is the author's local build until this is retargeted tomain.
5. What I tried to break
- The "every close path" claim, including the paths you did not list.
done()is reached by the close button, the WM, Escape and Ctrl+W, as traced above. The two paths that skipdone()are plainhide()and destruction — nothing inMainWindowhides this dialog (only show/raise/activate atMainWindow_DigitalModes.cpp:1636-1638), and~PskReporterMapDialog()at:2011still cancels a valid/armed/transmitting beacon, so quit-without-close stays Principle VI-clean. Re-show after adone()works:showEventseesm_started == falseand callsrestartClients(). - The sibling-left-behind check your own premise implies. If Escape bypassing
closeEvent()bites oneCanonWindow, it bites them all — so I greppedcloseEventinAx25HfPacketDecodeDialog.cpp,AetherRxDialog.cpp,AetherialAudioStrip.cpp,NetworkDiagnosticsDialog.cppandWaveformsDialog.cpp. None of them overrides it, so AetherModem's KISS server and shim thread are not sitting on the same defect. No sibling note to file. - "Saved positions carry over." Holds:
PersistentDialogwrites base64saveGeometry(), andCanonWindow::restoreGeometryFromSettings()falls back torestoreGeometry(QByteArray::fromBase64(...))when the value is not four comma-separated ints. The newsetMinimumSize(720, 520)cannot clip a restored size either — the comma path doesexpandedTo(minimumSize()). - Lifetime and the untracking. Neither base sets
WA_DeleteOnClose, andMainWindowholds aQPointerand reuses the instance (MainWindow.h:1687), so droppingtrackPersistentDialog()changes nothing but frameless propagation, whichCanonWindowignores by design. Them_persistentDialogswalk atMainWindow.cpp:10202is the list's only consumer. - The theme claim, against my own prior. Scoped canon tokens are nested in the theme JSON and a naive grep reports them missing, so I walked the documents: all 14 tokens the new sheet and
statusHex()use resolve in dark and light, andcanon/aetherMapneeds no registry edit —theme::setContainerauto-registers the path (src/core/ThemeManager.cpp:2164). - CodeGuard's 20 findings on these files (security-audit (CodeGuard) pass): all false positives, none PR-introduced. Every CMDI-BACKTICK hit is a markdown backtick quoting a radio verb inside a comment or a bridge error string; the CMDI-AMP hit at
MainWindow_Menus.cpp:1631is a C++ reference declarator; the SSRF/PII hits are example IPs in comments andQLineEditplaceholders (192.168.1.40,you@example.com); CG-PATH-001 is three constant install-layout probes offapplicationDirPath(); CS-VLN-PATH-UNC is\\x20-\\x7Bregex escapes in — of all things — an input validator.check_a11y.pyon the changed file reports one warning, at:638, in code this PR does not touch. - Could not check, and why: nothing was built or run here, so every finding above is reasoned from the head checkout at
6ed096brather than reproduced at runtime — the one exception isembed_screenshots.py --check, which I actually executed on both your head andmain(exit 1 vs exit 0). I cannot drive the GUI, so the Escape-with-a-keyed-beacon case your body marks "not exercised" stays unexercised on my side too; the structural argument above is as far as I can take it. No compile lane has run for this SHA (see the last nit).
6. Recommendation
Request changes — on the docs artefact only, and it is a few lines. The C++ is the strongest part of this PR: the done() funnel is the right seam, it is genuinely a Principle VI fix rather than a cosmetic one, and the conversion matches the AetherModem recipe closely enough that I could diff them line by line. Update the psk-reporter-sidebar entry in docs/user/screens.json (invoke target, caption, alt), run embed_screenshots.py, and say which shots are stale; then this is ready. Separately, a one-line maintainer confirmation on the "PSK Reporter" → "AetherMap" rename would close the only authority gap I found — the conversion is picked, the rename I could only trace to your own tracker body.
Thanks for the close-path analysis in the body — naming the exact Qt mechanism (reject() hides without a close event) and pinning it to a harness result is what made this one fast to verify.
🤖 aethersdr-agent · cost: $13.0269 · model: claude-opus-5
|
Cisco CodeGuard — static analysis of this PR (26 finding(s))
Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them. 🤖 aethersdr-agent · cost: $13.1843 · model: claude-opus-5 |
…, and every close path stops its clients and an armed beacon. Principle VI. The PSK Reporter map follows About's recipe for tool windows, as AetherRX, AetherTX and AetherModem do: - It's retitled AetherMap, in the window title, the canon header and the menu item. - CanonWindow chrome: no title bar, the ambient ground and the corner close button. Drag any empty area to move it, resize it from every edge, and close it with Escape or Ctrl+W. - Kind::Workspace: an independent top-level window with its own taskbar entry. - Geometry persists under its old key (PskReporterMapGeometry). - The minimum height grows from 480 to 520 px for the header. It sits under canon/aetherMap with a canon sheet in Network Diagnostics' vocabulary: - each sidebar section is a raised card, fields sit on control, and selection and focus are cyan; - check boxes use the canon indicators; - the connection dot uses the status tokens. The band-condition pills keep the palette they share with the Propagation Dashboard, and the map's hover card keeps its own colours. Closing used to work only through closeEvent(), which stopped the PSK Reporter clients and the weather radar and cancelled an armed or transmitting WSPR beacon. Escape goes through QDialog::reject(), which hides the window without a close event, so pressing it left the clients polling and a beacon armed behind a hidden window. That logic now lives in done(), which every close path reaches. A throwaway harness linked against the app's objects shows a client running after Escape without the change, and none with it. The sidebar's content widget is named pskReporterSidebarContent so the automation bridge can reach it. The user docs call the window AetherMap, at the same URL. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
6ed096b to
81ce6c3
Compare
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
…d, and a held beacon request counts as armed. Principle VI. Review of #6340. - MainWindow::closeEvent() now closes a visible AetherMap, as it already closes the floating applet panel. Its done() then cancels a WSPR beacon and stops its clients while the radio is still connected and RadioModel alive. Before, Qt closed the independent window only after MainWindow's close handler had disconnected the radio. A temporary log line in done(), run with AetherMap open, showed the order. With the change: close_event begin, then done(), then radio.disconnect. Without it: radio.disconnect, close_event end, then done(). - done() uses the destructor's test, so a held TX request counts, armed or not. done() is public, as QDialog's is. - The connection dot re-reads its colour on a theme change. - The canon sheet gives combo popups a raised panel. - The window title and header use QStringLiteral("AetherMap"): a product name is not for translation. - docs/user/screens.json: the sidebar shot's capture recipe invokes "AetherMap...", and its caption, alt text and the Tools-menu alt use the new name. embed_screenshots.py placed them, and --check passes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Addressed in c68e724. @aethersdr-agent: blocker,
The two stale shots, Rename authority. The maintainer chose it in the session that produced this PR, picking "PSK Reporter map, retitle AetherMap" over "keep title". The #6239 bullet records that choice. Nits, by @aethersdr-agent:
@ten9876 nit 2: committed close-path test. The maintainer decided against a committed test. Constructing the map needs most of the desktop sources, and no test target links them. Your probe and my harness both show the cleanup on every path. Automated
Accessibility bot (line 638) and CodeGuard. The a11y warning is pre-existing on code this PR doesn't change. The CodeGuard hits were refuted in the agent review above. Verified at c68e724:
|
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
There was a problem hiding this comment.
Issue fit
Yes. #6239 names this item explicitly — "AetherMap (PskReporterMapDialog, formerly 'PSK Reporter'), a workspace; every close path now cancels an armed WSPR beacon (#6340, stacked on #6339)" — and #6339 has merged (2eb01f48a on main), so the base is clean. Both halves land: the canon migration, and the Principle VI close-path fix.
The close-path claim holds under inspection, and it is the stronger half of the PR. CanonWindow derives QDialog (src/gui/CanonWindow.h:28), and QDialog::closeEvent() calls reject(), so the ✕ button (CanonWindow.cpp:81, QDialog::close), the window manager, Escape (QDialog::keyPressEvent) and the Ctrl+W shortcut (CanonWindow.cpp:86, QDialog::reject) all converge on the virtual done(int). The single override at src/gui/PskReporterMapDialog.cpp:2898 genuinely covers all four, and the pre-existing closeEvent() genuinely did not cover Escape. The bug the body describes on main is real.
Scope
| File / group | What it changes | Claimed? | Verdict |
|---|---|---|---|
src/gui/PskReporterMapDialog.{h,cpp} |
PersistentDialog → CanonWindow::Kind::Workspace, canon sheet, FramelessResizer, canon header, status-dot tokens, closeEvent → done(int), min height 480→520 |
Yes, every item | In scope |
src/gui/MainWindow_DigitalModes.cpp:1631-1638 |
drops setFramelessMode() and trackPersistentDialog() |
Yes (implied by the chrome bullet) | In scope — see "what I tried to break" for why nothing is lost |
src/gui/MainWindow.cpp:4302-4310 |
reject a visible AetherMap before the radio disconnect | Yes | In scope |
src/gui/MainWindow_Menus.cpp:1327 |
menu label rename | Yes | In scope; see nit 3 on naming authority |
docs/user/docs/*.md, docs/user/screens.json |
page title, link text, menu paths, capture recipe, alt/caption | Yes | In scope, and complete — all 8 cross-page links updated, versioned_docs/version-stable correctly untouched, slug /psk-reporter-map kept so no link breaks |
docs/style/{aethersdr-style-guide,dialog-patterns,theme-style-guide}.md |
add AetherMap to the canon-window lists and the scope list | Yes | In scope |
src/gui/PskReporterMapDialog.cpp:1085 |
new pskReporterSidebarContent objectName |
Yes | In scope but unreferenced — nit 1 |
Nothing in the diff is unexplained. No new settings key (the existing PskReporterMapGeometry is reused — the checklist's claim is accurate), no new protocol or wire surface, no CHANGELOG.md entry, no deleted guard. The only removals are the two PersistentDialog wiring calls, and both are correct to remove.
Socket tests: none added, modified or removed. No QTcpServer/QUdpSocket/QLocalServer/bind/listen/connectToHost and no Fake* peer appears anywhere in this diff; no test file is touched at all.
Security audit (security-audit subagent, CodeGuard rules): no findings. All 41 CodeGuard hits were confirmed as false positives or pre-existing-and-untouched, per category:
CS-VLN-CMDI-AMP— the only&&in a string literal is Qt's mnemonic escape atsrc/gui/PskReporterMapDialog.cpp:998(tr("Sources && licenses")); the rest are C++ logical-and. No shell is invoked from any changed file.CS-VLN-CMDI-BACKTICK— markdown code-quoting inside//comments (e.g.MainWindow.cpp:1279,:1803,:6145). The one process launch in the file, the UI-scale relaunch atMainWindow.cpp:9914-9921, is argument-listQProcess::startDetachedwithapplicationFilePath()and this process's own argv — and is untouched by this PR, whose soleMainWindow.cpphunk is thereject()at 4308.CS-VLN-PATH-PROC/CG-PATH-001— fixed literals (/proc/self/status, a constantsignal_classifier.onnxjoined toapplicationDirPath()); no caller-supplied segment; present onmain.CS-VLN-SSRF-LOCALHOST/CS-PII-IP/CS-PII-EMAIL— screenshot alt text indocs/user/screens.json(127.0.0.1for the built-in simulator,you@example.comwhich is RFC 2606 reserved, an elidedtskey-auth-…placeholder). Never fetched or parsed; real data is covered by the entry'sredactrectangles; all flagged lines exist verbatim onmain.CS-INJ-homoglyph/CS-INJ-role_override— ordinary typography (—,°,Météo-France) in comments andtr()strings, and the C++overridekeyword / the "Override Colors" settings string. Every PR-added region (kAetherMapStyle201-341, the constructor head,done(int), the menu label) is pure ASCII.- The new
kAetherMapStyleis aconstexpr const char*never concatenated with runtime data;{{token}}substitution inThemeManager::resolveFor()draws only from theme JSON, which is already same-trust-level as the binary. No new trust boundary.
Blockers
None.
Nits (non-blocking)
-
1.
src/gui/PskReporterMapDialog.cpp:1085— the new objectName is referenced nowhere.controls->setObjectName(QStringLiteral("pskReporterSidebarContent"));
The body says this is "so the automation bridge can reach it", but a repo-wide grep finds this line and nothing else, and the committed recipe still crops on the old scroll-area name (
docs/user/screens.json,psk-reporter-sidebar→"crop": {"union": ["pskReporterSidebar"]}, set atPskReporterMapDialog.cpp:895). I take it this is forward-looking for the recapture you flagged as pending — worth either pointing the crop at the new name here, or a half-line saying it's staged for the recapture, so a later reader doesn't read it as dead. -
2. Accessible names still say "PSK Reporter".
PskReporterMapDialog.cpp:481tr("PSK Reporter filters"),:486tr("PSK Reporter map callsign"),:503tr("Show all PSK Reporter callsigns"),:539tr("Show active PSK Reporter monitors"),:550tr("Show PSK Reporter as a globe"). Several are fine as-is — the data source is PSK Reporter, and the docs page keeps "AetherMap, the PSK Reporter map".:550is the one that now reads oddest to a screen-reader user ("Show PSK Reporter as a globe" inside a window announced as AetherMap). Entirely your call. -
3. Naming authority for the user-visible rename. The only authority I can find for the string "AetherMap" is the #6239 checklist line, which you authored and updated the same day as this PR;
list_issue_comments(6239)and the RFC #6226 approval carry maintainer rulings on the canon migration and on individual windows, but not on this name. I'm raising it only because §6 of my brief makes any user-visible rename a maintainer call — it is clearly consistent with the merged AetherRX / AetherTX (#6334) and AetherModem (#6339) precedent, so this is a confirmation item, not an objection. -
4. No committed test for the close path.
psk_reporter_map_behavior_testlinksaethercore Qt6::Coreonly (tests/tests.cmake:2519-2525), so it never constructs the dialog; the throwaway QtTest harness in your test plan is the real evidence and isn't committed. Your stated reason — no test target links the desktop sources — matches what I see, so I'm not asking for a synthetic stand-in. Classifying as a nit, not a blocker: a deterministic seam does exist (construct offscreen,reject(), assert the clients stopped) but no canon rule makes it merge-gating here.
What I tried to break
- The destructor double-stop. The whole premise of
c68e724is thatdone()runs whilem_radioModelis alive, since it's a value member ofMainWindowand so is destroyed before~QObjectdeletes the child dialog — making the dialog's destructor see a dangling pointer. I checked whetherdone()leaves the destructor's identical test true, which would re-enterstopBeacon()on the dangling model and defeat the fix. It doesn't:stopBeacon()(PskReporterMapDialog.cpp:~2130) doesstd::exchange(m_beaconRequest, {})and clearsm_beaconArmed/m_beaconTransmitting, so~PskReporterMapDialog()'s guard at:2017is false on the second pass. The ordering fix is real and the paths don't collide. - A cancellable quit. If
MainWindow::closeEvent()couldevent->ignore()after line 4308, a cancelled quit would leave the map closed behind it. It can't — I read the whole function fromMainWindow.cpp:4182and there is noignore()or early return anywhere in it. I also confirmed File → Quit routes throughclose()(MainWindow_Menus.cpp:335) rather thanqApp->quit(), so "every close path" survives that hole, and that thereject()at 4308 does precede the radio disconnect (~4389), as the body claims. - What
trackPersistentDialog()was for. Dropping it atMainWindow_DigitalModes.cpp:1634could have silently removed the map from a disconnect sweep or a minimal-mode walk. It couldn't:m_persistentDialogshas exactly one consumer, the frameless-propagation loop insetFramelessWindow()atMainWindow.cpp:10211-10218, which only callssetFramelessMode(on)(#2781). Since the window is now always frameless, losing tracking loses nothing — and the docs change saying "View → Frameless Window does not apply to it" is accurate. - Geometry migration. The comment and docs both claim saved positions carry over under the reused key, but the two bases store different formats —
PersistentDialog::saveGeometryToSettings()writessaveGeometry().toBase64()(PersistentDialog.cpp:137) whileCanonWindowwrites"x,y,w,h". The claim holds anyway:CanonWindow::restoreGeometryFromSettings()falls through its 4-part parse torestoreGeometry(QByteArray::fromBase64(...)), so an existing base64 value is read on first open and rewritten in the new format. - Missing theme tokens.
statusHex()resolvescolor.accent.{danger,warning,success}; if any were absent the connection dot would silently paint#000000. I walked bothresources/themes/default-{dark,light}.jsonrather than grepping (nested tokens don't grep): all three resolve at root ({color.red.500}/{color.amber.500}/{color.green.500}) and thecanonscope doesn't shadow them. Likewisecanon/aetherMaphas no sub-scope in the theme JSON — but neither doaetherRx/aetherTx/aetherModem, so it resolves tocanonexactly as the three merged migrations already do. - The menu move.
PskReporterMapDialog's action is added toviewMenu(MainWindow_Menus.cpp:1327), which initially looked like it contradicted every doc line saying Tools → AetherMap…. It doesn't —toolsMenu->addAction(pskMapAction)at:1563re-adds the sameQAction, so it appears under both andmenu-reference.mdis correct. - A minimized window at quit. The
isVisible()guard atMainWindow.cpp:4308would skip a minimized map ifisVisible()went false on minimize. It doesn't in Qt —isVisible()stays true unless the widget is explicitly hidden — so a minimized AetherMap with an armed beacon is still rejected on quit. - Docs completeness. Checked every in-tree reference: all 8 cross-page links in
docs/user/docs/are renamed,docs/user/sidebars.jskeys off the unchanged slug, and the 10 remaining "PSK Reporter Map" hits are all inversioned_docs/version-stable, which correctly describes the shipped release.
Could not check: nothing runtime. I'm headless — no build, no test run, no way to drive the GUI — so every statement above is reasoned from the code at the PR head, not reproduced. In particular your two quit-ordering and Esc-harness results, and the offscreen render, I can only say are consistent with the code I read; I can't reproduce them. CI at the time of writing: check-windows ✅, Build docs site ✅, Static checks ✅, Sanitizer option configures ✅, CodeQL ✅ — build and check-macos still in progress, so this is not a green suite yet, and note that ctest in ci.yml is -R-filtered regardless.
Recommendation
Approve with nits — pending the maintainer's nod on the name (nit 3) and build / check-macos finishing.
This is a clean migration, and the close-path half is a genuine Principle VI fix rather than cosmetic churn: Escape really did leave the clients polling and a beacon armed behind a hidden window on main, and routing the teardown through done(int) is the right seam — it's the one point all four close paths pass through, and widening the test to m_beaconRequest.valid() closes the held-request gap the destructor already knew about. I went looking for the two ways this kind of change usually breaks — a destructor re-entry on a dangling model, and lost PersistentDialog wiring — and both are clear. Next step: confirm the "AetherMap" string, then let the two in-flight jobs land. Thanks for the detailed test plan; the quit-ordering probe in particular made the ordering claim checkable rather than something I had to take on faith.
🤖 aethersdr-agent · cost: $13.0159 · model: claude-opus-5
|
Cisco CodeGuard — static analysis of this PR (46 finding(s))
Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them. 🤖 aethersdr-agent · cost: $14.9620 · model: claude-opus-5 |
… palette. Principle VIII. (#6341) Builds on #6339 and #6340, both merged. ## Summary Part of #6239. **Connect to Radio** becomes a `CanonWindow`, following the About window's recipe as AetherRX, AetherTX (#6334), AetherModem (#6339) and AetherMap (#6340) do: - **Chrome:** no title bar, the ambient ground, a canon header ("Connect to a Radio", replacing the in-body heading) and the corner close button. Drag any empty area to move it, resize it from every edge, and close it with Esc or Ctrl+W. - **Kind:** still a parented dialog (`Kind::Dialog`). MainWindow still anchors it above the status bar; the new `setOwnerPlaced()` stops CanonWindow from centering it on first show. - **Return:** the new `setReturnClicksDefault(false)` means Return in a field (an IP address, a SmartLink password) only does what that field does. Without it, QDialog would also press the first auto-default button. - **Frameless mode:** the window no longer follows **View → Frameless Window**. `setFramelessMode()`, the frameless title bar, the opaque `paintEvent()` and the framed branch of `screenFitFrameMargins()` are removed. The #4515 screen fitting and the #4725 xcb re-show path are unchanged. **Look:** it sits under the `canon/connection` theme scope, with a canon sheet in Network Diagnostics' vocabulary: - groups are raised cards with muted titles, the radio list is nested and fields sit on control; - mode cards are raised, and the selected one has a cyan edge; - selection and focus are cyan, and check boxes use the canon indicators; - hint, info and error labels use canon muted, canon cyan and `accent.danger`. **CanonWindow:** - gains `setReturnClicksDefault()` (the Workspace kind's Return handling, now a setting a dialog can turn off) and `setOwnerPlaced()`; - now owns the header helper `makeCanonHeader()`, moved from StagePage, so every canon window can reach it without the stage-page sources. **Touchpoint manifest:** StagePage drops its now-unused ThemeManager include, so the `ThemeManager.h` includer count goes from 173 to 172. There are no new touchpoints. **Docs:** - `first-connection.md` describes the window. - `docs/style/*` list Connect to Radio among the canon windows, and `dialog-patterns.md` documents the two new options. - **Stale screenshot, to recapture after merge:** `connect-dialog.png` (it still shows the old title bar and heading). ## Constitution principle honored Principle VIII (Evidence Over Assertion): the checks below, each mutation-verified. The visual direction is RFC #6226's. ## Test plan - [x] Local build passes - [x] `canon_window_test`: a dialog with Return opted out ignores Return in a field, and an owner-placed window keeps its position on first show. Each check fails when its option is removed. - [x] `connection_panel_size_test`: the framed cases are dropped, since the window has no frame any more. The growth check now settles the layout over two event-loop turns, because the body sits one layout deeper. It still fails with `fitToScreen()`'s growth branch disabled. - [x] `startup_autoconnect_lockout_test` passes, now linking `CanonWindow.cpp` instead of `FramelessWindowTitleBar.cpp`. - [x] `startup_autoconnect_lockout_test` also checks that attaching an already signed-in SmartLink client replaces the sign-in hint with the account. No socket is opened. The check fails when the account line's state is read back from its stylesheet, as it was before the review fix. - [x] The static-check scripts pass locally, and the touchpoint manifest is regenerated. - [x] Built locally for manual testing: the window opens above the status bar; Esc, Ctrl+W and ✕ close it; it can be dragged; all three pages are canon. ## Checklist - [x] Commits are signed - [x] No new `AppSettings` keys - [x] Code is clean-room - [x] All meter UI uses `MeterSmoother` (unchanged) - [x] Documentation updated: `docs/user/docs/first-connection.md`, `docs/style/*` - [x] Security-sensitive changes: n/a (the SmartLink login fields are only restyled) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…inned to the canon palette. Principle VIII. (#6364) ## Summary Part of #6239. The GPS window becomes **AetherGPS**, a `CanonWindow` workspace. It follows the recipe AetherRX and AetherTX (#6334), AetherModem (#6339) and AetherMap (#6340) use: - **Name:** AetherGPS in the window title, the canon header, the menu item (**Tools → AetherGPS…**) and the tooltip on the status-bar GPS indicator. - **Chrome:** no title bar, the ambient ground, the canon header and the corner close button. Drag any empty area to move it, resize it from any edge, and close it with Esc or Ctrl+W. The footer **Close** button is gone. - **Kind:** `CanonWindow::Kind::Workspace`, an independent top-level window with its own taskbar entry. It no longer follows **View → Frameless Window**, so `showGpsLocationDialog()` creates it directly instead of through `showOrRaisePersistent()`, which would set frameless mode on it. - **Geometry:** persists under its old key (`GpsLocationDialogGeometry`), so saved positions carry over. - **Size:** the minimum grows from 860×600 to 860×640, and the first-open size from 1080×720 to 1080×760, to make room for the header. **Look:** it sits under the `canon/aetherGps` theme scope, with a canon sheet in AetherMap's vocabulary: - the top status strip and each section are raised cards; the metric tiles, the NTP tip and the clock-sync group are nested; - section titles are cyan, values ink and field titles muted; - buttons and the NTP server field are canon controls, and check boxes use the canon indicators; - the satellite bar keeps its green fill, now the `color.background.success` token instead of the `#17663f` and `#ffffff` literals. The object name `gpsLocationDialog` is unchanged because `IcomControls.cpp` refers to it. **Docs:** - The user guide, menu reference, Radio Setup page, in-app help and the Tools-menu alt text in `docs/user/screens.json` use the new name. - The GPS page has a "Using AetherGPS" section that describes the window. - `docs/style/*` list AetherGPS among the canon windows. - **Stale screenshot, to recapture after merge:** `menu-tools.png` still shows "GPS Dashboard...". ## Constitution principle honored Principle VIII (Evidence Over Assertion) covers the checks below. The visual direction is RFC #6226's. ## Test plan - [x] Local build passes - [x] Rendered offscreen with the app on scratch settings and the radio disconnected, opened via **Tools → AetherGPS…** over the automation bridge: the canon header, the cards, the cyan section titles and the corner ✕ draw. - [x] The static-check scripts pass locally. The touchpoint manifest is unchanged. - [x] `embed_screenshots.py --check` and `gen_reference.py --check` pass. - [ ] Not exercised: the window with live GPS data, since no radio was connected. No test covers this window, and this PR adds none. ## Checklist - [x] Commits are signed - [x] No new flat-key `AppSettings` calls. The existing geometry key is reused. - [x] Code is clean-room - [x] All meter UI uses `MeterSmoother` (unchanged) - [x] Documentation updated: `docs/user/docs/aetherclock-and-gps.md` and the pages that name the window, `docs/style/*` - [x] Security-sensitive changes: n/a 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…remote operation over CGNAT (#6389) ## Summary Release prep for **v26.10.2**: the six release files plus the Stable docs snapshot. Metadata and docs only; no source, test or CI changes. Cutoff: `origin/main` at `46943eb3c` (#6083, merged 2026-10-11). Anything that merges after it is folded in before the tag. **106 merged changes** from **15 human contributors**, AetherClaude (2) and Dependabot (0). First-time contributor: **@dacacioa**. This was confirmed by checking each login for a merged PR before the previous tag, not from `author_association`. ## Changelog The range `v26.10.1..46943eb` was mapped through the commit-to-PR API (`repos/…/commits/<sha>/pulls`), not from commit subjects. It holds 106 PRs and no direct commits, and every one is cited in the section. `v26.10.1` is an ancestor of `main`, so nothing is excluded. #6083 merged before the branch was cut, so it is inside the range and needs no unmerged-PR banner. The notes lead with: - the unified 52 px title bar (#6198); - docs.aethersdr.com (#6286, #6298, #6294); - remote operation over CGNAT through the in-radio tailnet shim (#6273, #6318, #6325, #6319); - the canon windows (#6227, #6334, #6339, #6340, #6341, #6364, #6372, #6386). Then come DVK import, FFT Smooth/De-comb, TCI tune and receiver events, RTL-SDR multi-receiver delivery, HL2 and ANAN-G2 state memory, the IC-7300MK2 fixes, the first clean sanitizer runs (stated beside #6350's TSan red) and Authenticode signing. Numbers come from the PR bodies. "On upgrading" callouts cover the behavior changes the bodies flagged: - HL2 receive audio is 12.04 dB quieter (#6254); - HL2 host CW is +6.02 dB at a fixed drive (#5975); - ANAN AGC-T is remapped (#6282); - sleep prevention is on by default (#6195); - the Tailscale MTU cap has no opt-out (#6319); - IC-7300MK2 Auto SQL is withdrawn (#6184). DeepFist (#5953, #6103) is described as applying only to `ENABLE_DEEPFIST_EXPERIMENT` builds. ## Docs refresh, not just version strings - **README**: - Highlights: FFT Smooth/De-comb on the spectrum line; AetherMap naming; remote over CGNAT and TCI tune/receiver events on the SmartLink/TCI line; pre-NR Copy Assist; a new docs.aethersdr.com line. - Supported Hardware: the KPA1500, marked as not yet validated on hardware. - Other radio families: HL2 state restore extended; ANAN-G2 gains AGC-T, NB2, supply voltage/link and per-radio memory; RTL-SDR now "bundled in every official package", with WFM stereo, PPM, per-receiver SQL/meters and DAX/TCI feeds, and multi-receiver explicitly an evaluation. - Roadmap section: the CTR2 line advanced and a visual-canon line added. - Maturity labels are unchanged. - **ROADMAP**: - In flight: a v26.10.2 sentence each for aetherd, Icom, ANAN-G2, RTL-SDR (remaining is now production multi-receiver qualification and digital FM, RFC #6079), Hermes-Lite 2 and the CTR2 relay (RFC #6332 extensions). A new entry for the visual canon (RFC #6226 / #6239). - Queued: RFC #4214 (approved 2026-10-09) added. - Open RFCs, checked against `label:rfc`: #4214 leaves (approved); #4764 and #4861 leave (shipped by #6198 and #6216); #6346, #6232, #6212 and #6209 join. #6332 is approved and folds into the CTR2 entry. - Recently shipped: nine new `(v26.10.2)` entries, still within the v26.10.x cycle, so no month-rollover trim. - Backends (six: anan, flex, hl2, icom, rtl, sim) and NR engines (seven) were checked against the tree; both are unchanged. ## Judgment calls flagged for the maintainer All of these were decided through AskUserQuestion: - Version **26.10.2**, date **2026-10-11**, opened ready for review. - **#6350**, the TSan red in the weekly sanitizer lane: proceed, and name it below. - RFCs **#4764** and **#4861** come off Open RFCs as shipped. The issues themselves are left open for you to close. - RFC **#4214** goes under Queued (next cycle). - A **visual canon** In-flight entry, plus a README Roadmap line. - The **KPA1500** is listed in README Supported Hardware with "not yet validated on hardware". - Including **#6083**: you asked for it, and it merged before the cutoff, so it ships as an ordinary part of the range. ## Known failing on `main` - **`ctr2_hid_thread_port_test`** fails in the weekly TSan lane (runs of 2026-10-08 and 2026-10-10; #6350). `main`'s CI and Full Suite were green at `414082197`. The CHANGELOG headline and intro say so: the lanes achieved their first clean runs this cycle, and the test remains failing under TSan. This prep does not fix the test, and the release ships with it red in that lane. ## Scope | file | change | |---|---| | `CMakeLists.txt` | `project(AetherSDR VERSION 26.10.2)` | | `README.md` | Current version line, plus the prose refresh above | | `AGENTS.md` | Current version line | | `CHANGELOG.md` | new `[v26.10.2] — 2026-10-11` section; `[Unreleased]` stays and stays empty; history below byte-identical | | `packaging/linux/io.github.aethersdr.aethersdr.metainfo.xml` | `<release version="26.10.2" date="2026-10-11"/>` first in `<releases>` | | `ROADMAP.md` | cycle heading → post-v26.10.2, plus the prose refresh above | | `docs/user/versioned_docs/version-stable/**`, `docs/user/versioned_sidebars/version-stable-sidebars.json`, `docs/user/stable-version.json`, `docs/user/src/components/LogAnalyzer/rules.json` | docs snapshot (`tools/docs/snapshot_stable.py 26.10.2`), in its own commit. This is the first snapshot since #6286. | ## Validation Output of `check_release_prep.py --mapping mapping.json` at the branch head: 30 checks, 1 FAIL (explained below), 1 WARN (the snapshot files, listed in the scope table). ``` PASS v26.10.1 is an ancestor of origin/main PASS all six version spots say 26.10.2 PASS CHANGELOG heading date equals metainfo release date PASS appstreamcli validate PASS metainfo <release> list is date-descending with unique versions PASS `## [Unreleased]` stays and stays empty PASS `## [v26.10.2]` sits directly under `[Unreleased]` PASS the previous section is `[v26.10.1]` PASS CHANGELOG diff is exactly the inserted section PASS new section opens with a `### <headline>` PASS intro sentence has the house shape PASS new section has a `### Contributors` block PASS release_contents.py --check (citations, intro and Contributors counts) PRs in range: 106 cited: 106; intro 106/15 matches; Contributors (16 entries) matches; first-time ['dacacioa'] citations outside the range: #4764, #4861, #5468, #6192, #6226, #6271, #6332 (RFCs), #6238, #6350 (issues) PASS code fences balance / no duplicated table headers or doubled `---` (README, ROADMAP, new section) PASS every local link in README.md and ROADMAP.md resolves PASS no heading removed from README/ROADMAP is still linked elsewhere PASS Recently shipped `(vX)` markers run newest-first and lead with the new version PASS Recently shipped intro names the current cycle (v26.10.x) FAIL only the four current-version lines lose the `26.10.1` string outside CHANGELOG historical mentions must stay: "label": "26.10.1" PASS diff touches no src/, tests/, third_party/ or .github/workflows/ path PASS all six release files are in the diff WARN files beyond the six (the docs snapshot) PASS git diff --check ``` - **The one FAIL is the snapshot's version label, not a historical mention.** `docs/user/stable-version.json`'s `"label"` is the docs site's current Stable version, and `snapshot_stable.py` rewrites it by design. The validator predates #6286 and knows only the four current-version lines. Run on the six-file commit alone, the same check passes (29 checks, 0 FAIL, 0 WARN). - The docs build passes: `npm ci && npm run build` in `docs/user`. `node tools/docs/test_log_rules.mjs` reports 707 checks passed for 45 rules. The line moves it reports were already on `main`. - Four merged PRs (#6340, #6341, #6364, #6372) named screenshots to recapture after merge (`psk-reporter-sidebar.png`, `menu-tools.png`, `connect-dialog.png`, `rx-agc-calibration.png`), and #6386 named the panadapter shots (`panadapter.png` and the main-window captures, which still show the old title strip). They were not recaptured, so the Stable snapshot carries the older images. - No configure, build or test was run for this prep; it is metadata only, and CI builds it. - `main` CI at the cutoff `46943eb3c`: Docs and CodeQL - Code Quality are green; CI, Full Suite, CodeQL and the System Libraries Canary were still running when this was opened. At `414082197`, every one of them was green. After merge: tag the resulting **main** commit, not this branch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… palette. Principle VIII. (#6401) ## Summary Part of #6239. **SpotHub** (`DxClusterDialog`) moves from `PersistentDialog` to a `CanonWindow` workspace, following the recipe AetherGPS (#6364) and AetherMap (#6340) use: - **Chrome:** no title bar; the ambient ground, the canon header and the corner close button. Drag any empty area to move it, resize it from any edge, close it with Esc or Ctrl+W. - **Kind:** `CanonWindow::Kind::Workspace`, an independent top-level window with its own taskbar entry. It no longer follows **View → Frameless Window**, so the **Settings → SpotHub…** handler creates it directly instead of through `showOrRaisePersistent()`, which would set frameless mode on it. - **Geometry:** persists under its old key (`DxClusterDialogGeometry`), so saved positions carry over. - **Size:** minimum 360×600 (was 360×560) and first-open 760×680 (was 760×640), to make room for the header. **Look.** It sits under the `canon/spotHub` theme scope, which maps the tokens the tabs' remaining per-widget sheets use (status colors, labels) onto the canon palette, plus one canon sheet: - the tab pages are a raised card; the selected tab is cyan with a cyan underline; - group boxes are nested cards with cyan titles; - fields, spin boxes and combos sit on `control`; the consoles and the spot list are nested, with a canon header row; - check boxes use the canon indicators; - each source's **Connect / Start** button is the filled cyan primary; - a checked toggle (Auto-Connect, Spots, Passive, Enabled/Disabled…) keeps SpotHub's green fill, now the `color.toggle.success.*` tokens instead of literals. The maintainer chose this over the canon aqua outline, because the fill is the only on/off signal on the Display tab's name-only toggles. **Cleanup.** About 60 per-widget field, console, table, tab, checkbox and toggle sheets are replaced by that one sheet, and the remaining hex literals (swatch borders, the server and DXCC/Signal History labels) are tokens. `kSpotHubToggle` is gone. **Docs:** - `docs/user/docs/spothub.md` describes the window (move, resize, close, taskbar, Frameless Window). - `docs/style/*` list SpotHub among the canon windows and its scope. - **Stale screenshot, to recapture after merge:** `spothub.png` shows the old window. ## Constitution principle honored Principle VIII (Evidence Over Assertion) covers the checks below. The visual direction is RFC #6226's; the checked-toggle color was the maintainer's call. ## Test plan - [x] Local build passes (Qt 6.12). - [x] Full GUI on a scratch profile, opened via **Settings → SpotHub…** over the automation bridge, every tab grabbed: Cluster, WSJT-X, FreeDV, Spot List and Display draw the canon header, cards, cyan tabs and titles, canon fields and the corner ✕; checked toggles fill green. A clipped **Freeze** label (60 px fixed width) was fixed by tightening button padding, and re-grabbed. - [x] The bridge's `close` closed the window and saved `DxClusterDialogGeometry` (`570,237,760,680`); reopening restored 760×680. - [x] The static-check scripts (`check_engine_boundary`, `gen_touchpoint_manifest --check`, `check_capability_records`, `check_command_plane`) and `check_a11y.py` pass. The color-audit ratchet is 43 references and 40 `setStyleSheet` calls below `main`. - [x] `embed_screenshots.py --check` and `gen_reference.py --check` pass. - [ ] Not exercised: live spot traffic (no cluster connected) and the live radio. No test covers this window, and this PR adds none. ## Checklist - [x] Commits are signed - [x] No new flat-key `AppSettings` calls; the existing geometry key is reused - [x] Code is clean-room - [x] No meter UI touched - [x] Documentation updated: `docs/user/docs/spothub.md`, `docs/style/*`; no `CHANGELOG.md` entry - [x] Security-sensitive changes: n/a 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Builds on #6339, which has merged: AetherMap's check boxes use its canon indicators (
gui/CanonIndicators.h,applyCanonSheet(), cached throughgui/ThemedImageCache.h).Summary
Part of #6239. The PSK Reporter map becomes AetherMap, a
CanonWindowworkspace, following the About window's recipe for tool windows as AetherRX, AetherTX (#6334) and AetherModem (#6339) do:CanonWindow::Kind::Workspace, an independent top-level window with its own taskbar entry.PskReporterMapGeometry), so saved positions carry over.Look: it sits under the
canon/aetherMaptheme scope with a canon sheet in Network Diagnostics' vocabulary:The band-condition pills keep the palette they share with the Propagation Dashboard, and the map's hover card keeps its own colours.
Close-path fix (Principle VI). Closing used to work only through
closeEvent(), which stopped the PSK Reporter clients and the weather radar and cancelled an armed or transmitting WSPR beacon. Esc goes throughQDialog::reject(), which hides the window without a close event. Onmain, pressing Esc in the PSK Reporter window leaves the clients polling and a WSPR beacon armed behind a hidden window. The logic now lives indone(), which every close path reaches: the close button and the window manager (throughQDialog::closeEvent()), Esc and Ctrl+W (throughreject()), and quitting the app.MainWindow::closeEvent()now closes a visible AetherMap before it disconnects the radio; before, Qt closed it only after the disconnect.done()uses the destructor's test, so a held TX request counts too.Automation: the sidebar's content widget is named
pskReporterSidebarContent, so the automation bridge can reach it.Docs:
/psk-reporter-map).docs/user/screens.json: thepsk-reporter-sidebarcapture recipe invokesAetherMap..., and its caption, alt text and the Tools-menu alt use the new name.embed_screenshots.py --checkpasses.docs/style/*list AetherMap among the canon windows.psk-reporter-sidebar.png(old title bar and indicators) andmenu-tools.png(still shows "PSK Reporter...").versioned_docsstable copies are untouched; they describe the shipped release.Constitution principle honored
Principle VI (TX safety): every way of closing the window cancels an armed or transmitting WSPR beacon. Principle VIII (Evidence Over Assertion) covers the checks below. The visual direction is RFC #6226's.
Test plan
done(), with AetherMap open, then Quit. With the change the log readsmain_window.close_event begin→PROBE_AETHERMAP_DONE→radio.disconnect begin; without it,radio.disconnect→close_event end→PROBE_AETHERMAP_DONE.visible=0 running=0. With the stop logic skipped (the old behaviour):visible=0 running=1. The harness is not committed: constructing the window needs most of the desktop sources, which no test target links.canon_window_test,modem_chrome_test,window_show_state_testandtheme_manager_testpass locally.npm ci && npm run build), andgen_reference --checkpasses.pskReporterSidebarContent.Checklist
AppSettingscalls. The existing geometry key is reused.MeterSmoother(unchanged)docs/user/docs/psk-reporter-map.mdand the pages that name it,docs/style/*🤖 Generated with Claude Code