Skip to content

feat(gui): AetherRX and AetherTX are CanonWindows, re-skinned to the canon palette through a canon theme scope. Principle VIII. - #6334

Merged
ten9876 merged 3 commits into
mainfrom
canon-aether-rx-tx
Oct 10, 2026
Merged

ten9876 merged 3 commits into
mainfrom
canon-aether-rx-tx

Conversation

@ten9876

@ten9876 ten9876 commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Part of #6239 (listed there under "Picked: tool windows"). AetherRX and AetherTX move to CanonWindow (the visual canon, RFC #6226), using the About window's recipe the way Network Diagnostics and Waveforms do, and their contents are re-skinned to the canon palette.

Chrome. Each window gets:

  • No title bar and 16 px rounded corners.
  • The ambient ground and a canon header.
  • A corner close button. Escape and Ctrl+W close the window too.
  • Drag on any empty area to move it, and FramelessResizer on every edge.

Saved positions carry over through setGeometryKey() on the existing keys (AetherRxDialogGeometry2, AetherialStripGeometry2) and are flushed whenever the window hides, including on Escape. Both windows still pin their size at 720×480 on every open, through the new CanonWindow::setLaunchSize().

AetherTX loses its inline 18 px title bar, its minimize/maximize buttons, and its own geometry and frameless-toggle code. Neither window follows View → Frameless Window any more. Closing AetherTX still hides it rather than deleting it, because MainWindow keeps feeding it state.

Independent workspace windows. Both use the new CanonWindow::Kind::Workspace. It's an independent Qt::Window, so AetherTX keeps its own taskbar entry (#2301), minimises on its own and can live on another monitor. WA_QuitOnClose is off. In a workspace, Return and Enter in a field never click a default button. QDialog makes every push button an auto-default, so Return in a knob's value field had also pressed the Gate tab. Kind::Dialog (About, Waveforms, Network Diagnostics) is unchanged.

Re-skin through a theme scope. A new canon scope in both bundled themes maps these tokens to color.canon.*:

  • color.background.0–3
  • color.text.primary/secondary/label
  • color.border.subtle/strong
  • color.accent, color.accent.bright
  • color.knob.*, color.slider.*

The windows sit under canon/aetherRx and canon/aetherTx. Content they share with other surfaces (the AetherNR widget, the curve, meter and knob canvases) turns canon inside these two windows only. Docked applets and floating editors look the same as before.

The painted widgets involved now look colours up through the widget's scope: color(this, …), plus a new widget-aware theme::withAlpha(widget, …) overload. This changes nothing outside a scope. None of the converted files reads the three tokens that the applet/tx|rx|comp scopes override. The Strip*Panels, constructed only in these two windows, trade their hard-coded hex colours for tokens.

Startup fix. AetherTX's startup restore now waits for MainWindow's first show. Shown before its parent was mapped, a tiling compositor (Hyprland) took the parentless dialog for a top-level window and tiled it, and CanonWindow then saved the tiled size.

Final Output layout. The waveform's title row now sits directly on top of the plot. The scope has a fixed height, so the spare room goes above the title row instead of between the row and the plot.

Visible changes to call out

  • The output-meter gradient loses its −12 dB yellow-green stop, because no token maps it.
  • In Default Light, the reverb and de-esser canvases now follow the theme instead of staying dark islands. The meter tracks use color.canvas.background.
  • docs/user/docs/aetherial-audio.md describes the new window behaviour. The docs/user screenshots of these windows (aethertx*.png, aetherrx.png, first-session-aetherrx-nnr.png) show the old chrome and can be recaptured in a follow-up.

Constitution principle honored

Principle VIII (Evidence Over Assertion): every behavioural claim here has a test, a mutation that breaks it, or a runtime reproduction, listed below. The visual direction is RFC #6226's.

Test plan

  • Local build passes
  • theme_manager_test: a new block pins every canon scope mapping to its color.canon.* token in both bundled themes, plus a check that root is untouched. Dark repeats the canon hex values in the scope, because scope values can only alias primitives. Changing one dark value by one fails the test.
  • canon_window_test also pins both CanonWindow kinds: window type, WA_QuitOnClose, and Return or keypad Enter in a field (a workspace ignores them; a dialog clicks its default). Disabling the guard fails it. A throwaway harness linked against the app's objects reproduces the review's scenario on the real AetherTX window: Return in a Compressor knob field switches to Gate with the guard off and stays on Compressor with it on.
  • canon_window_test also covers two geometry behaviours. Escape flushes the saved geometry to disk (it survives AppSettings::load()). A pinned launch size wins over a saved 1420×900 and centres when the saved position is on no screen. Each test fails when its fix is removed.
  • The user docs site builds (npm ci && npm run build), and gen_reference --check and test_log_rules pass.
  • canon_window_test, theme_seed_test, stage_tab_bar_drag_test, modem_chrome_test, compact_metrics_test, comp_makeup_fader_test, rx_chain_runner_test, rx_stage_reorder_test and window_show_state_test pass locally.
  • The static-check scripts pass locally. The theme seed and the touchpoint manifest are regenerated.
  • Real GUI (Wayland, Hyprland), driven through the automation bridge:
    • Both windows render, including the QRhi waveform.
    • AetherTX restored at startup opens floating at 720×480.
    • The EQ combo popup stays opaque.
    • Default Light compared against main: the new readability problems are none, and the light-theme knob value text was already near-invisible on main.
  • Run by the maintainer against the FLEX-8600.
  • Edge resize and ground drag in a real compositor were not exercised by hand. canon_window_test covers the move fallback offscreen.

Checklist

  • Commits are signed
  • No new flat-key AppSettings calls. Existing keys are reused.
  • Code is clean-room
  • All meter UI uses MeterSmoother (unchanged)
  • Documentation updated: docs/user/docs/aetherial-audio.md, docs/style/{aethersdr-style-guide,dialog-patterns,theme-style-guide}.md
  • Security-sensitive changes: n/a

🤖 Generated with Claude Code

…canon palette through a canon theme scope. Principle VIII.

Both windows now use the About window's recipe for tool windows, as Network
Diagnostics and Waveforms do: CanonWindow chrome (no title bar, 16 px rounded
corners, ambient ground, corner close button, drag any empty area to move,
Escape / Ctrl+W to close), a canon header, FramelessResizer on every edge, and
the existing geometry keys via setGeometryKey(), so saved positions carry
over. Both still pin their size at 720x480 on open.

AetherTX loses its inline 18 px title bar, its min/max buttons and its own
geometry and frameless-toggle code. It no longer follows View -> Frameless
Window. Close still hides it rather than deleting it, because MainWindow keeps
feeding it state. AetherTX's startup restore now waits for MainWindow's first
show. Shown earlier, a tiling compositor (Hyprland) took the parentless dialog
for a top-level window, tiled it, and CanonWindow saved the tiled size.

Re-skin: a new `canon` scope in both bundled themes remaps the base background,
text, border, accent, knob and slider tokens to the color.canon.* palette. The
windows sit under canon/aetherRx and canon/aetherTx. Shared content (AetherNR's
widget, the curve, meter and knob canvases) therefore turns canon inside these
windows only, and the docked applets and floating editors keep their look. The
painted widgets they use resolve colours through the widget's scope
(color(this, ...), plus a widget-aware theme::withAlpha overload). The Strip*
panels, which are only used here, trade their hard-coded colours for tokens.

theme_manager_test pins every scope mapping to its color.canon.* token in both
themes. Dark repeats the canon hex values in the scope, since scope values can
only alias primitives. A mutation (one dark value off by one) fails it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876
ten9876 requested a review from a team as a code owner October 9, 2026 21:07
@ten9876 ten9876 assigned ten9876 and unassigned ten9876 Oct 9, 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.

The visual migration largely delivers its stated intent: both windows adopt CanonWindow, scoped content uses the canon palette, existing geometry keys are reused, and startup restoration is deferred until MainWindow shows. There is no closing issue; I reviewed against the PR body and the approved direction in RFC #6226 / tracker #6239. Request changes: the new QDialog host introduces an Enter-key regression, and the user-facing changes need their matching user-guide update.

Scope

