Skip to content

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

Merged
ten9876 merged 2 commits into
mainfrom
canon-aether-map
Oct 10, 2026
Merged

ten9876 merged 2 commits into
mainfrom
canon-aether-map

Conversation

@ten9876

@ten9876 ten9876 commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Builds on #6339, which has merged: AetherMap's check boxes use its canon indicators (gui/CanonIndicators.h, applyCanonSheet(), cached through gui/ThemedImageCache.h).

Summary

Part of #6239. The PSK Reporter map becomes AetherMap, a CanonWindow workspace, following the About window's recipe for tool windows as AetherRX, AetherTX (#6334) and AetherModem (#6339) do:

  • Name: retitled AetherMap in the window title, the canon header and the menu item (Tools → AetherMap…).
  • 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 Esc or Ctrl+W.
  • Kind: CanonWindow::Kind::Workspace, an independent top-level window with its own taskbar entry.
  • Geometry: persists under its old key (PskReporterMapGeometry), so saved positions carry over.
  • Size: the minimum grows from 720×480 to 720×520 for the header.

Look: it sits under the canon/aetherMap theme scope 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.

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 through QDialog::reject(), which hides the window without a close event. On main, pressing Esc in the PSK Reporter window leaves the clients polling and a WSPR beacon armed behind a hidden window. The logic now lives in done(), which every close path reaches: the close button and the window manager (through QDialog::closeEvent()), Esc and Ctrl+W (through reject()), 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:

  • The user-guide page is titled AetherMap, at the same URL (/psk-reporter-map).
  • The menu reference, Before you transmit, the docs style guide's menu-path example and every link to the page use the new name.
  • docs/user/screens.json: the psk-reporter-sidebar capture recipe invokes AetherMap..., and its caption, alt text and the Tools-menu alt use the new name. embed_screenshots.py --check passes.
  • docs/style/* list AetherMap among the canon windows.
  • Stale screenshots, to recapture after merge: psk-reporter-sidebar.png (old title bar and indicators) and menu-tools.png (still shows "PSK Reporter...").
  • The versioned_docs stable 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

  • Local build passes
  • Quit ordering: a temporary log line in done(), with AetherMap open, then Quit. With the change the log reads main_window.close_event begin → PROBE_AETHERMAP_DONE → radio.disconnect begin; without it, radio.disconnect → close_event end → PROBE_AETHERMAP_DONE.
  • Close path: a throwaway QtTest harness, linked against the app's objects, shows AetherMap and presses Esc. With the change: 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_test and theme_manager_test pass locally.
  • The static-check scripts pass locally. The touchpoint manifest is unchanged.
  • The user docs site builds (npm ci && npm run build), and gen_reference --check passes.
  • Rendered offscreen with the demo app on scratch settings: the window, and the full sidebar grabbed as pskReporterSidebarContent.
  • Not exercised: transmitting a WSPR beacon and then closing with Esc (the bridge keeps TX verbs gated, and no live radio was used). The harness covers the shared stop path.

Checklist

  • Commits are signed
  • No new flat-key AppSettings calls. The existing geometry key is reused.
  • Code is clean-room
  • All meter UI uses MeterSmoother (unchanged)
  • Documentation updated: docs/user/docs/psk-reporter-map.md and the pages that name it, docs/style/*
  • Security-sensitive changes: n/a

🤖 Generated with Claude Code

@ten9876
ten9876 requested a review from a team as a code owner October 10, 2026 05:42
@github-actions

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 1 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 4
Files with findings: 1
Total findings : 1

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@ten9876 ten9876 self-assigned this Oct 10, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

  1. Repair the screenshot recipe and refresh/disclose stale captures. docs/user/screens.json:3822 still invokes PSK Reporter...; src/gui/MainWindow_Menus.cpp:1327 now registers AetherMap.... 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:3838 too; 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.

  2. Retain regression coverage for the shutdown path. src/gui/PskReporterMapDialog.cpp:2894 has no committed test of its new cleanup placement. The throwaway harness is useful evidence, but cannot prevent a future move back to closeEvent(). 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 yield visible=0 client=0 global=0 started=0 armed=0 transmitting=0 clean=1. Removing the done() cleanup yields running/armed flags still set and clean=0 on 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 only DEMO-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 on m_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-review skill 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.

@ten9876 ten9876 removed their assignment Oct 10, 2026

@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.

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:3838 still reads "The PSK Reporter window's sidebar: …". tools/docs/embed_screenshots.py writes 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 1
    

    and on main: screenshots are embedded (119 shots), exit 0. So this is PR-introduced. AGENTS.md:185-189 is the rule: "Screenshots (docs/user/screens.json): if your UI change makes a shot stale, re-shoot it with tools/docs/capture_screenshots.py --only <id> … then place it with tools/docs/embed_screenshots.py (--check reports a page out of step), or say in the PR which shot is stale." The body does neither, and ticks "Documentation updated". Note --check is in no workflow (only AGENTS.md and docs/user/README.md cite 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::doInvoke falls through to resolveMenuBarAction (src/core/AutomationServer.cpp:1203), which matches the action by its display text; pskMapAction has no objectName, so after MainWindow_Menus.cpp:1327 this 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 at psk-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 lists PSK Reporter... among the items (it comes from screens.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), but screens.json:3831-3836 still crops the union of pskReporterSidebar (the QScrollArea, 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, and updateConnectionIndicator() is only reached from client-status signals (:1434-1437, :2446, :2845). applyCanonSheet() rebuilds the sheet on themeChanged, but the dot keeps the old hex until the next status change. Self-heals within a poll cycle; a themeChanged → updateConnectionIndicator connection would make it immediate.
  • src/gui/PskReporterMapDialog.cpp:197-349 — kAetherMapStyle is file-local, so nothing can do to it what modem_chrome_test's everyThemeTokenInTheSheetResolves() does for ModemChrome: 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 both default-dark.json and default-light.json. If this sheet grows, the ModemChrome shape earns its keep.
  • src/gui/PskReporterMapDialog.cpp:454 — CanonWindow(tr("AetherMap"), …): a brand name in tr() invites translators to translate it. AetherModem uses QStringLiteral("AetherModem") (Ax25HfPacketDecodeDialog.cpp:885).
  • src/gui/PskReporterMapDialog.cpp:4 — ThemeManager is used seven times but only reaches the TU through CanonIndicators.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, and ci.yml is pull_request: branches: [main], so no build or test lane has run for 6ed096b — 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 to main.

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 skip done() are plain hide() and destruction — nothing in MainWindow hides this dialog (only show/raise/activate at MainWindow_DigitalModes.cpp:1636-1638), and ~PskReporterMapDialog() at :2011 still cancels a valid/armed/transmitting beacon, so quit-without-close stays Principle VI-clean. Re-show after a done() works: showEvent sees m_started == false and calls restartClients().
  • The sibling-left-behind check your own premise implies. If Escape bypassing closeEvent() bites one CanonWindow, it bites them all — so I grepped closeEvent in Ax25HfPacketDecodeDialog.cpp, AetherRxDialog.cpp, AetherialAudioStrip.cpp, NetworkDiagnosticsDialog.cpp and WaveformsDialog.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: PersistentDialog writes base64 saveGeometry(), and CanonWindow::restoreGeometryFromSettings() falls back to restoreGeometry(QByteArray::fromBase64(...)) when the value is not four comma-separated ints. The new setMinimumSize(720, 520) cannot clip a restored size either — the comma path does expandedTo(minimumSize()).
  • Lifetime and the untracking. Neither base sets WA_DeleteOnClose, and MainWindow holds a QPointer and reuses the instance (MainWindow.h:1687), so dropping trackPersistentDialog() changes nothing but frameless propagation, which CanonWindow ignores by design. The m_persistentDialogs walk at MainWindow.cpp:10202 is 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, and canon/aetherMap needs no registry edit — theme::setContainer auto-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:1631 is a C++ reference declarator; the SSRF/PII hits are example IPs in comments and QLineEdit placeholders (192.168.1.40, you@example.com); CG-PATH-001 is three constant install-layout probes off applicationDirPath(); CS-VLN-PATH-UNC is \\x20-\\x7B regex escapes in — of all things — an input validator. check_a11y.py on 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 6ed096b rather than reproduced at runtime — the one exception is embed_screenshots.py --check, which I actually executed on both your head and main (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

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (26 finding(s))

  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/AetherDspWidget.cpp /tmp/aetherclaude/pr-6340/src/gui/AetherDspWidget.cpp:123
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/Ax25HfPacketDecodeDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/Ax25HfPacketDecodeDialog.cpp:168
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/Ax25HfPacketDecodeDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/Ax25HfPacketDecodeDialog.cpp:1636
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/Ax25HfPacketDecodeDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/Ax25HfPacketDecodeDialog.cpp:1893
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/Ax25HfPacketDecodeDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/Ax25HfPacketDecodeDialog.cpp:1974
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/Ax25HfPacketDecodeDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/Ax25HfPacketDecodeDialog.cpp:2658
  • [MEDIUM] CS-PII-DOB-ISO — PII detected: dob in src/gui/Ax25HfPacketDecodeDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/Ax25HfPacketDecodeDialog.cpp:2661
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/Ax25HfPacketDecodeDialog.h /tmp/aetherclaude/pr-6340/src/gui/Ax25HfPacketDecodeDialog.h:154
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow_Menus.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow_Menus.cpp:1631
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/gui/NetworkDiagnosticsDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/NetworkDiagnosticsDialog.cpp:1175
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in src/gui/NetworkDiagnosticsDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/NetworkDiagnosticsDialog.cpp:1547
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/NetworkDiagnosticsDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/NetworkDiagnosticsDialog.cpp:431
  • [LOW] CS-PII-IP — PII detected: ip_address in src/gui/NetworkDiagnosticsDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/NetworkDiagnosticsDialog.cpp:1175
  • [LOW] CS-PII-IP — PII detected: ip_address in src/gui/NetworkDiagnosticsDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/NetworkDiagnosticsDialog.cpp:1547
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/PskReporterMapDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/PskReporterMapDialog.cpp:1890
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/PskReporterMapDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/PskReporterMapDialog.cpp:2197
  • [HIGH] CS-VLN-SSRF-PRIV-192 — Vulnerability: CS-VLN-SSRF-PRIV-192 in src/gui/TailnetShimDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/TailnetShimDialog.cpp:281
  • [HIGH] CS-VLN-SSRF-PRIV-192 — Vulnerability: CS-VLN-SSRF-PRIV-192 in src/gui/TailnetShimDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/TailnetShimDialog.cpp:281
  • [MEDIUM] CS-PII-EMAIL — PII detected: email in src/gui/TailnetShimDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/TailnetShimDialog.cpp:238
  • [LOW] CS-PII-IP — PII detected: ip_address in src/gui/TailnetShimDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/TailnetShimDialog.cpp:281
  • [LOW] CS-PII-IP — PII detected: ip_address in src/gui/TailnetShimDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/TailnetShimDialog.cpp:281
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/WaveformsDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/WaveformsDialog.cpp:218
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/WaveformsDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/WaveformsDialog.cpp:219
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/WaveformsDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/WaveformsDialog.cpp:220
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in src/gui/WaveformsDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/WaveformsDialog.cpp:1255
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in src/gui/WaveformsDialog.cpp /tmp/aetherclaude/pr-6340/src/gui/WaveformsDialog.cpp:1255

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

Base automatically changed from canon-aether-modem to main October 10, 2026 08:03
…, 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>
@github-actions

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 1 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 4
Files with findings: 1
Total findings : 1

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@ten9876 ten9876 self-assigned this Oct 10, 2026
…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>
@ten9876

ten9876 commented Oct 10, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed in c68e724.

@aethersdr-agent: blocker, docs/user/screens.json not updated. Fixed:

  • the psk-reporter-sidebar recipe invokes "AetherMap...";
  • its caption and alt text, and the Tools-menu alt (menu-tools), use the new name;
  • embed_screenshots.py placed them, and --check now reports screenshots are embedded (119 shots).

The two stale shots, psk-reporter-sidebar.png and menu-tools.png, are named in the description for a re-shoot after merge.

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:

  • Connection dot after a theme change: fixed. themeChanged now calls updateConnectionIndicator().
  • tr("AetherMap"): fixed. It's now QStringLiteral, as AetherModem's title is.
  • Direct #include "core/ThemeManager.h": declined on evidence. Adding it raises core/ThemeManager.h's includer count in the aetherd touchpoint manifest (gen_touchpoint_manifest.py --check goes stale). The file already reaches ThemeManager through CanonIndicators.h and ComboStyle.h.
  • Token test for kAetherMapStyle: not added. You resolved all 14 tokens by hand in both themes, and the ModemChrome-style test pays off once the sheet grows.
  • pskReporterSidebarContent: kept as the bridge handle. Grabbing it renders the whole scrolled sidebar (1190 px), which is how the full sidebar was verified. The shot recipe crops the scroll area on purpose.

@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 /code-review during landing.

  • Fixed:
    • Quitting now closes AetherMap from MainWindow::closeEvent(), before radio.disconnect. Probe logs with and without the change are in the description. done() already ran before the destructor, so the use-after-free it claimed doesn't happen, but the beacon's PTT-off would have followed the disconnect.
    • done() now uses the destructor's condition, m_beaconRequest.valid() || armed || transmitting.
    • done() is public.
    • Combo popups get a raised panel.
  • Closed:
    • The band-condition pills and the map's hover card are deliberately unchanged: the pills share the Propagation Dashboard's palette, as the description says.
    • Folding kAetherMapStyle into canonBodyStyleSheet() isn't done: that helper is the minimal body rule set, and this sheet is the Network Diagnostics form vocabulary.

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:

  • the build passes, along with canon_window_test, modem_chrome_test, window_show_state_test and theme_manager_test;
  • the static checks pass, and the touchpoint manifest is unchanged;
  • embed_screenshots --check and gen_reference --check pass;
  • in the offscreen demo app, AetherMap renders, quits cleanly with the window open, and the probe ordering is as above.

@github-actions

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 1 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 5
Files with findings: 1
Total findings : 1

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@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.

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 at src/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 at MainWindow.cpp:9914-9921, is argument-list QProcess::startDetached with applicationFilePath() and this process's own argv — and is untouched by this PR, whose sole MainWindow.cpp hunk is the reject() at 4308.
  • CS-VLN-PATH-PROC / CG-PATH-001 — fixed literals (/proc/self/status, a constant signal_classifier.onnx joined to applicationDirPath()); no caller-supplied segment; present on main.
  • CS-VLN-SSRF-LOCALHOST / CS-PII-IP / CS-PII-EMAIL — screenshot alt text in docs/user/screens.json (127.0.0.1 for the built-in simulator, you@example.com which is RFC 2606 reserved, an elided tskey-auth-… placeholder). Never fetched or parsed; real data is covered by the entry's redact rectangles; all flagged lines exist verbatim on main.
  • CS-INJ-homoglyph / CS-INJ-role_override — ordinary typography (—, °, Météo-France) in comments and tr() strings, and the C++ override keyword / the "Override Colors" settings string. Every PR-added region (kAetherMapStyle 201-341, the constructor head, done(int), the menu label) is pure ASCII.
  • The new kAetherMapStyle is a constexpr const char* never concatenated with runtime data; {{token}} substitution in ThemeManager::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 at PskReporterMapDialog.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:481 tr("PSK Reporter filters"), :486 tr("PSK Reporter map callsign"), :503 tr("Show all PSK Reporter callsigns"), :539 tr("Show active PSK Reporter monitors"), :550 tr("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". :550 is 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_test links aethercore Qt6::Core only (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 c68e724 is that done() runs while m_radioModel is alive, since it's a value member of MainWindow and so is destroyed before ~QObject deletes the child dialog — making the dialog's destructor see a dangling pointer. I checked whether done() leaves the destructor's identical test true, which would re-enter stopBeacon() on the dangling model and defeat the fix. It doesn't: stopBeacon() (PskReporterMapDialog.cpp:~2130) does std::exchange(m_beaconRequest, {}) and clears m_beaconArmed / m_beaconTransmitting, so ~PskReporterMapDialog()'s guard at :2017 is false on the second pass. The ordering fix is real and the paths don't collide.
  • A cancellable quit. If MainWindow::closeEvent() could event->ignore() after line 4308, a cancelled quit would leave the map closed behind it. It can't — I read the whole function from MainWindow.cpp:4182 and there is no ignore() or early return anywhere in it. I also confirmed File → Quit routes through close() (MainWindow_Menus.cpp:335) rather than qApp->quit(), so "every close path" survives that hole, and that the reject() at 4308 does precede the radio disconnect (~4389), as the body claims.
  • What trackPersistentDialog() was for. Dropping it at MainWindow_DigitalModes.cpp:1634 could have silently removed the map from a disconnect sweep or a minimal-mode walk. It couldn't: m_persistentDialogs has exactly one consumer, the frameless-propagation loop in setFramelessWindow() at MainWindow.cpp:10211-10218, which only calls setFramelessMode(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() writes saveGeometry().toBase64() (PersistentDialog.cpp:137) while CanonWindow writes "x,y,w,h". The claim holds anyway: CanonWindow::restoreGeometryFromSettings() falls through its 4-part parse to restoreGeometry(QByteArray::fromBase64(...)), so an existing base64 value is read on first open and rewritten in the new format.
  • Missing theme tokens. statusHex() resolves color.accent.{danger,warning,success}; if any were absent the connection dot would silently paint #000000. I walked both resources/themes/default-{dark,light}.json rather than grepping (nested tokens don't grep): all three resolve at root ({color.red.500} / {color.amber.500} / {color.green.500}) and the canon scope doesn't shadow them. Likewise canon/aetherMap has no sub-scope in the theme JSON — but neither do aetherRx/aetherTx/aetherModem, so it resolves to canon exactly as the three merged migrations already do.
  • The menu move. PskReporterMapDialog's action is added to viewMenu (MainWindow_Menus.cpp:1327), which initially looked like it contradicted every doc line saying Tools → AetherMap…. It doesn't — toolsMenu->addAction(pskMapAction) at :1563 re-adds the same QAction, so it appears under both and menu-reference.md is correct.
  • A minimized window at quit. The isVisible() guard at MainWindow.cpp:4308 would skip a minimized map if isVisible() 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.js keys off the unchanged slug, and the 10 remaining "PSK Reporter Map" hits are all in versioned_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

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (46 finding(s))

  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6340/docs/user/screens.json:2579
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6340/docs/user/screens.json:4219
  • [MEDIUM] CS-PII-EMAIL — PII detected: email in docs/user/screens.json /tmp/aetherclaude/pr-6340/docs/user/screens.json:3877
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6340/docs/user/screens.json:1122
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6340/docs/user/screens.json:2579
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6340/docs/user/screens.json:2625
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6340/docs/user/screens.json:2659
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6340/docs/user/screens.json:2756
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6340/docs/user/screens.json:2796
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6340/docs/user/screens.json:4219
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:9672
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:9784
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:9906
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:9907
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:9908
  • [HIGH] CS-VLN-PATH-PROC — Vulnerability: CS-VLN-PATH-PROC in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:6203
  • [HIGH] CS-VLN-PATH-PROC — Vulnerability: CS-VLN-PATH-PROC in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:6282
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:525
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:555
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:558
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:580
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:3643
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:1279
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:1803
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:1895
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:1972
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:2014
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:2049
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:2328
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:3585
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:3676
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:6145
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:7951
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:8016
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:8090
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:8175
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:8356
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:8559
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:9773
  • [LOW] CS-PII-IP — PII detected: ip_address in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6340/src/gui/MainWindow.cpp:6895

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

@ten9876
ten9876 merged commit 0156894 into main Oct 10, 2026
10 checks passed
@ten9876
ten9876 deleted the canon-aether-map branch October 10, 2026 08:32
ten9876 added a commit that referenced this pull request Oct 10, 2026
… 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>
ten9876 added a commit that referenced this pull request Oct 11, 2026
…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>
ten9876 added a commit that referenced this pull request Oct 11, 2026
…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>
ten9876 added a commit that referenced this pull request Oct 12, 2026
… 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>
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.

1 participant