Skip to content

feat(gui): AetherModem and the AetherRX/AetherTX Settings dialogs move to CanonWindow, with canon check boxes and radio buttons in every canon window. Principle VIII. - #6339

Merged
ten9876 merged 4 commits into
mainfrom
canon-aether-modem
Oct 10, 2026
Merged

ten9876 merged 4 commits into
mainfrom
canon-aether-modem

Conversation

@ten9876

@ten9876 ten9876 commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

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

  • Local build passes

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

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

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

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

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

  • The static-check scripts pass locally. The touchpoint manifest is regenerated.

  • The user docs site builds (npm ci && npm run build), and gen_reference --check passes.

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

  • Commits are signed
  • No new flat-key AppSettings calls. Existing geometry keys are reused.
  • Code is clean-room
  • All meter UI uses MeterSmoother (unchanged)
  • Documentation updated: docs/user/docs/aethermodem-packet-radio.md, docs/style/*
  • Security-sensitive changes: n/a

🤖 Generated with Claude Code

ten9876 and others added 3 commits October 9, 2026 21:05
…canon palette. Principle VIII.

AetherModem follows About's recipe for tool windows, as AetherRX and AetherTX
do:
- CanonWindow chrome: no title bar, rounded corners, the ambient ground, a
  canon header and a corner close button. Drag any empty area to move it,
  resize it from every edge, and close it with Escape or Ctrl+W.
- Kind::Workspace: an independent top-level window with its own taskbar
  entry. Return in a field never clicks a default button, and an open window
  doesn't keep the app running.
- Geometry persists under the key it used as a PersistentDialog, so saved
  positions carry over.

It is still 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. It no longer follows View -> Frameless Window. Its minimum height
grows from 680 to 720 px, the height of the canon header that replaces the
title bar, so the tabs' fields keep their height.

Re-skin: the window sits under canon/aetherModem. kAetherModemStyle and the
D-STAR page's stylesheet are rewritten in color.canon.* tokens, using Network
Diagnostics' vocabulary:
- content panels are canon raised with a hairline, fields are control,
  tables and logs are nested;
- selection and focus are canon cyan (tabs, radios, checkboxes, field focus,
  the unread-mail button). Check boxes and radio buttons are painted canon
  indicators (gui/CanonIndicators.h): a checked box has a tick, a selected
  radio has a centre dot, and both have disabled variants;
- status dots keep green, amber and red through color.accent.*, the
  experimental banner and error text keep the warning and danger tokens.
The log's rich-text lines resolve their colours through the window's scope,
since a stylesheet token can't reach them. The rate graphs already used
scoped lookups.

The user guide's AetherModem page describes the new window behaviour.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… in every canon window, and the AetherNR page wears the canon look. Principle VIII.

The canon indicators (gui/CanonIndicators.h) are now the style guide's check
box and radio button:
- 18 px, control fill, lineHi edge;
- 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 this, a checked box only changed fill and a selected radio was only an
outline, so the selection was hard to read. They are painted at 1x, 2x and 3x,
because a stylesheet border can draw neither a tick nor a clean circle in the
translucent canon hairline. applyCanonSheet() applies a sheet that carries
them and rebuilds it on a theme change, since the images bake the theme's
colours.

Every canon window uses them: AetherModem, AetherRX, AetherTX, Network
Diagnostics (its flat squares removed), Waveforms (likewise) and Remote Access.

ModemChrome gains Look::Canon. Every colour in its sheet is now a named slot
filled per look. Look::Modem produces the sheet byte-for-byte as before (13,481
bytes, checked against the merge base at both scales), so the docked applets
are unchanged. Look::Canon gives:
- canon surfaces and ink;
- cyan selection and focus;
- stage tabs in ink, as page names rather than cyan buttons;
- the canon indicators.
AetherRX/AetherTX's stage column and AetherRX's AetherNR page
(AetherDspWidget in dialog mode) use the canon look. The AetherNR page's
hard-coded label colours become text tokens, and its painted reset glyph,
status dot and slider tick resolve through the window's scope. The docked
applet's labels now take the same tokens: primary is unchanged, and secondary
and disabled move a shade to the theme's own values.

canon_window_test samples the 3x images: the checked box shows the tick, the
selected radio shows the dot, and the unchecked ones show neither. It fails
with the tick and dot removed. modem_chrome_test checks that both looks fill
every slot, and that only the canon look carries the indicators.

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

The profile library behind each window's Settings gear moves to a short-lived
CanonWindow dialog, still modal over its window. It gets:
- no title bar, the canon ground, a canon header and the corner close button
  (Escape and the Close button close it too);
- ModemChrome's canon look, applied with applyCanonSheet() so the painted
  check boxes and radio buttons follow a theme change.
The canon look gains a list rule, so the profile list sits on a nested card
with a control-filled selection. The modem look is still byte-identical to
main. The default size grows from 460x380 to 460x420 for the header.

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 10, 2026 04:48
@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 : 16
Files with findings: 1
Total findings : 1

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

Findings are warnings only — the build is not blocked.

@ten9876 ten9876 self-assigned this Oct 10, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Implements the surfaces explicitly picked in tracking issue #6239 under approved RFC #6226: AetherModem becomes a persistent service-host workspace on CanonWindow; the RX/TX profile libraries become short-lived canon dialogs; the shared indicators and canon ModemChrome look reach the named windows. No blocking correctness or governance defect found. The custom-theme cache and screenshot follow-ups below are nonblocking.

Scope

File/group Change; claimed in title/body? Verdict
Ax25HfPacketDecodeDialog.{h,cpp}, MainWindow_DigitalModes.cpp CanonWindow workspace, old geometry key, canon styles; yes In approved scope; service ownership retained
AetherRxSettingsDialog.{h,cpp}, AetherTxSettingsDialog.{h,cpp} Modal profile libraries on CanonWindow; yes In approved scope
CanonIndicators.h, ModemChrome.{h,cpp} Painted states, per-look colours, theme rebuild helper; yes In approved scope; cache nit below
AetherDspWidget.cpp, StageTabBar.cpp Canon dialog look and scoped painting; disclosed applet label token cleanup In scope; shared-widget effect disclosed
DStarModemPage.cpp, NetworkDiagnosticsDialog.cpp, TailnetShimDialog.cpp, WaveformsDialog.cpp Canon tokens/shared indicators; yes In approved scope
Two modified test sources Indicator pixel sampling and both-look sheet checks; yes In scope; mutation checks detect removed drawings/rules
Style/user docs and touchpoint manifest Behaviour/style documentation and ThemeManager includer count; yes In scope; stale screenshots need disclosure

All three commits were authored October 10 UTC (October 9 Pacific). No unrelated commit or file, new dependency/thread, base-token change, new settings key, CHANGELOG entry, or weakened baseline found. The two new ThemeManager includers explain the disclosed 171→173 manifest count; the UI-support classification is unchanged. The tracker explicitly lists all three migrations, so the bundled visual work has maintainer direction.

Blockers

None.

Nits — nonblocking

  1. Include every painted colour in the cache key — src/gui/CanonIndicators.h:78. The key omits line and aqua, although disabled outlines and the checked-box edge use them. A Qt 6.12 probe edited only these two root theme tokens, then regenerated the rules:

    changed line=#ff00ff aqua=#ff00ff rules_identical=1
    checked_box_top_edge=#8ef7e6 expected=#ff00ff
    

    The existing PNG is reused, so those theme edits do not reach the indicators. Normal Dark/Light switches may mask it by changing another keyed colour. Add the missing colours:

        const QString key = QString(control.name(QColor::HexArgb) + lineHi.name(QColor::HexArgb)
                                    + line.name(QColor::HexArgb) + cyan.name(QColor::HexArgb)
                                    + aqua.name(QColor::HexArgb) + onAccent.name(QColor::HexArgb)
                                    + muted.name(QColor::HexArgb)).remove(QLatin1Char('#'));

    This was reproduced through the image-generation helper in a standalone Qt process, not the desktop Theme Editor. A socket-free test changing only one omitted token would cover it.

  2. Refresh or disclose stale screenshots — docs/user/docs/aethermodem-packet-radio.md:31, docs/user/docs/aetherial-audio.md:206. The pages still embed aethermodem-aprs and aetherrx-profiles from before these window migrations; the changed screenshot files are absent and the PR body does not name stale IDs. The AGENTS user-doc workflow permits either refreshing affected shots or saying which are stale. Name these and the affected RX/TX stage-column shots in the body if captures are deferred.

What I tried to break

  • Built only AetherSDR and the ten named focused test targets, with pinned Qt 6.12, shared core, -O2 -g1 and isolated test settings. All ten pass. The first invocation lacked the offscreen platform for rx_chain_runner_test and aborted during Qt display initialization; setting QT_QPA_PLATFORM=offscreen resolves it. This is an environment issue, not a PR failure. Both modified tests and the additional selected targets were inspected for socket ownership; no socket-owning test was run.
  • Removed tick/dot drawing in separate scratch mutants: canon_window_test fails both positive pixel assertions. Removed the Canon indicator append: modem_chrome_test fails its new look check. Original exact-head tests pass. Mutation binaries use exact PR sources and unchanged pinned-Qt settings/theme support objects from the earlier review; no production sources were edited.
  • Compiled old/new ModemChrome side by side: the default Modem look is byte-identical at Dialog and Compact scales. Engine-boundary findings remain 102 with only shifted source lines; capability and command-plane findings are identical. Both manifests check clean; registration, frozen CI gate and generated user-reference checks pass. The accessibility bot warning at Ax25HfPacketDecodeDialog.cpp:3333 is pre-existing unchanged code, verified against the merge base.
  • Drove only owned review instances (PIDs 2346720, 2349613 and 2351782), offscreen with isolated settings, the explicit aethersdr-pr6339-review socket and TX disabled. The radio response was {"model":"AetherSDR Demo","serial":"DEMO-0001","connected":true,"transmitting":false}. Captured and inspected all five demo-visible modem tabs, RX/TX, both modal Settings dialogs, Network Diagnostics and an RX Dark→Light→Dark switch. Checked tick/dot state in actual window captures. Hiding/reopening the modem retained {"profileId":"Vhf1200","baud":1200,"enabled":false}. After resizing to 1180×860 and restarting, the bridge returned {"geometry":{"x":200,"y":12,"w":1180,"h":860}}. A separate legacy-base64 seed restored to {"geometry":{"x":1,"y":39,"w":1080,"h":760}}, matching Qt's existing restoration/clamping behavior on the same virtual screen. The first legacy attempt used a different virtual-screen size and did not preserve the requested rectangle, so it was repeated with matching configuration rather than classified as a PR regression. Slice removal and simulated disconnect completed without a crash. All owned instances were closed and their exit confirmed; no live radio was connected.

Verification limits and CI

The demo hides D-STAR and disables Waveforms, and Remote Access requires a connected tailnet-shim radio; those three changed surfaces were inspected in source but not rendered locally. No live KISS/APRS service was enabled, so continuity of an actively running service was checked through retained ownership/teardown code rather than live traffic. OS taskbar behavior, compositor move/resize gestures and native Windows/macOS runtime were not demonstrated. The shared-core build differs from shipping linkage; none of the findings depends on that choice. The desktop theme switch worked, but the two-colour cache nit was reproduced in the standalone helper rather than through Theme Editor. A full local default-target build or broad suite was not run.

Required build, check-windows, check-macos and Static checks are successful. Head is 8f53d2b76438e34ec3deaec182b9f0aea263f5bc; CI/Static/Docs runs were created 2026-10-10T04:48:41Z, after current main ca5e0634d3 (2026-10-10T02:35:53Z). No stale-main timing gap found. The docs-site CI build passed; I did not repeat npm installation/build locally. The optional automated code-review skill was unavailable; manual and parallel audits were performed.

Recommendation: Approve with nits. The picked migrations, retained service ownership and focused regressions hold up to the checks above. Fix the cache key and refresh or disclose affected screenshots. This is a COMMENT review, not an approval; approval and merge remain the operator's call.

@ten9876 ten9876 removed their assignment Oct 10, 2026

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Issue fit

Yes, for all three picked items. #6239's "Picked: tool windows on CanonWindow" names this PR against exactly three lines — AetherModem (Ax25HfPacketDecodeDialog), the AetherRX/AetherTX Settings dialogs, and the canon check boxes/radio buttons plus ModemChrome's canon look — so three items in one PR is the maintainer's own tracking ruling, not a bundling violation. Each lands as its own signed commit. The docs half of the ask is there too: dialog-patterns.md, the style guide, theme-style-guide.md and the user page all gained the AetherModem rows, and the user page documents the chrome change the operator will actually notice (no title bar, Esc/Ctrl+W, closing only hides).

One claim I want to flag as not holding up: src/gui/ModemChrome.cpp (end of styleSheet) says of the canon indicators "their rules are more specific than the ones above". They are not — see nit 1.

Scope

File / group What it changes Claimed? Verdict
docs/style/{aethersdr-style-guide,dialog-patterns,theme-style-guide}.md, docs/user/docs/aethermodem-packet-radio.md AetherModem + Settings added to the canon lists; new style-guide rows for the indicators and the canon ModemChrome look; user-facing chrome paragraph Yes In scope
docs/architecture/aetherd-touchpoints.md ThemeManager.h 171→173 Implied In scope — two new includers (CanonIndicators.h, DStarModemPage.cpp); the manifest check is green
src/gui/CanonIndicators.h (new) Painted canon indicators + applyCanonSheet() Yes In scope; referenced by 6 TUs — not a dead addition
src/gui/Ax25HfPacketDecodeDialog.{h,cpp}, src/gui/DStarModemPage.cpp PersistentDialog → CanonWindow::Kind::Workspace; sheet and log HTML to tokens Yes In scope (the D-STAR page is only ever a tab inside AetherModem)
src/gui/MainWindow_DigitalModes.cpp drops setFramelessMode() ×3 and trackPersistentDialog() ×3 Yes Correct and necessary — the class is no longer a PersistentDialog; the only thing lost is View → Frameless propagation, which dialog-patterns.md documents as deliberate
src/gui/Aether{Rx,Tx}SettingsDialog.{h,cpp} QDialog → CanonWindow, canon body sheet, 460×380 → 460×420 Yes In scope
src/gui/ModemChrome.{h,cpp}, src/gui/StageTabBar.cpp Look::{Modem,Canon}; stage column takes Canon Yes In scope — StageTabBar is only built by AetherRxDialog.cpp:129 and AetherialAudioStrip.cpp:138, both canon windows
src/gui/{NetworkDiagnostics,TailnetShim,Waveforms}Dialog.cpp delete local fill-only indicator rules, append canonIndicatorRules() Yes ("in every canon window") In scope
src/gui/AetherDspWidget.cpp setDialogMode applies the canon sheet; ModemChrome::colour(…, this); label hex → {{color.text.*}} Mostly In scope, with one undisclosed side effect — nit 4
tests/{canon_window,modem_chrome}_test.cpp indicator pixel assertions; canon-look sheet assertions Yes In scope

Nothing in the diff is unexplained by #6239. No new protocol verb, settings key, capability or CLI surface. No CHANGELOG.md entry (correct). No removed guard; the one deleted comment block (AetherDspWidget.cpp:703, "Nothing left to do per-widget…") is replaced by a real implementation, not by a regression.

Socket tests: none added, modified or removed. No QTcpServer/QUdpSocket/bind/listen/connectToHost and no Fake* peer in either changed test; both are offscreen widget tests.

Blockers

1. src/gui/CanonIndicators.h:45-58 — the indicator cache fails open, and the failure mode is invisible check boxes.

    if (QFile::exists(path) && QFile::exists(base + QStringLiteral("@3x.png"))) {
        return path;
    }
    for (int scale = 1; scale <= 3; ++scale) {
        ...
        pm.save(scale == 1 ? path : base + QStringLiteral("@%1x.png").arg(scale), "PNG");
    }
    return path;

pm.save()'s return value is discarded and return path; runs unconditionally, so a path that was never successfully written still gets interpolated into the sheet:

        "QCheckBox::indicator, QRadioButton::indicator {"
        " width: 18px; height: 18px; border: none; background: transparent; }"
        "QCheckBox::indicator:unchecked { image: url(%1); }"

Because the same rule set clears the border and the background, a missing image leaves nothing drawn at all — not a degraded box, an empty one. Every check box and radio button in every canon window (AetherModem's Enable Modem and Autostart, Network Diagnostics, Waveforms, Tailnet, the AetherNR page, the stage column) would be blank, both states identical. That is precisely the outcome the header and the new style-guide row say must never happen: "Never a fill or ring alone: the operator must be able to see what is selected."

Reachable without an adversary: a read-only or full TMPDIR, a restrictive TMPDIR on a hardened host, or an existing unwritable file at that name. And with one — this is the confirmed half of the security audit (security-audit (CodeGuard), codeguard-0-file-handling-and-uploads, medium, Linux shared /tmp only; macOS /var/folders and Windows %TEMP% are per-user) — the name is fully predictable (aethersdr_canon_<one of 8 literals>_v1_<hex of five shipped theme tokens>), so a local user can pre-create both files and have Qt decode their PNG for every indicator in the app: UI-state spoofing on toggles that gate TX-adjacent behaviour, plus untrusted bytes into libpng. If only <base>.png is pre-created, the two-file gate falls through, save() fails silently, and the attacker's file is used anyway.

Worth saying plainly: src/gui/ComboStyle.h:32 already does this (temp dir, colour-keyed name, single-file gate, unchecked save()), so the pattern is pre-existing and hardening it everywhere belongs in its own issue — this PR is only on the hook for the new copy. Fail-closed plus a per-user directory covers both the deterministic break and the local-attacker case:

inline QString canonIndicatorPath(const QString& name, const QString& colourKey,
                                  const IndicatorPainter& paint)
{
    constexpr int kSize = 18;
    // Bump when the drawing changes: the cache is keyed by colours, so an old
    // file would otherwise outlive a new design.
    constexpr int kDrawingVersion = 1;
    const QString dir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
                        + QStringLiteral("/canon-indicators");
    if (!QDir().mkpath(dir)) {
        return {};
    }
    const QString base = dir + QStringLiteral("/%1_v%2_%3")
        .arg(name).arg(kDrawingVersion).arg(colourKey);
    const QString path = base + QStringLiteral(".png");
    if (QFile::exists(path) && QFile::exists(base + QStringLiteral("@2x.png"))
        && QFile::exists(base + QStringLiteral("@3x.png"))) {
        return path;
    }
    for (int scale = 1; scale <= 3; ++scale) {
        QPixmap pm(kSize * scale, kSize * scale);
        pm.fill(Qt::transparent);
        QPainter p(&pm);
        p.setRenderHint(QPainter::Antialiasing);
        p.scale(scale, scale);
        paint(p);
        p.end();
        // Fail closed: a path we did not write would render as no indicator at
        // all, since the rules below clear the border and the background.
        if (!pm.save(scale == 1 ? path : base + QStringLiteral("@%1x.png").arg(scale), "PNG")) {
            qCWarning(lcGui) << "canon indicators: cannot write" << path;
            return {};
        }
    }
    return path;
}

…and in canonIndicatorRules(), return QString() if any of the eight paths came back empty, so the window falls back to Qt's native indicators instead of blank squares. (Needs <QStandardPaths>; lcGui or a local category for the warning.)

Two smaller things to fold into the same edit:

  • The cache key omits two tokens it paints with. key is built from control, lineHi, cyan, onAccent, muted (CanonIndicators.h:78-80), but aqua is the checked box's edge (:117) and line is the disabled edge (:119, :127). Two themes differing only in color.canon.aqua or color.canon.line share a filename and serve each other's images — the exact collision the header comment says the key prevents. Add both.
  • image: url(%1) embeds the path unquoted. A temp path containing a space is routine on Windows (C:/Users/John Smith/AppData/Local/Temp/…), and Qt's CSS url() token is not reliable for unquoted paths with spaces or parens. Quoting it (image: url("%1")) is free insurance, and with CacheLocation the path still comes from the user profile. I could not render this to confirm Qt's lexer behaviour — flagging it as cheap to make moot rather than as a proven break. (tests/canon_window_test.cpp:248's url\\(([^)]+)\\) regex would need the quotes stripped.)

Nits

  • src/gui/ModemChrome.cpp (end of styleSheet) — "their rules are more specific than the ones above" is not right for the state rules. canonIndicatorRules()'s generic QCheckBox::indicator, QRadioButton::indicator { border: none; background: transparent; } wins over the base unchecked block only on source order (equal specificity), and loses to the base QCheckBox::indicator:checked { background: @checkFillChecked@; border-color: %18; } and QRadioButton::indicator:checked:hover { border-color: %19; }, which are more specific. So in the canon look a checked box/radio should still get a 2 px cyan/aqua QSS ring drawn around the painted 18 px image. The three dialogs in this very diff took the clean route — NetworkDiagnosticsDialog.cpp, WaveformsDialog.cpp and TailnetShimDialog.cpp all delete their indicator blocks. Doing the same here (elide the QRadioButton::indicator* / QCheckBox::indicator* blocks when look == Look::Canon) removes the question. Reasoned from QSS specificity; I can't render it to show the ring.
  • src/gui/CanonIndicators.h:150-158 — applyCanonSheet() double-registers. ThemeManager::applyStyleSheet already tracks the widget and reapplies on themeChanged (ThemeManager.cpp:354), so each theme switch applies twice: first the tracked reapply, which re-expands the stored template still carrying the old theme's PNG paths, then this connection's rebuild with the new key. Correct only because ThemeManager's connection is made in its constructor and therefore runs first. Worth one line of comment, or skipping the tracked path for these sheets.
  • tests/canon_window_test.cpp:248 and tests/modem_chrome_test.cpp:122 write the same eight temp PNGs; whichever runs second can hit the two-file exists-gate against a partially written @3x.png under ctest -j and sample torn bytes. Today they differ (modem_chrome_test links no ${THEME_TEST_RESOURCES}, so its canon tokens come from the seed and the colour key differs), so this is latent, not live. Writing to a per-test CacheLocation under TestSettingsProfile would close it for good.
  • src/gui/AetherDspWidget.cpp:755-760, :1230-1231, :1462-1463 — the hex→token migration changes the docked AetherNR applet too, not just the canon page, and the body doesn't mention it. In dark it is near-identical (#c8d8e8 → color.text.primary = #c8d8e8 exactly; #8090a0 → color.text.secondary = #8ea8c0; #48515a → color.text.disabled = #3a4a5a) and in light it is a strict improvement, so this reads as a welcome ratchet payment rather than a smuggled restyle — but #6239 says "nothing here authorises restyling a screen as a side effect of other work", so it is worth a line in the PR body.
  • tests/modem_chrome_test.cpp:122 asserts only that the canon sheet contains aethersdr_canon_check_on — it would still pass with an empty tick. The real guard is canon_window_test's pixel sampling, which would fail if the tick path or the dot were dropped. Good pairing; no change wanted, just noting which half carries the weight.

What I tried to break

  • "Saved geometry carries over" (Ax25HfPacketDecodeDialog.cpp:890). PersistentDialog stored saveGeometry().toBase64(); CanonWindow stores "x,y,w,h". The claim holds: a base64 blob has no ,, so split(',') yields 1 part and CanonWindow.cpp:188 falls through to the restoreGeometry(fromBase64(...)) branch, then the next save rewrites it in the new form. The minimumSize bump 680→720 is absorbed by expandedTo(minimumSize()) at :173.
  • The shared-instance trap on setDialogMode. It used to be a no-op and is now a one-way canon restyle, and the style guide calls the AetherNR page "the docked applet's widget" — so I checked whether the docked applet and AetherRX share one AetherDspWidget. They don't: AetherRxDialog.cpp:172 and ClientRxDspApplet.cpp:28 each construct their own, and setDialogMode(true) has exactly one caller (AetherRxDialog.cpp:173). The docked applet keeps Look::Modem. This was my leading blocker candidate and it did not survive.
  • Return/Enter after Kind::Workspace. AetherModem has three QLineEdit::returnPressed paths (TX text :1214, terminal :3937, APRS message :4602). None relied on auto-default: :1479-1482 already clears setAutoDefault/setDefault on every body button (pre-existing, commit 6af248b), and Workspace additionally suppresses default buttons. closeSettingsIfOpen()'s reject() and both dlg.exec() call sites still compile and behave, since CanonWindow derives from QDialog.
  • What dropping trackPersistentDialog() costs. Read MainWindow::setFramelessWindow (MainWindow.cpp:10202): the tracked list is used for setFramelessMode() propagation only — no shutdown geometry flush, no disconnect teardown. Nothing else is lost, and CanonWindow::hideEvent (:138) does the geometry flush Escape/⌘W needs.
  • Unresolved theme tokens. Extracted every {{token}} added across Ax25HfPacketDecodeDialog.cpp, DStarModemPage.cpp, AetherDspWidget.cpp, ModemChrome.cpp and CanonIndicators.h and resolved each against both shipped themes — all present (color.accent.* live as flat dotted keys, so a naive nested walk reports false misses). All seven color.canon.* the indicators paint with are also in ThemeSeedGenerated.cpp, so the compiled-seed fallback doesn't render them transparent. canon_window_test does link ${THEME_TEST_RESOURCES}, so its pixel assertions run against the real palette rather than an all-zero one.
  • CodeGuard's 13 handed-down leads: all refuted (security-audit (CodeGuard) re-read every cited line). The eight CS-VLN-CMDI-BACKTICK hits are Markdown backticks in C++ comments or in operator-help strings passed to automationError(); CS-PII-DOB-ISO is "Observed 2026-07-31 on the HL2" in a comment; the three SSRF hits are a comment about AppSettings keys, a comment showing "127.0.0.1:51234 (wsjtx)" as a display format, and a QLineEdit placeholder; CS-PII-EMAIL is you@example.com placeholder text (RFC 2606); CG-PATH-001 is compile-time ../Resources/ bundle probing feeding a QFileDialog start directory; CS-VLN-PATH-UNC is "^[\x20-\x7B\x7D-\x7E]{0,20}$", a D-STAR message validator, not a path. Only the CanonIndicators.h file-handling finding survived.
  • Could not check: anything requiring a run. No build, no GUI, so the double ring, the blank-indicator failure and the Windows url() lexing are all reasoned from the code and from Qt's documented QSS behaviour, not observed. CI is green on 8f53d2b across all 10 checks — but note that ci.yml only ever runs ctest -R against a named handful (lines 820, 832, 1174, 1189); neither canon_window_test nor modem_chrome_test is among them, so green does not mean the new assertions passed anywhere.

Recommendation

Request changes — on blocker 1 alone. The three-window move itself is careful, well-documented work that survived everything I threw at it: the geometry migration, the Return-key seam, the shared-widget trap, the propagation that got deleted, and every token. What needs fixing is one new helper that hands back a file path it never confirmed writing, into rules that clear the border and the background, so the failure renders as no indicator at all; the fix is mechanical and the cache-key and quoting tweaks ride along in the same edit. Nit 1 (dropping the shadowed indicator blocks from Look::Canon, as the other three dialogs in this diff already do) is the one I'd most like to see taken even though it's non-blocking. The ComboStyle.h:32 twin should get its own hardening issue rather than being pulled in here. Thanks for the user-facing paragraph on the new chrome — that's the part operators will actually hit first.


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

@aethersdr-agent

Copy link
Copy Markdown
Contributor

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

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

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


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

…cally and fail closed, and canon windows draw only the painted indicators. Principle VIII.

Review of #6339.

- New gui/ThemedImageCache.h: one cache for the small theme-coloured PNGs a
  stylesheet names. It writes to the user's own cache directory, not the
  shared temp directory, where another local user could plant a file under
  the predictable name. Each file is written atomically with QSaveFile, and a
  path is returned only once every scale was written.
- The canon indicators and ComboStyle's combo arrow both use it. If the
  indicators can't be written, canonIndicatorRules() comes back empty and the
  window keeps Qt's own check boxes. Before, it named a missing file, and the
  rules clear the border and background, so nothing at all was drawn. The
  arrow's rule is likewise left out, so Qt's own arrow shows.
- The indicator cache key now covers every colour painted, including aqua
  (the checked edge) and line (the disabled edge).
- Image paths are quoted in url("..."), so a Windows profile name with a
  space still parses.
- ModemChrome's canon look removes its own indicator rules before adding the
  painted ones. Its :checked rules were more specific than the canon reset
  and drew a border around the image. The modem look is still byte-identical
  to main.
- applyCanonSheet() documents why its rebuild lands after ThemeManager's own
  reapply.
- AetherModem drops a loop that cleared auto-default buttons, which the
  workspace kind's Return handling made dead.

canon_window_test now covers:
- the cache key changes with aqua and with line;
- an unwritable cache gives empty indicator rules and recovers once writable;
- the same holds for the combo arrow, with a quoted path.
modem_chrome_test checks that the canon look's only indicator rules are the
painted ones. Each fails when its fix is removed.

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

Copy link
Copy Markdown

Accessibility check findings

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

Files scanned : 18
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 commented Oct 10, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed in 20f28a9.

@aethersdr-agent: blocker, the indicator cache fails open. Fixed by the new src/gui/ThemedImageCache.h, which both the canon indicators and ComboStyle.h's combo arrow now use (the maintainer's call to fold the arrow in):

  • Location: it writes to the user's own QStandardPaths::CacheLocation, never the shared temp directory.
  • Atomic writes: each file goes through QSaveFile.
  • Fails closed: a path comes back only once every scale was written. Otherwise canonIndicatorRules() and comboArrowRule() return nothing, and the window keeps Qt's own indicators and arrow.

canon_window_test blocks the cache directory with a regular file. The indicator rules and the arrow rule then come back empty, and both recover once the block is removed. Mutation: returning a path despite the failure fails it at canon_window_test.cpp:316 (indicators) and :329 (arrow).

Both reviews: cache key missing line and aqua. Fixed; the key now covers all seven colours painted. canon_window_test changes only color.canon.aqua, then only color.canon.line, and expects a new rule set each time. With the two colours removed from the key it fails at :301 and :302.

Unquoted url(). Fixed: url("...") for both the indicators and the arrow. The test's regex reads the quoted form, and the arrow test asserts the quote.

Nit: ModemChrome's :checked indicator rules outrank the canon reset. You were right, and the comment was wrong. The canon look now removes ModemChrome's own indicator rules before adding the painted ones. modem_chrome_test asserts that the canon sheet's only ::indicator rules are the painted ones; keeping the old rules fails it. The modem look is still byte-identical to the merge base at both scales (13,481 bytes, old and new ModemChrome.cpp built side by side).

Nit: applyCanonSheet() registers twice on a theme change. It's documented in place now. ThemeManager's own reapply runs first, because its connection is made in its constructor, so the rebuild with the new theme's images lands last.

Nit: tests sharing temp PNGs under ctest -j. Closed by the above. Each test now writes to its own TestSettingsProfile cache through atomic writes.

Nit: the docked applet's label change is undisclosed. It's in the description: "In the docked applet, primary labels are unchanged, while the secondary and disabled labels move a shade to the theme's own values."

@ten9876: stale screenshots. The description now names them: aethermodem-aprs.png, aetherrx-profiles.png, aetherrx.png, aethertx.png, aethertx-eq.png, aethertx-compressor.png and first-session-aetherrx-nnr.png.

Automated /code-review during landing. Its findings 1–3 and 5 are the fixes above. #9 is also fixed: AetherModem's loop clearing auto-default buttons is removed, because the workspace kind's Return handling made it dead. #7 is closed without change: AetherModem's log lines were fixed hex on main; new lines now follow the theme. Closed without change by maintainer decision:

  • the 720 px minimum height stays;
  • the indicators keep the root theme's colours (no bundled theme overrides color.canon.* per scope);
  • cached image sets aren't pruned (per-user, about 1 KB each).

Accessibility bot (Ax25HfPacketDecodeDialog.cpp:3333). Pre-existing code that this PR doesn't change; @ten9876's review checked it against the merge base.

CodeGuard. All 13 leads were refuted in the agent review above. The one real file-handling finding is the cache fix in this commit.

Verification at 20f28a9:

  • The build and 11 focused tests pass, including ctcss_tone_label_test for ComboStyle.
  • The static checks pass, and the touchpoint manifest is unchanged.
  • The offscreen demo app renders the indicators and the combo arrows from ~/.cache/AetherSDR/AetherSDR/{canon-indicators,combo-arrows}/ with no stylesheet parse warnings.

@ten9876
ten9876 merged commit 2eb01f4 into main Oct 10, 2026
10 checks passed
@ten9876
ten9876 deleted the canon-aether-modem branch October 10, 2026 08:03
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 10, 2026
…eir canon indicators again. Principle VIII. (#6345)

## Summary

Part of #6239. Fixes a regression from #6339: in AetherRX and AetherTX,
unchecked check boxes and radio buttons drew nothing. A checked box
showed only a bare tick and a selected radio only a small dot, so it
wasn't clear what was selected.

**Cause:** ModemChrome's canon look takes the sheet's own
`QCheckBox`/`QRadioButton` indicator rules out before appending the
painted canon indicators (`canonIndicatorRules()`). The pattern ended
each rule at its first `}`. In the canon look those rules hold
`{{color.canon.*}}` placeholders, so every match stopped inside a token
and left `}};` fragments behind. Qt rejects a stylesheet it cannot parse
as a whole, so the stage column and the AetherNR page lost the entire
canon chrome sheet, not only their indicators.

**Fix:** the pattern now ends a rule body at the first brace outside a
token (`src/gui/ModemChrome.cpp`).

**Visible result:** the canon chrome sheet applies to the stage column
and the AetherNR page again. That restores:
- the painted canon indicators;
- the canon tab, button and slider rules, which is the look before
#6339's final commit.

The AetherRX and AetherTX Settings dialogs also use ModemChrome's canon
look, and they get the repaired sheet too. Windows whose canon sheet
doesn't go through ModemChrome were never affected: Connect to Radio,
AetherModem, AetherMap and Network Diagnostics.

## Constitution principle honored

Principle VIII (Evidence Over Assertion). The existing test only counted
`::indicator` selectors, so it passed while the sheet was broken. The
new checks test what Qt parses and draws.

## Test plan

- [x] Local build passes
- [x] `modem_chrome_test` gains two checks, and both fail with the old
pattern:
- `canonSheetLeavesNoRuleFragments`: with tokens set aside, every brace
in the canon sheet opens or closes a top-level rule.
- `canonIndicatorsRenderTheirImages`: a check box and a radio button
under the canon sheet draw the same pixels as under the canon indicator
rules alone, both checked and unchecked.
- [x] `modem_chrome_test`, `stage_tab_bar_drag_test` and
`canon_window_test` pass.
- [x] Before and after, rendered offscreen with scratch settings and the
radio disconnected: AetherRX and AetherTX grabbed through the automation
bridge. After the fix the stage-column boxes, the AetherNR radios and
the AE Filter / Noise fill boxes all draw the canon box, tick, ring and
dot.
- [x] `check_engine_boundary.py --strict` and
`gen_touchpoint_manifest.py --check` pass. No new touchpoints.

## Checklist

- [x] Commits are signed
- [x] No new `AppSettings` keys
- [x] Code is clean-room
- [x] All meter UI uses `MeterSmoother` (unchanged)
- [x] Documentation: n/a (restores the documented canon look)
- [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
…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