File/group Change Claimed? Verdict
AetherRxDialog.{cpp,h}, AetherialAudioStrip.{cpp,h} CanonWindow hosts, headers, geometry and close lifecycle Yes In scope; keyboard regression below
MainWindow.{cpp,h} Create/raise RX directly, defer TX startup show, remove frameless propagation Yes In scope
StagePage.{cpp,h} Shared canon header and transparent-body stylesheet Yes In scope
Nine ClientComp* / ClientGate* / ClientDeEssCurveWidget / ClientTubeCurveWidget canvases and WaveformWidget.cpp Widget-aware paint-token lookup Yes In scope
Ten Strip*Panel.cpp files Replace fixed colors with tokens; keep waveform title with plot Yes In scope
Bundled theme JSONs, ThemeManager.h, ThemeSeedGenerated.cpp, theme_manager_test.cpp Canon scope, alpha helper, generated seed and mapping assertions Yes In scope
Three docs/style guides and docs/architecture/aetherd-touchpoints.md Migration documentation and regenerated theme include count Yes In scope; user guide still missing

Everything in the diff is explained by the body. The single commit's author date is 2026-10-09; there is no unrelated historical commit. No base theme token values, protocol verbs, new dependencies, or enforcement baselines change. The documentation checklist accurately names contributor/style documentation, but it does not satisfy the separate user-documentation requirement.

Blockers

  1. Enter in a compressor value editor also activates the Gate tab. src/gui/AetherialAudioStrip.cpp:95, with the button construction at src/gui/StageTabBar.cpp:139.

    Switching from QWidget to CanonWindow makes the processing controls descendants of a QDialog. Their QPushButtons retain autoDefault=true. In a QtTest harness linked against the PR's actual desktop objects, I opened AetherTX, clicked Compressor, focused a visible knob's QLineEdit, typed 4, and pressed Return. The edit committed, but QDialog also clicked the default Gate button and switched away from Compressor:

    Before Enter: Compressor_selected=1
    Before: RN2=0 autoDefault=1 default=0 focus=QLineEdit
    DEFAULT: Gate visible=1 enabled=1
    CLICKED: Gate aetherTxTabGate
    After Enter: RN2=0 RN2_clicked=0 knob=4.001
    After Enter: Compressor_selected=0
    

    With automatic/default buttons disabled, the identical edit stays on Compressor:

    Before Enter: Compressor_selected=1
    Before: RN2=0 autoDefault=0 default=0 focus=QLineEdit
    After Enter: RN2=0 RN2_clicked=0 knob=4
    After Enter: Compressor_selected=1
    

    A separate fixture using the former QWidget/Qt::Window host with the same gate panel produces no button click on Return. This comparator isolates the host-type change; it is not a full merge-base app build. The harness is socket-free and has no radio connection. The demo bridge independently verified the real window's tab selection and close/reopen paths; its invoke API does not send physical Return keys, so the keyboard reproduction uses QtTest on the real widgets.

    Disable automatic defaults on the processing-window buttons, preserving deliberate defaults in separate confirmation dialogs. A drop-in mitigation at the end of AetherialAudioStrip's constructor, after the panels have been embedded, is:

        for (QPushButton* button : findChildren<QPushButton*>()) {
            if (button->window() == this) {
                button->setAutoDefault(false);
                button->setDefault(false);
            }
        }

    Add a socket-free regression that selects a different stage, commits a knob edit with Return, and checks the selected stage remains unchanged. This also implements the existing docs/DEVELOPER-GUIDE.md instruction: “Disable autoDefault on QPushButtons inside QDialogs.”

  2. Update the matching user guide in this PR. docs/user/docs/aetherial-audio.md:59.

    This section explains using the two windows, but the PR changes their close shortcuts, drag/resize chrome, relationship to View → Frameless Window, and AetherTX's independent-window/taskbar behavior without editing any docs/user content. AGENTS.md:162–163 explicitly requires: “a PR that changes what a user sees or does updates the matching page in the same PR, never in a follow-up.” Add the new window behavior here and validate the docs build. The stale screenshot disclosure in the body meets the separate screenshot exception; I am not requiring screenshot recapture to block this review.

Nits — non-blocking

  • Correct the principle's name in the PR body. CONSTITUTION.md:226: Principle VIII is Evidence Over Assertion, not “one visual language.” Cite VIII for the verification evidence and RFC #6226 for the visual direction.

Verification and attacks that held up

  • Built the exact head be53b1c2757547c8234f4180aee45025adfe8fe8 in a scratch worktree with Qt 6.12 on Linux, shared core and -O2 -g1. All ten tests named in the body passed: theme manager/seed, CanonWindow, stage drag, modem chrome, compact metrics, comp makeup fader, RX chain runner/reorder, and window show state. RX chain runner needed explicit QT_QPA_PLATFORM=offscreen in this headless environment. No new/modified socket-owning test was found. No independent automated code-review skill was available.
  • Mutation-checked the added mapping assertion using a separate resource object: changed dark canon.color.background.0 from #0a121e to #0a121f; theme_manager_test.cpp:1512 failed with the exact mismatched RGBA values. The original head test passed. Original sources and resource objects were untouched.
  • Ran engine boundary, capability-record, command-plane and touchpoint checks on merge base 7a649a9e6 and head; their findings are identical. The two added ThemeManager includes are UI theming and disclosed; the generated manifest and theme seed check cleanly. No seam, TX-gate, or settings-key growth was found.
  • Drove the owned aethersdr-pr6334-review bridge against DEMO-0001 only, with fresh settings, offscreen rendering and TX disabled. get radio confirmed model: "AetherSDR Demo", connected: true, transmitting: false. Opened both menus/windows, switched TX stages, captured Dark and Light windows, closed/reopened both, and resized TX to 900×600 then confirmed reopening pins it back to 720×480. A separate restart with the saved visibility flag explicitly set True restored TX at { "h": 480, "w": 720, "x": 340, "y": 160 }. All owned app instances were closed.
  • Verification limits: no physical compositor edge drag, native Windows/macOS window-manager behavior, or live-radio behavior was tested. Offscreen Qt reports QRhi unsupported, so the black waveform capture is not evidence for the PR's Wayland/GPU claim. A normal demo-session quit also cleared TX's visibility flag; I have not attributed that to this PR without a merge-base comparison, so startup verification is specifically the seeded-flag restore path rather than a claim that quit persistence was proven.

All required CI checks are successful on the reviewed head. Their pull-request runs were created at 2026-10-09 21:07:16 UTC, after current main's 20:30:34 UTC tip (d6f6a4f00), so there is no observed stale-main timing gap.

Recommendation: Request changes. Fix Return/default-button dispatch and add the user-guide update, then rerun the focused keyboard regression. The rest of the scoped migration survived the checks above.

@ten9876 ten9876 removed their assignment Oct 9, 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.

Issue fit

No fixes/closes line in the body, so I reviewed against the PR's own stated intent and against the canon-migration tracking issue #6239. The stated work is all present and coherent: both windows derive CanonWindow, both carry theme::setContainer(this, "canon/aether{Rx,Tx}"), setGeometryKey() reuses the old keys (and CanonWindow::restoreGeometryFromSettings() has a base64 fallback, so the pre-existing saveGeometry() blobs under AetherRxDialogGeometry2 / AetherialStripGeometry2 do carry over — I checked that path specifically), the canon scope exists in both bundled themes plus the generated seed, and theme_manager_test pins it.

The governance question is whether these two windows were in scope yet — see Blockers 1.

Scope

The task handed me a 30-file list; the commit is 37 files (src/gui/StripPuduPanel.cpp, StripReverbPanel.cpp, StripRxOutputPanel.cpp, StripTubePanel.cpp, StripWaveformPanel.cpp, WaveformWidget.cpp, tests/theme_manager_test.cpp were missing). This review covers all 37.

File / group What it changes Claimed? Verdict
AetherRxDialog.{h,cpp}, AetherialAudioStrip.{h,cpp} PersistentDialog/QWidget(Qt::Window) → CanonWindow; inline title bar, frameless toggle and own geometry code deleted yes In scope
MainWindow.{h,cpp} ensureAetherRxDialog() creates directly instead of showOrRaisePersistent(); strip restore deferred to showEvent; setFramelessMode/setWindowFlag(Qt::Window) calls dropped yes In scope (see Blockers 2)
StagePage.{h,cpp} new makeCanonHeader() / canonBodyStyleSheet() yes In scope
resources/themes/default-{dark,light}.json, ThemeSeedGenerated.cpp new canon scope, 15 token remaps each yes In scope. No base token value changed — I diffed the root scopes; this respects #6239's "a migration PR never changes a base token's value"
ThemeManager.h widget-aware theme::withAlpha overload yes In scope, used in 4 files
ClientComp*, ClientGate*, ClientDeEss*, ClientTube*, WaveformWidget root color(...) → widget-aware color(this, ...) yes In scope
Strip*Panel.cpp (10 files) hard-coded hex QSS → {{token}} via applyStyleSheet yes In scope. "only used here" verified: every Strip*Panel is instantiated only from AetherRxDialog.cpp / AetherialAudioStrip.cpp
docs/style/*.md, docs/architecture/aetherd-touchpoints.md doc updates + touchpoint count 169→171 yes In scope
tests/theme_manager_test.cpp +37 lines pinning the scope yes In scope

No unrelated files, no new public/protocol surface, no new settings key (both geometry keys are reused), no CHANGELOG.md entry. No socket tests added or modified — I read the added test source, not the working tree.

Blockers

1. #6239 does not list AetherRX or AetherTX, and this is two windows in one PR. #6239 (the tracking issue this work sits under) says:

Each item below is its own issue and its own PR. Screens move to the canon only when an issue asks them to; nothing here authorises restyling a screen as a side effect of other work. Items under "Candidates" are not approved scope until the maintainer picks them.

Neither window appears on it. The nearest line is "Persistent tool dialogs (one PR per dialog)" under Candidates: surfaces whose widgets move from color.canon.* — explicitly unapproved, and explicitly one dialog per PR. docs/style/aethersdr-style-guide.md:112 likewise reads "the tool windows the maintainer has moved to it", and this PR edits that sentence to add the two windows.

I checked the authorship before raising this: git log shows the Waveforms and Network Diagnostics canon migrations, and #6239 itself, are all by the same identity as this commit (Jeremy [KK7GWY] <105957118+ten9876@…>). So this is most likely you ticking your own list, not a scope objection — in which case the fix is procedural: add the two items to #6239 (or open the per-window issues) and link them from the body, so the trail matches the rule the tracking issue sets. I'm flagging rather than waving it through because the rule is unusually explicit about authorisation, and the next contributor will read this PR as precedent for moving a screen with no issue.

2. MainWindow.cpp:10189 — AetherTX stops being an independent top-level window; the deleted comment names what is lost. The diff removes:

        // Override the parent-window relationship so the strip behaves as
        // an independent window (own taskbar entry, raisable separately).
        m_aetherialStrip->setWindowFlag(Qt::Window, true);

That call was itself a no-op (the old ctor already passed QWidget(parent, Qt::Window)), but the behaviour came from that ctor flag, and CanonWindow's ctor replaces it with setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint) (src/gui/CanonWindow.cpp:44). A parented Qt::Dialog gets no taskbar button on Windows, stays above MainWindow, and is hidden when MainWindow minimises — and with the min/max trio deleted (AetherialAudioStrip.cpp, old lines ~155-185) there is now no way to minimise AetherTX at all. The body discloses losing min/max and the Frameless toggle; it does not mention the taskbar entry or the stays-above/hides-with-parent consequences.

AetherRX is unaffected — PersistentDialog already derives QDialog (src/gui/PersistentDialog.h:21). And Network Diagnostics and Waveforms already took this trade, so this isn't novel. But AetherTX is the one window that was deliberately built as an independent Qt::Window (#2301), and operators park it on a second monitor. Needs a maintainer decision: accept the trade and say so in the body, or construct these two with Qt::Window | Qt::FramelessWindowHint instead of Qt::Dialog.

Nits (non-blocking)

  • src/gui/StagePage.cpp:139-146 — the descendant-wide transparent rule may reach child dialogs' inputs. "QWidget#canonBody QWidget { background: transparent; }" is more specific (id + type) than a plain type selector, so it can beat a QSpinBox { … } rule set further down the tree. The exception list covers QMenu and combo popups, but not the child QDialogs built from these pages — e.g. the Quindar editor at src/gui/StripFinalOutputPanel.cpp:858-883, whose QSpinBoxes at :1002-1019 have no self-set background. Combos are safe (ComboStyle.h:61 sets QComboBox { background: … } on the combo itself, and it resolves scope-aware). This is reasoned from the cascade, not observed — worth one look at that dialog in both themes. Neither Network Diagnostics nor Waveforms uses a descendant-wide rule; they style by object name, which sidesteps this.
  • src/gui/AetherRxDialog.cpp:419-421 and AetherialAudioStrip.cpp showEvent — the size pin silently discards every operator resize. CanonWindow::showEvent sets m_placed = true before returning, so the subclass's resize(kLaunchSize) fires resizeEvent → saveGeometryToSettings() → 720×480 is written back over the stored rectangle. Position survives; size never does, which makes the newly installed FramelessResizer a no-op across sessions and doesn't meet dialog-patterns.md's "a workspace tool keeps the size its operator gave it". The doc hunk does say "for now", so this is disclosed — just noting that the mechanism is destructive, not merely ignored.
  • src/gui/AetherDspWidget.cpp:601-613, 750-755 — the page the new style-guide line names as the motivating case wasn't converted. The added guidance says painted code "must use the widget-aware lookups … to see it", but AetherNR's labels are raw setStyleSheet with hard-coded hexes (#8090a0, #c8d8e8, #48515a) and the status dot resolves root-scope ThemeManager::instance().color("color.text.label"), so they stay base-coloured inside the canon window. Same class at src/gui/ClientCompKnob.cpp:61-68 (the knob canvas became scope-aware; its value editor's QSS did not). Cosmetic only — on light, #8090a0 was already ~2.9:1 against root's #f5f5f8, so canon's #ffffff is not an a11y regression.
  • The canon scope covers 2 of the 5 canon windows. About, Waveforms (dialog/waveforms) and Network Diagnostics (dialog/networkDiag) keep dialog/* containers, so a theme edit to canon won't reach them. docs/style/theme-style-guide.md's new "The canon scope does the same for whole windows" reads broader than what ships.
  • Pre-existing user themes get an empty canon scope. loadThemeFile() wipes scope children and then only replays declared paths (ThemeManager.cpp:519) with no tokens, so a theme saved by an earlier version resolves these 15 tokens from root — base-dark content inside a canon ground until it's re-saved. Same exposure as applet/*, so architectural rather than introduced here; worth a line in the body. Related: any operator override previously written under dialog/aetherRx is now orphaned.
  • The new test discriminates only in dark. tests/theme_manager_test.cpp:1484+ is a real test (delete the scope and it fails), but in light color.background.1/color.text.primary alias the same primitives as canon.nested/canon.ink (#dde5ed, #1a2a3a), so several light rows pass vacuously — which is exactly the aliasing hazard #6239's open question flags. The colorAt(QString(), …) == color(…) assertion is identically true for any input.

What I tried to break

  • Dead scope path. lookupRaw() falls back to root, not to the nearest ancestor scope, when scopeForPath() misses — and the themes declare canon with no children, so canon/aetherRx only resolves because theme::setContainer → registerDeclaredContainer → scopeOrCreate links the child under canon (ThemeManager.cpp:252-279). It does, and :519 replays it across theme loads. Holds.
  • Dead token mappings. The four knob/slider remaps are live, not decoration: ClientCompKnob.cpp:40-41 already uses color(w, …) and sliders go through applyStyleSheet → resolveFor(widget).
  • Collateral re-skin of the docked applets. This was my main suspicion for the widget-aware switch. applet/tx|rx|comp override only color.slider.foreground, color.knob.foreground and color.toggle.accent.background.checked; no changed file reads any of those three (verified by grepping all 37 changed files). WaveformWidget is shared with WaveApplet but resolves root outside the scope. No change outside the two windows.
  • "Only used here." Confirmed per-class: every Strip*Panel has exactly two (or one) instantiation sites, both in AetherRX/AetherTX. So the hard-coded-hex → token swaps can't shift a docked surface.
  • Lost guard on the embedded panels. The deleted block in AetherialAudioStrip.cpp hid each panel's min/max/close and recoloured #08121d. That is not a regression — StagePage::tidyEmbeddedPanel() (pre-existing, called at AetherialAudioStrip.cpp:340-344) already does both; the deleted copy was redundant.
  • Dangling m_rxDialog. ensureAetherRxDialog() now news the dialog itself with WA_DeleteOnClose; m_rxDialog is a QPointer (MainWindow.h:1692), so the wasFresh test and the ~20 other call sites are safe. Escape routes through QDialog::reject() rather than close(), and either way the next toggle press re-shows or re-creates correctly.
  • Stale setFramelessMode callers. Grepped the tree: nothing calls it on the strip any more, and the declaration is gone from the header.
  • Theme value drift. Resolved every canon scope value against the color.canon.* palette in both themes by hand (script over the JSON): all 15 match in both, so the new test should pass — consistent with the green Static checks / build / check-macos / check-windows runs on be53b1c.
  • Could not check: anything visual. No build, no GUI, no Wayland/X11/Windows session here — the chrome, the transparent-descendant cascade, the drag/resize interaction between FramelessResizer and CanonWindow's drag-anywhere mousePressEvent, and the light-theme canon rendering are all unverified. Every finding above is reasoned from the code as it will exist after merge, not reproduced at runtime.
  • Security audit (CodeGuard): ran, no confirmed findings. Worth knowing: the harness's file list and its 423 findings were computed against a stale merge base — none of the files this PR actually re-skins were in the list, while most listed files (AutomationServer.cpp, AudioEngine.cpp, CopyAssistController.cpp, codeql.yml, …) are unchanged main content from #6216/#6205/#6182. The CMDI-* hits are QSS {{token}} braces and Qt && label escaping; SSRF-LOCALHOST is the TCI simulator dialing our own loopback; PII-BANK is a SHA-256 download pin. All refuted at source.

Recommendation

Needs maintainer decision — on the #6239 authorisation trail (Blockers 1) and on AetherTX giving up its independent-top-level-window behaviour (Blockers 2). The engineering is clean: the scope mechanism is sound, the test genuinely discriminates, the "only used here" and "no collateral applet change" claims both hold under checking, and nothing in the diff is unexplained by the stated work. If you're the maintainer ticking your own list, Blocker 1 collapses to adding the items/issue links to #6239 and a line in the body about the taskbar/minimise trade — at which point this is approve-with-nits. Concrete next step: rule on the Qt::Dialog vs Qt::Window question, and take one look at the Quindar sub-dialog's spin boxes in the light theme for the cascade nit.

Nice work on the geometry-format fallback and on keeping the scope purely additive — no base token moved, which is the part of #6239 that would have been easiest to get wrong.


🤖 aethersdr-agent · cost: $16.3123 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

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

  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/codeql.yml /tmp/aetherclaude/pr-6334/.github/workflows/codeql.yml:22
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/codeql.yml /tmp/aetherclaude/pr-6334/.github/workflows/codeql.yml:27
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6334/docs/architecture/aetherd-touchpoint-tags.json:10
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6334/docs/architecture/aetherd-touchpoint-tags.json:1145
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/AudioEngine.cpp /tmp/aetherclaude/pr-6334/src/core/AudioEngine.cpp:9742
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/AudioEngine.cpp /tmp/aetherclaude/pr-6334/src/core/AudioEngine.cpp:9743
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AudioEngine.cpp /tmp/aetherclaude/pr-6334/src/core/AudioEngine.cpp:1378
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AudioEngine.cpp /tmp/aetherclaude/pr-6334/src/core/AudioEngine.cpp:5432
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.cpp /tmp/aetherclaude/pr-6334/src/core/AudioEngine.cpp:9283
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.h /tmp/aetherclaude/pr-6334/src/core/AudioEngine.h:630
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.h /tmp/aetherclaude/pr-6334/src/core/AudioEngine.h:650
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.h /tmp/aetherclaude/pr-6334/src/core/AudioEngine.h:692
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.h /tmp/aetherclaude/pr-6334/src/core/AudioEngine.h:756
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.h /tmp/aetherclaude/pr-6334/src/core/AudioEngine.h:771
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.h /tmp/aetherclaude/pr-6334/src/core/AudioEngine.h:803
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.h /tmp/aetherclaude/pr-6334/src/core/AudioEngine.h:1025
  • [HIGH] CS-VLN-SSRF-PRIV-192 — Vulnerability: CS-VLN-SSRF-PRIV-192 in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:6967
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:12202
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:12203
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:6177
  • [CRITICAL] CS-VLN-CMDI-SEMI — Vulnerability: CS-VLN-CMDI-SEMI in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1757
  • [CRITICAL] CS-VLN-CMDI-SEMI — Vulnerability: CS-VLN-CMDI-SEMI in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:7455
  • [CRITICAL] CS-VLN-CMDI-SEMI — Vulnerability: CS-VLN-CMDI-SEMI in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:12993
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:2389
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:8418
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:9416
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:11437
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:11800
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:12359
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:299
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:463
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:840
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:949
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1306
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1379
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1531
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1608
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1753
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1783
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1787

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: $16.4831 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Antares Detector — candidate vulnerable file(s)

  • src/core/ThemeManager.cpp
  • src/core/ThemeSeedGenerated.cpp

The theme loader reads user-supplied theme JSON and user-supplied theme paths from disk. ThemeManager.cpp implements loadThemeFromPath and scope/creation logic that resolves paths without enforcing a safe root-directory restriction. ThemeSeedGenerated.cpp generates the authoritative theme JSON from resources/themes/*, so untrusted themes can be loaded. This creates a path traversal/injection risk if an attacker can supply a theme file path (e.g. via a network path or crafted filename) that is not validated before being used to open/read files.

Localized by Cisco Foundation AI Antares-1B running locally in the AetherClaude sandbox, seeded by the Cartographer security map. Advisory only — please verify before acting.


🤖 aethersdr-agent · cost: $16.4831 · model: claude-opus-5

…d Return in a field no longer clicks a stage tab. Principle VIII.

Review of #6334. CanonWindow gains Kind::Workspace. A workspace is an
independent Qt::Window, which gives back AetherTX's own taskbar entry (#2301),
lets it minimise on its own and lets it sit on another monitor. It has
WA_QuitOnClose off. In a workspace QDialog's default-button dispatch is
skipped: QDialog had made every QPushButton an auto-default, so Return in a
compressor knob's value field also clicked the Gate tab. A focused button
still takes Return itself. Kind::Dialog keeps the old behaviour, which
About's OK relies on.

canon_window_test pins both kinds: the window type, WA_QuitOnClose, and
Return and keypad Enter in a field (no click in a workspace, a click in a
dialog). Disabling the guard fails it. A throwaway harness linked against
the app's objects reproduces the review's scenario on the real AetherTX
window: the stage changes with the guard off and stays put with it on.

The user guide's AetherTX/AetherRX section now describes the window: moving
it, resizing it, closing it, its taskbar entry, and that View -> Frameless
Window does not apply.

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

@ten9876's review

  1. Return in a knob field clicked the Gate tab. Fixed by a new CanonWindow::Kind::Workspace, which both windows now use. In a workspace, Return and Enter skip QDialog's default-button dispatch, so buttons the panels create later are covered too. A focused button still takes Return itself. Kind::Dialog keeps the old behaviour, which About's OK relies on. canon_window_test checks Return and keypad Enter in a field for both kinds, and disabling the guard fails it. I also reproduced your scenario on the real AetherTX with a throwaway harness linked against the app's objects. Return in a Compressor knob field switches to Gate with the guard off and stays on Compressor with it on.
  2. User guide. docs/user/docs/aetherial-audio.md now describes the window: drag to move, resize from the edges, close with ✕, Esc or Ctrl+W, its own taskbar entry, View → Frameless Window doesn't apply, and Return in a value field only commits the value. The docs site builds locally.

@aethersdr-agent's review

  1. Tracking: AetherSDR style guide follow-ups (RFC #6226) #6239 authorisation trail. Tracking: AetherSDR style guide follow-ups (RFC #6226) #6239 now lists both windows under "Picked: tool windows", with the reason they share one PR: the tab column, every Strip* panel and the canon scope are common to both. The PR body links it.
  2. AetherTX's independent window. Maintainer ruling: keep it. A workspace is an independent Qt::Window, so AetherTX keeps its own taskbar entry (Aetherial Audio: Channel Strip — unified TX DSP window #2301), minimises on its own and can sit on another monitor. AetherRX matches. WA_QuitOnClose is off for both. Both still float at 720×480 under Hyprland, including AetherTX's startup restore.

The nits are noted and left for follow-up. One finding from the verification limits: AetherTX left open at a normal quit doesn't reopen on the next launch on main either (merge base 7a649a9e6 tested side by side, same result), so it isn't from this PR.

@ten9876 ten9876 self-assigned this 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.

Issue fit

Yes, with one authorisation question. #6239's "Picked: tool windows on CanonWindow" section names both windows, authorises the two-in-one-PR exception on exactly the reason this PR gives (shared stage tab column, shared Strip* panels, shared canon scope), and specifies Kind::Workspace. Every claim in the body maps to a hunk: chrome (CanonWindow.cpp:42-52, AetherRxDialog.cpp:92-111, AetherialAudioStrip.cpp:95-110), the scope (both theme JSONs + ThemeSeedGenerated.cpp:379-393), Kind::Workspace (CanonWindow.h:32-41, CanonWindow.cpp:117-131), the geometry carry-over (setGeometryKey() on the existing keys), and the startup fix (MainWindow.cpp:3956-3975).

The authorisation caveat: that section lives in an issue body you authored, and #6239's updated_at (2026-10-10 01:22) postdates this PR's creation (2026-10-09 21:07). The precedent in that same thread — your own #6273 status comment — cites the maintainer ruling as a linked #issuecomment URL. Please link the ruling that picked these two, the way #6273 does. The test plan's "Run by the maintainer against the FLEX-8600" suggests it exists; it just isn't citable from the issue as it stands.

Scope

41 files (the harness's file list was truncated at 29; I reviewed the full 7a649a9e6..a37c18a2b range).

File / group What it changes Claimed? Verdict
CanonWindow.{h,cpp} Kind enum, Qt::Window + WA_QuitOnClose off, Return/Enter guard Yes In scope
AetherRxDialog.{h,cpp}, AetherialAudioStrip.{h,cpp} PersistentDialog/QWidget → CanonWindow; deletes the inline 18 px title bar, the trio buttons, setFramelessMode(), and the private geometry save/restore Yes In scope
MainWindow.{h,cpp} ensureAetherRxDialog() create-or-raise; drops the strip from setFramelessWindow(); defers the AetherTX startup restore to showEvent Yes (incl. the Hyprland tiling rationale) In scope
StagePage.{h,cpp} makeCanonHeader(), canonBodyStyleSheet() Yes In scope
ThemeManager.h, ThemeSeedGenerated.cpp, both theme JSONs widget-aware withAlpha, canon scope, regenerated seed Yes In scope
ClientComp*, ClientGate*, ClientDeEss*, ClientTube*, WaveformWidget (9 files) color(token) → color(this, token), mechanical Yes In scope — required for the scope to be visible
Strip*Panel (10 files) hard-coded hex → tokens Yes In scope
StripFinalOutputPanel.cpp also moves the waveform title row onto the plot Body calls it out, no requirement needs it Scope note, not a blocker — it's a layout change riding a re-skin PR; fine to keep since you disclosed it, but say so in the commit message too
docs/ (5 files), tests/ (2) doc + test updates Yes In scope

docs/architecture/aetherd-touchpoints.md is the regenerated manifest (169→171). No CHANGELOG.md entry — correct.

Blockers

1. src/gui/StripFinalOutputPanel.cpp:208 and src/gui/StripRxOutputPanel.cpp:84 — the output level meter's track goes to a mid-grey in Default Light, and the bar on it is effectively invisible.

        p.setBrush(ThemeManager::instance().color(this, "color.canvas.background"));

color.canvas.background is WorkspaceCanvas's backdrop token (src/gui/workspace/WorkspaceCanvas.cpp:862 is its only other consumer), and in resources/themes/default-light.json:43 it is #7b7b7c — a mid grey. The canon scope does not remap it, so inside these windows the meter track stays #7b7b7c while the ground is #f5f5f8. Resolving the gradient (:227-230) against it:

Bar stop On old literal #0a0e16 On color.canvas.background light #7b7b7c
accent.success #1a8040 3.87:1 1.18:1
accent.warning #d08010 — 1.36:1
accent.danger #c02020 — 1.43:1

At 1.18:1 the green portion of the bar is indistinguishable from its own track. This is the TX output level meter — the thing an operator watches to avoid overdriving the transmitter — and the RX meter has the identical change. It also falsifies the body's "Default Light compared against main: the new readability problems are none"; the visual comparison missed it. tools/check_a11y.py has no contrast rule, so nothing gates it.

Not covered by #6239's "light-theme canon values are AI-generated, dark is the canon priority" note: color.canvas.background is a base token, not a color.canon.* one.

The one-line swap, which the canon scope does remap (→ canon.raised: #ffffff light, #0a121e dark):

        p.setBrush(ThemeManager::instance().color(this, "color.background.0"));

Light becomes green 4.99 / amber 3.10 / red 6.05; dark becomes 3.76 / 6.06 / 3.10. Both clear the 3:1 graphical-object floor in every stop, where today's light does not. Dark loses a little (4.0→3.76 green, 3.3→3.1 red) — if you'd rather not move dark at all, a dedicated color.meter.bar.background token is the cleaner answer, but color.background.0 is correct today and needs no theme change.

Nits

  • src/gui/StripFinalOutputPanel.cpp:227-230 — the body says the −12 dB stop was dropped "because no token maps it", but one does: color.meter.bar.fillGradient (resources/themes/default-light.json:157-167) is a 5-stop theme-aware gradient with exactly this green → yellow-green → amber → red ramp, it has a widget-aware accessor (ThemeManager.h:230), and the sibling meters in these same two windows already consume it — ClientCompMeter.cpp:238 and ClientLevelMeter.cpp:111. Using it keeps the calibration stop and makes the three meters agree. As written, 0.00 and dbToRatio(-12.0f) are both color.accent.success, so the duplicate stop is also dead weight.
  • src/gui/StripFinalOutputPanel.cpp:1260-1263 — the flashing LIMIT indicator now has background: {{color.accent.danger}} and border: 1px solid {{color.accent.danger}}, where it was #ff3030 on a lighter #ff8080 border. Same token on both means the outline disappears on the flash frame. color.accent.bright or a lightened variant restores the two-tone read. Same shape in setQuindarActive() at :1282-1284.
  • docs/style/aethersdr-style-guide.md:97 — "remaps the base color.background.*, color.text.*, color.border.*, color.accent*, color.knob.* and color.slider.* tokens" overstates color.accent*: only color.accent and color.accent.bright are in the scope. color.accent.dim, .warning, .danger and .success are untouched, and the converted ClientComp*/ClientGate* widgets read them heavily — so those stay base-palette inside the canon windows. Worth being precise, since the next surface migration will read this table.
  • src/gui/StagePage.cpp:138 — "they get their panel back after the rule above" points at a rule that is the first line of the same returned string, not something above it.
  • src/gui/MainWindow.cpp:10192 — m_aetherialStrip->setAttribute(Qt::WA_QuitOnClose, false) is now redundant; CanonWindow's Kind::Workspace ctor (CanonWindow.cpp:49-51) already does it. Harmless, but the comment there now describes someone else's job.
  • src/gui/MainWindow.cpp:3957 — the AetherTX restore is gated on MainWindow::showEvent, so a start where the main window is never shown (tray/minimised launch) silently skips it and leaves AetherialStripVisible=True. Low-likelihood, and the Hyprland bug is the worse failure, so this is the right trade — just noting it's now a possible path.

Security audit

Required by the harness (CodeGuard: 423 findings / 14 rule IDs). Ran, no findings attributable to this PR. Only three of the 33 flagged files are in this diff (src/core/ThemeManager.h, src/gui/MainWindow.{h,cpp}), and their changed lines are Qt widget lifecycle and theme-token plumbing — no I/O, parsing, credentials or process execution. All 14 flagged rule IDs are false positives on this codebase and I verified each rather than repeating it: CS-VLN-CMDI-AMP/PIPE/SEMI/BACKTICK fire on C++ &/&& (Qt connect(…, &Class::signal, …)), bitwise-or flag combinations, the statement terminator, and markdown backticks in doc comments; CS-VLN-SSRF-* on a fixed-loopback in-process simulator, on 192.168.x in comments next to code that denies non-discovered targets (AutomationServer.cpp:6960-6999), and on hex colour constants and VITA-49 OUIs; CS-SEC-PASSWORD on setEchoMode(QLineEdit::Password) and on the code that refuses to serialise masked fields. A substantive sweep found no QProcess/system()/popen sink in the audited non-GUI files and no unsafe C string functions anywhere in them.

Two pre-existing items outside this diff, credited to security-audit (CodeGuard), for separate routing — not this PR's to fix:

  • src/gui/CopyAssistController.cpp:1343 — the remote-ASR API key is persisted into plaintext AppSettings (setValue("AsrRemoteApiKey", …)) while the automation-bridge token already goes through QtKeychain (MainWindow_Session.cpp:3085). AGENTS.md's credential rule applies. The endpoint placeholder is also http:// with no scheme check, so the Bearer key can go in cleartext.
  • src/gui/SpectrumWidget.cpp:10140 — openUrl(QUrl("https://www.qrz.com/db/" + call)) concatenates an unencoded callsign that DxSpotLineParser.cpp:113 captures as bare (\S+) from a DX cluster. Bounded (authority already terminated; needs an operator right-click), but QUrl::toPercentEncoding() is the fix.

Socket tests: none added or modified. The two changed test files (tests/canon_window_test.cpp, tests/theme_manager_test.cpp) contain no QTcpServer/QUdpSocket/QLocalServer/bind/listen/connectToHost, and no fake peer.

What I tried to break

  • The new theme_manager_test block, by hand. Flattened both bundled theme JSONs and checked all 15 canon-scope mappings against color.canon.*. Every one matches — dark by repeated literal, light by {alias} — so the block passes, and drifting one dark hex by a digit fails it, as the body claims. The canon_window_test block likewise fails with the Kind::Workspace guard removed: the dialog arm asserts clicks == 2 on the same keystrokes, so it isn't asserting the implementation back to itself.
  • Every token the PR newly references. color.canvas.background, color.toggle.footer.{danger,success}.*, color.background.tx, color.background.warning, color.meter.gainReduction all resolve in both themes — no unresolved token (walked the nested JSON rather than grepping dotted paths, which returns nothing even when defined). That walk is also what surfaced the blocker.
  • "None of the converted files reads the three tokens that the applet/tx|rx|comp scopes override." Extracted all nine seedScopedToken("applet/…") entries and grepped every changed src/gui/*.cpp for color.knob.foreground, color.slider.foreground, color.toggle.accent.background.checked. Zero hits. The claim holds: making these widgets scope-aware cannot change a docked applet.
  • "The Strip*Panels trade their hard-coded hex colours for tokens." git diff | grep '^+' | grep -oE '#[0-9a-f]{6}' over the whole range yields 13 distinct literals: twelve are the canon-scope values in the two theme JSONs, and exactly one is in code — the #ffffff on the flashing LIMIT label. The sweep is complete.
  • "Strip panels are constructed only in these two windows." All 16 new Strip*Panel sites are in AetherRxDialog.cpp or AetherialAudioStrip.cpp. The re-skin cannot leak to a docked applet or floating editor.
  • Geometry carry-over. Base64 saveGeometry() blobs contain no comma, so split(',').size() != 4 and CanonWindow::restoreGeometryFromSettings() falls through to restoreGeometry() — old AetherRxDialogGeometry2 / AetherialStripGeometry2 rectangles do carry over, then get replaced in the new form on the next save.
  • Dangling pointer on WA_DeleteOnClose. ensureAetherRxDialog() now hand-rolls create-or-raise, so I checked the member: MainWindow.h:1692 is QPointer<AetherRxDialog>, which nulls on delete, so wasFresh is correct and the second open re-creates rather than dereferencing a freed dialog.
  • The deleted #4266 guard. AetherialAudioStrip lost FramelessResizer::install(this, 6, kTitleBarHeight) and the comment "Reserved from the frameless resize edge so a title-bar grab starts a move, not a top-edge resize (#4266)". The symptom can't recur: there is no title bar left to grab, and plain install(this) is what NetworkDiagnosticsDialog.cpp:350 and WaveformsDialog.cpp:939 already use.
  • The keyPressEvent guard's logic. & binds tighter than &&, so plainEnter parses as (mods & Keypad) && (key == Enter) — identical to QDialog's own test. event->ignore() can't leak Return to MainWindow either: Qt stops key propagation at a widget that is a window, and Kind::Workspace sets Qt::Window.
  • The deleted recolour / #08121d hack. tidyEmbeddedPanel() (StagePage.cpp:112-114) still performs that replacement, and no Strip*Panel still hard-codes #08121d — so removing the per-panel loop didn't orphan anything.
  • Dangling references to everything deleted. setFramelessMode, m_titleBar, m_titleLbl, m_bodyLayout, m_restoring, EditorFramelessTitleBar in the strip: no remaining reference in src/ or tests/.
  • Contrast of every text pair the canon scope introduces, both themes. All clear docs/a11y.md's 4.5:1; the lowest are dark muted on ground at 6.39:1 and light muted on ground at 5.94:1. The blocker above is a graphical-object pair, not text.

Couldn't check, and not counted as clean:

  • canonBodyStyleSheet()'s QWidget#canonBody QWidget { background: transparent; } is an ancestor sheet, which in Qt's cascade beats the application sheet. Any QLineEdit/QSpinBox/QComboBox in these windows that carries no sheet of its own loses the app-wide input background and sits directly on the bloom ground. You ran the real GUI and I can't — which input widgets were in the Default Light comparison?
  • Runtime behaviour generally: the compositor claims (Wayland/Hyprland tiling, edge resize, ground drag), the taskbar entry, and the "Return in a Compressor knob field switched to Gate" reproduction. All of my findings are reasoned from the source and from arithmetic on the theme JSONs, not reproduced at runtime.
  • CI at post time: check-macos, Static checks, Build docs site, Sanitizer configure and all three CodeQL analyses green; build and check-windows still in progress. Worth a glance before merge — check-windows is the only -DENABLE_ASR=OFF lane, though nothing here is ASR-gated.

Recommendation

Request changes — for the one token swap at StripFinalOutputPanel.cpp:208 and StripRxOutputPanel.cpp:84. Everything else is either a nit or a question. This is a careful, well-evidenced migration: the scope mechanism is the right seam, the two new test blocks genuinely fail when the behaviour they pin is broken, the "nothing changes outside a scope" claim survived the greps I aimed at it, and the hex-to-token sweep is complete to one literal. Next step: swap the meter-track token (and ideally move the gradient to color.meter.bar.fillGradient while you're in there), and link the maintainer ruling that picked these two windows the way #6273's status comment does. Thanks for the thorough test plan — listing the mutation that breaks each new assertion is what made this reviewable without a build.


🤖 aethersdr-agent · cost: $16.7141 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

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

  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/codeql.yml /tmp/aetherclaude/pr-6334/.github/workflows/codeql.yml:22
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/codeql.yml /tmp/aetherclaude/pr-6334/.github/workflows/codeql.yml:27
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6334/docs/architecture/aetherd-touchpoint-tags.json:10
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6334/docs/architecture/aetherd-touchpoint-tags.json:1145
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/AudioEngine.cpp /tmp/aetherclaude/pr-6334/src/core/AudioEngine.cpp:9742
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/AudioEngine.cpp /tmp/aetherclaude/pr-6334/src/core/AudioEngine.cpp:9743
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AudioEngine.cpp /tmp/aetherclaude/pr-6334/src/core/AudioEngine.cpp:1378
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AudioEngine.cpp /tmp/aetherclaude/pr-6334/src/core/AudioEngine.cpp:5432
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.cpp /tmp/aetherclaude/pr-6334/src/core/AudioEngine.cpp:9283
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.h /tmp/aetherclaude/pr-6334/src/core/AudioEngine.h:630
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.h /tmp/aetherclaude/pr-6334/src/core/AudioEngine.h:650
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.h /tmp/aetherclaude/pr-6334/src/core/AudioEngine.h:692
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.h /tmp/aetherclaude/pr-6334/src/core/AudioEngine.h:756
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.h /tmp/aetherclaude/pr-6334/src/core/AudioEngine.h:771
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.h /tmp/aetherclaude/pr-6334/src/core/AudioEngine.h:803
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AudioEngine.h /tmp/aetherclaude/pr-6334/src/core/AudioEngine.h:1025
  • [HIGH] CS-VLN-SSRF-PRIV-192 — Vulnerability: CS-VLN-SSRF-PRIV-192 in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:6967
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:12202
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:12203
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:6177
  • [CRITICAL] CS-VLN-CMDI-SEMI — Vulnerability: CS-VLN-CMDI-SEMI in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1757
  • [CRITICAL] CS-VLN-CMDI-SEMI — Vulnerability: CS-VLN-CMDI-SEMI in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:7455
  • [CRITICAL] CS-VLN-CMDI-SEMI — Vulnerability: CS-VLN-CMDI-SEMI in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:12993
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:2389
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:8418
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:9416
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:11437
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:11800
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:12359
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:299
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:463
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:840
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:949
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1306
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1379
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1531
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1608
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1753
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1783
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/AutomationServer.cpp /tmp/aetherclaude/pr-6334/src/core/AutomationServer.cpp:1787

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: $16.8913 · model: claude-opus-5

…ir pinned size, and keep colour lookups out of the paint loops. Principle VIII.

Fixes from the automated review of #6334:

- CanonWindow flushes saved geometry when it hides, not on close.
  Escape and Ctrl+W go through QDialog::reject(), which hides the window
  without a close event, so a position set before them was never written
  to disk.
- CanonWindow::setLaunchSize() pins the size on every open and applies it
  before centring. AetherRX and AetherTX used to resize after the base had
  centred the restored size, so a window with a size but no usable
  position (an unplugged monitor) landed off-centre.
- WaveformWidget resolves its bar colours once per paint rather than per
  column. Each scoped lookup walks the parent chain.
- The Final Output stage re-applies its OVR, LIMIT and activity styles only
  when they change, not on every meter tick.
- Removes tidyEmbeddedPanel()'s #08121d recolour, which no panel matches
  any more, the stale comments that referred to it, and MainWindow's
  WA_QuitOnClose call, which CanonWindow's workspace kind now sets.

canon_window_test covers both CanonWindow changes, and each test fails
when its fix is removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876

ten9876 commented Oct 10, 2026

Copy link
Copy Markdown
Collaborator Author

On the two security scans above.

  • CodeGuard (431 findings): none apply to this change. The file list came from a stale merge base: most listed files (AutomationServer.cpp, AudioEngine.cpp, codeql.yml, …) are untouched main content. The CMDI-* hits match QSS {{token}} braces and Qt && label escaping. The agent review above already refuted these at the source.
  • Antares (theme loader path traversal): this PR doesn't touch the loader. Its only theme-side changes are a canon scope added to the two bundled themes (resources/themes/*.json), the regenerated seed ThemeSeedGenerated.cpp, and a theme::withAlpha(widget, …) overload in ThemeManager.h. ThemeManager.cpp, loadThemeFromPath and every path handling are unchanged from main.

Automated code review (/code-review, run during landing), fixed in fe14ca6:

  • Escape and Ctrl+W didn't flush the saved geometry. CanonWindow now flushes on hide.
  • A pinned size was applied after centring. setLaunchSize() now applies it first.
  • Colour lookups ran per column in the paint loops. They now resolve once per paint.
  • The meter indicators were restyled on every tick. They now restyle only on change.
  • A dead recolour lambda, its stale comments and a redundant WA_QuitOnClose are removed.

Both CanonWindow fixes have tests that fail when the fix is removed.

Closed without change:

  • Transparent background on dialogs opened inside the window. Not reproduced. A QMessageBox opened from inside AetherRX, under a MainWindow-like parent with a window-wide QWidget background rule, renders opaque canon raised (#0a121e, alpha 255) with or without an extra rule.
  • Escape closes AetherTX. This is the canon close behaviour the maintainer chose; the user guide documents it.
  • #ffffff text on the red LIMIT and QUIN fills. The literal predates this PR; maintainer call to leave it.
  • Geometry format is one-way on downgrade. Maintainer call to keep the existing keys, as Network Diagnostics did. A downgrade only loses the saved position.
  • Two new ThemeManager.h includers (touchpoints 169→171). Accepted by the maintainer as ui-support; the manifest is regenerated.

@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 : 31
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 merged commit e32e3b8 into main Oct 10, 2026
10 checks passed
@ten9876
ten9876 deleted the canon-aether-rx-tx branch October 10, 2026 02:06
ten9876 added a commit that referenced this pull request Oct 10, 2026
…e to CanonWindow, with canon check boxes and radio buttons in every canon window. Principle VIII. (#6339)

## Summary

Part of #6239 (style guide follow-ups to RFC #6226). Three canon
migrations, one commit each.

**1. AetherModem moves to `CanonWindow`,** following the About window's
recipe for tool windows, as AetherRX and AetherTX did in #6334:
- No title bar, the ambient ground, a canon header 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.
- It's a `CanonWindow::Kind::Workspace`: an independent top-level window
with its own taskbar entry, where Return in a field never clicks a
default button.
- It stays a service host. MainWindow keeps one instance for the session
because it owns the KISS TCP server and the APRS modem, so closing it
only hides it.
- Geometry persists under its old key
(`Ax25HfPacketDecodeDialogGeometry`), so saved positions carry over.
- The minimum height grows from 680 to 720 px, the extra height of the
header that replaces the title bar, so the tabs' fields keep their
height.
- It sits under the `canon/aetherModem` theme scope. Its stylesheet and
the D-STAR page's are rewritten in `color.canon.*` tokens:
  - selection and focus are canon cyan (maintainer's choice);
  - status dots keep green, amber and red;
  - the log's rich-text colours resolve through the window's scope.

**2. Canon check boxes and radio buttons** (`gui/CanonIndicators.h`),
now the style guide's building block:
- **Look:** 18 px; ModemChrome's own indicator rules are removed from
its canon look, so only the painted images draw. A checked box is cyan
with an `onAccent` tick, a selected radio is a cyan ring with a centre
dot, and each has a muted disabled variant. Before, a checked box only
changed fill and a selected radio was only an outline, so it was hard to
see what was selected.
- **Painted:** they're drawn at 1x, 2x and 3x, because a stylesheet
border can draw neither a tick nor a clean translucent circle.
- **Cached safely:** they're cached through the new
`gui/ThemedImageCache.h`, which `ComboStyle.h`'s combo arrow now uses
too (maintainer's call to fold it in). It writes to the user's own cache
directory, never the shared temp directory, keyed by every colour
painted. Writes are atomic, and it fails closed: if the images can't be
written, the rules are left out and Qt's own indicators and arrow show
rather than blank ones. Paths are quoted in `url("...")`.
- **Theme:** `applyCanonSheet()` rebuilds the sheet on a theme change,
so the images follow the theme.
- **Everywhere:** every canon window uses them: AetherModem, AetherRX,
AetherTX, Network Diagnostics, Waveforms and Remote Access.

**ModemChrome gains a canon look** (`ModemChrome::Look::Canon`):
- **Slots:** every colour in its sheet is now a named slot filled per
look.
- **Modem look:** what the docked applets wear, and byte-identical to
`main` (13,481 bytes at each scale, compared against the merge base).
- **Canon look:** canon surfaces and ink, cyan selection, stage tabs in
ink, the canon indicators and a list card.
- **Who uses it:** AetherRX/AetherTX's stage column and AetherRX's
AetherNR page (`AetherDspWidget` in dialog mode). Those had kept the old
navy-and-green chrome.
- **AetherNR page:** its hard-coded label colours become text tokens,
and its painted parts resolve through the window's scope. In the docked
applet, primary labels are unchanged, while the secondary and disabled
labels move a shade to the theme's own values.

**3. The AetherRX and AetherTX Settings dialogs** (the profile library
behind the gear) are short-lived `CanonWindow` dialogs. They're still
modal over their window, use the canon look, and are 40 px taller for
the header.

**Docs:**
-
`docs/style/{aethersdr-style-guide,dialog-patterns,theme-style-guide}.md`
add the canon check box and radio button, ModemChrome's canon look, and
the new canon windows.
- The AetherModem user-guide page describes the window behaviour.

**Touchpoints:** `core/ThemeManager.h` includers go 171 → 173, from
`DStarModemPage.cpp` and the new `CanonIndicators.h`. Both are UI
theming (ui-support), and the manifest is regenerated.

**Stale screenshots, to recapture after merge:** `aethermodem-aprs.png`
(`aethermodem-packet-radio.md`), `aetherrx-profiles.png`
(`aetherial-audio.md`), and the AetherRX/AetherTX stage-column shots
`aetherrx.png`, `aethertx.png`, `aethertx-eq.png`,
`aethertx-compressor.png` and `first-session-aetherrx-nnr.png`. They
show the old chrome and green indicators.

**Known and unchanged from `main`:** the Mailbox tab's Listen callsign
and Vanity alias fields clip their text the same way on `main`.

## Constitution principle honored

Principle VIII (Evidence Over Assertion): each behavioural claim below
has a test that fails without its fix, a byte comparison, or a render.
The visual direction is RFC #6226's.

## Test plan

- [x] Local build passes
- [x] `canon_window_test` samples the 3x indicator images: the checked
box shows its tick, the selected radio its dot, and the unchecked ones
neither. Removing the tick and the dot fails it.
- [x] `modem_chrome_test`: both looks fill every `%N` argument and
`@slot@`, only the canon look carries the canon indicators, and the
canon look's only indicator rules are the painted ones.
- [x] `canon_window_test` also covers the cache:
  - the key changes when `aqua` or `line` change;
- an unwritable cache gives empty indicator rules, and they recover once
it's writable;
- the combo arrow's rule likewise disappears when blocked and names a
quoted path otherwise.
  
  Each of these fails when its fix is removed.
- [x] The modem look is byte-identical to the merge base at both scales:
old and new `ModemChrome.cpp` built side by side, and their output
compared.
- [x] `canon_window_test`, `modem_chrome_test`,
`stage_tab_bar_drag_test`, `theme_manager_test`, `theme_seed_test`,
`compact_metrics_test`, `rx_chain_runner_test`, `rx_stage_reorder_test`,
`window_show_state_test` and `comp_makeup_fader_test` pass locally.
- [x] The static-check scripts pass locally. The touchpoint manifest is
regenerated.
- [x] The user docs site builds (`npm ci && npm run build`), and
`gen_reference --check` passes.
- [x] Rendered offscreen with the demo app on scratch settings:
  - all six AetherModem tabs;
  - AetherRX (AetherNR and Gate) and AetherTX;
  - Network Diagnostics (Overview and Application Logs);
  - Waveforms;
  - both Settings dialogs;
  - a 2x render of the indicators.
- [x] The maintainer ran the AetherModem build against the FLEX-8600.
The check box and radio fix came from that run.
- [ ] Remote Access was not rendered. It only opens with a radio running
the tailnet shim connected; its change is the same shared indicator
rules.

## Checklist

- [x] Commits are signed
- [x] No new flat-key `AppSettings` calls. Existing geometry keys are
reused.
- [x] Code is clean-room
- [x] All meter UI uses `MeterSmoother` (unchanged)
- [x] Documentation updated:
`docs/user/docs/aethermodem-packet-radio.md`, `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 10, 2026
…, and every close path stops its clients and an armed beacon. Principle VI. (#6340)

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

- [x] Local build passes
- [x] 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`.
- [x] 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.
- [x] `canon_window_test`, `modem_chrome_test`, `window_show_state_test`
and `theme_manager_test` pass locally.
- [x] The static-check scripts pass locally. The touchpoint manifest is
unchanged.
- [x] The user docs site builds (`npm ci && npm run build`), and
`gen_reference --check` passes.
- [x] 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

- [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/psk-reporter-map.md` and
the pages that name it, `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 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
… and re-take the Stable snapshot. Principle VIII. (#6396)

## Summary

This brings the user guide (docs.aethersdr.com) up to date with
everything in v26.10.2 and re-takes the Stable snapshot so the default
site shows it. Docs only, apart from the TCI page prose in
`tools/docs/gen_reference.py`; no application source, test or CI
changes.

Every page was audited against the 106 PRs in the release. Every fact
that was added or kept was checked against the source at `f753dbfc4`,
the v26.10.2 release commit, not taken from the PR bodies alone.

## What changed

- **Menu paths.**
- AetherModem, Callsign Lookup, AetherMap and FreeDV Reporter are on
**Tools**. They are created on View and then moved to Tools
(`MainWindow_Menus.cpp:1519-1520`, `:1569-1575`); the style guide's
example says **Tools → AetherMap...**.
  - **Show live CW contacts** is on **View**.
  - Waveforms is on **Tools**, not File.
  - The VUDU applet button was called TXDSP.
  - Labels now match the source's `...` vs `…`.
- **Gaps from this cycle.**
- **Windows and Network Diagnostics:** Windows Firewall rules and the
Network Diagnostics Windows Firewall page (#6289); the `aether.network`
and `aether.nvafx` log categories (#6289, #6308).
  - **Remote access:** Tailscale shim revocation semantics (#6325).
  - **Sleep, App Nap and power throttling (#6195):** per-OS notes.
- **Hermes-Lite 2:** state restore, RX audio at unity, CW at TUNE level,
multi-interface discovery and a discovery troubleshooting entry (#6259,
#6254, #5975, #6009, #6293).
- **ANAN-G2:** AGC-T range, a "What is remembered" section, and supply
voltage and link status (#6282, #6129, #6137, #6211).
  - **NB2** on the HL2 and ANAN-G2 (#5966).
- **DVK:** WAV import requirements, the slot menu without Delete, status
text and licensing (#6247, #6262, #6251).
  - **Copy Assist** from unprocessed audio (#6216).
  - **Live CW contacts** replace the decoder's contact card (#6268).
  - **Status-bar indicators** work from the keyboard (#6267).
- **Volume:** controllers, hotkeys and TCI `volume` follow the active
output (#6129, #6260).
- **Peripherals:** the KPA1500 (#5905), CTR2 link extensions and the
AetherKnob (#6338), TMate 2 udev (#6250), MIDI RADE Modem and FDV modes
(#6031, #6038).
  - **RTL-SDR:** live PPM (#6082).
  - **Icom:** COMP and the MK2 squelch scale (#6174, #6184).
- **Known issues.** Entries whose GitHub issues are closed are removed
(each checked with `gh issue view`). As decided, this includes three
closed today without a demonstrated fix: #5107, #4824 and #5665.
- **US spelling.** `docs-style-guide.md` asked for British spelling; it
now asks for US, by the maintainer's decision, and every page is
converted. The generated TCI pages are converted at their source, the
prose strings in `tools/docs/gen_reference.py`. Code spans, links and
URLs were left alone. UI labels were checked against the source strings,
and none of them are British. Two in-page anchors and two Log Analyzer
rule anchors (`#bnr-is-grayed-out-…`) follow the renamed headings.
- **Stable snapshot.** `snapshot_stable.py 26.10.2` was re-run on the
release tree with these corrections, before `main` was merged in, so
Stable carries no post-release docs. `docs/` also has `main`'s
post-release pages, in US spelling. Stable alone keeps the IC-9700
waterfall-rate entry (#5996): #6402 fixed it after the 26.10.2 tag, so
26.10.2 still has it. The PDF manual already attached to the v26.10.2
release keeps the tag's docs.

## Validation

- `npm ci && npm run build` in `docs/user`: success. This is the
broken-link and broken-anchor check.
- `node tools/docs/test_log_rules.mjs`: 707 checks passed for 45 rules.
- `python3 tools/docs/gen_reference.py --check`: up to date (8 pages).
- `git diff --check`: clean.
- Menu paths on a build of this branch (offscreen, isolated settings, no
radio), `menu list`: `Tools → AetherModem...`, `Tools → Callsign
Lookup...`, `Tools → AetherMap...`, `Tools → FreeDV Reporter...`, `Tools
→ Waveforms...`, `View → Show live CW contacts`.

## Screenshots now out of date (not recaptured)

These need a recapture pass on the radio:
- **Old panadapter title strip (#6386):** `panadapter`, `main-window`,
`first-session-*`, `multi-slice-two-slices`, `split-pair`,
`tnf-markers`, `offscreen-vfo-indicator`, `pan-3d-stacked-trace`,
`default-light-theme`.
- **Pre-canon windows (#6334, #6339, #6341, #6372):** `aetherrx*`,
`aethertx*`, `aethermodem-aprs`, `bnr-panel`, `connect-dialog`,
`connect-by-ip-card`, `connect-icom-rows`, `connect-smartlink-card`,
`rx-agc-calibration`.
- **Other:**
  - `title-bar` (lowercase tab states, #6281);
  - `menu-help` (#6294);
  - `display-panel` (#6371);
  - `support-diagnostics-dialog`;
  - `waveforms-dialog` and `tailnet-remote-access` (show shim 0.4.0);
  - `about-window` (v26.10.1);
  - `radio-setup-qrz`.

## Found during the audit, not fixed here

- **App string bug:** `TailnetShimDialog.cpp:628` tells the user to
"Connect remotely with Connect → Manual"; the real path is **File →
Connect to Radio... → Connect by IP**.
- **ANAN AGC-T tooltips:** the RX applet and VFO tooltips show the
slider position as dB, which is wrong on ANAN. #6282 lists this as a
follow-up.
- **MIDI Master Volume on non-Flex radios:** it is refused while PC
Audio is off, whereas hotkeys, FlexControl and HID drive the line out.
The docs describe the code as it is.
- **RTL mode names:** `rtl-sdr.md` writes "FM-N" and "CW-R", but the
source strings are `FMN` and `CWR`. Left as is until someone confirms
what the mode menu displays.
- **IC-705 COMP readback:** inferred from the MK2 (#6174) and not
verified on an IC-705.
- **Not yet documented:** the #6230 Radio Health row "Forward responses
skipped", and the Store/MSIX package.

🤖 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