Repository navigation
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
Conversation
…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>
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
ten9876
left a comment
There was a problem hiding this comment.
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
-
Include every painted colour in the cache key —
src/gui/CanonIndicators.h:78. The key omitslineandaqua, 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=#ff00ffThe 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.
-
Refresh or disclose stale screenshots —
docs/user/docs/aethermodem-packet-radio.md:31,docs/user/docs/aetherial-audio.md:206. The pages still embedaethermodem-aprsandaetherrx-profilesfrom 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
AetherSDRand the ten named focused test targets, with pinned Qt 6.12, shared core,-O2 -g1and isolated test settings. All ten pass. The first invocation lacked the offscreen platform forrx_chain_runner_testand aborted during Qt display initialization; settingQT_QPA_PLATFORM=offscreenresolves 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_testfails both positive pixel assertions. Removed the Canon indicator append:modem_chrome_testfails 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:3333is 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-reviewsocket 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.
There was a problem hiding this comment.
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.
keyis built fromcontrol,lineHi,cyan,onAccent,muted(CanonIndicators.h:78-80), butaquais the checked box's edge (:117) andlineis the disabled edge (:119,:127). Two themes differing only incolor.canon.aquaorcolor.canon.lineshare 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 CSSurl()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'surl\\(([^)]+)\\)regex would need the quotes stripped.)
Nits
src/gui/ModemChrome.cpp(end ofstyleSheet) — "their rules are more specific than the ones above" is not right for the state rules.canonIndicatorRules()'s genericQCheckBox::indicator, QRadioButton::indicator { border: none; background: transparent; }wins over the base unchecked block only on source order (equal specificity), and loses to the baseQCheckBox::indicator:checked { background: @checkFillChecked@; border-color: %18; }andQRadioButton::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.cppandTailnetShimDialog.cppall delete their indicator blocks. Doing the same here (elide theQRadioButton::indicator*/QCheckBox::indicator*blocks whenlook == 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::applyStyleSheetalready tracks the widget and reapplies onthemeChanged(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:248andtests/modem_chrome_test.cpp:122write the same eight temp PNGs; whichever runs second can hit the two-file exists-gate against a partially written@3x.pngunderctest -jand sample torn bytes. Today they differ (modem_chrome_testlinks 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-testCacheLocationunderTestSettingsProfilewould 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=#c8d8e8exactly;#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:122asserts only that the canon sheet containsaethersdr_canon_check_on— it would still pass with an empty tick. The real guard iscanon_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).PersistentDialogstoredsaveGeometry().toBase64();CanonWindowstores"x,y,w,h". The claim holds: a base64 blob has no,, sosplit(',')yields 1 part andCanonWindow.cpp:188falls through to therestoreGeometry(fromBase64(...))branch, then the next save rewrites it in the new form. TheminimumSizebump 680→720 is absorbed byexpandedTo(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 oneAetherDspWidget. They don't:AetherRxDialog.cpp:172andClientRxDspApplet.cpp:28each construct their own, andsetDialogMode(true)has exactly one caller (AetherRxDialog.cpp:173). The docked applet keepsLook::Modem. This was my leading blocker candidate and it did not survive. - Return/Enter after
Kind::Workspace. AetherModem has threeQLineEdit::returnPressedpaths (TX text:1214, terminal:3937, APRS message:4602). None relied on auto-default::1479-1482already clearssetAutoDefault/setDefaulton every body button (pre-existing, commit6af248b), and Workspace additionally suppresses default buttons.closeSettingsIfOpen()'sreject()and bothdlg.exec()call sites still compile and behave, sinceCanonWindowderives fromQDialog. - What dropping
trackPersistentDialog()costs. ReadMainWindow::setFramelessWindow(MainWindow.cpp:10202): the tracked list is used forsetFramelessMode()propagation only — no shutdown geometry flush, no disconnect teardown. Nothing else is lost, andCanonWindow::hideEvent(:138) does the geometry flush Escape/⌘W needs. - Unresolved theme tokens. Extracted every
{{token}}added acrossAx25HfPacketDecodeDialog.cpp,DStarModemPage.cpp,AetherDspWidget.cpp,ModemChrome.cppandCanonIndicators.hand resolved each against both shipped themes — all present (color.accent.*live as flat dotted keys, so a naive nested walk reports false misses). All sevencolor.canon.*the indicators paint with are also inThemeSeedGenerated.cpp, so the compiled-seed fallback doesn't render them transparent.canon_window_testdoes 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-BACKTICKhits are Markdown backticks in C++ comments or in operator-help strings passed toautomationError();CS-PII-DOB-ISOis "Observed 2026-07-31 on the HL2" in a comment; the three SSRF hits are a comment aboutAppSettingskeys, a comment showing"127.0.0.1:51234 (wsjtx)"as a display format, and aQLineEditplaceholder;CS-PII-EMAILisyou@example.complaceholder text (RFC 2606);CG-PATH-001is compile-time../Resources/bundle probing feeding aQFileDialogstart directory;CS-VLN-PATH-UNCis"^[\x20-\x7B\x7D-\x7E]{0,20}$", a D-STAR message validator, not a path. Only theCanonIndicators.hfile-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 on8f53d2bacross all 10 checks — but note thatci.ymlonly ever runsctest -Ragainst a named handful (lines 820, 832, 1174, 1189); neithercanon_window_testnormodem_chrome_testis 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
|
Cisco CodeGuard — static analysis of this PR (23 finding(s))
Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them. 🤖 aethersdr-agent · cost: $13.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>
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
|
Addressed in 20f28a9. @aethersdr-agent: blocker, the indicator cache fails open. Fixed by the new
Both reviews: cache key missing Unquoted Nit: ModemChrome's Nit: Nit: tests sharing temp PNGs under 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: Automated
Accessibility bot ( 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:
|
…, 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>
… 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>
…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>
…inned to the canon palette. Principle VIII. (#6364) ## Summary Part of #6239. The GPS window becomes **AetherGPS**, a `CanonWindow` workspace. It follows the recipe AetherRX and AetherTX (#6334), AetherModem (#6339) and AetherMap (#6340) use: - **Name:** AetherGPS in the window title, the canon header, the menu item (**Tools → AetherGPS…**) and the tooltip on the status-bar GPS indicator. - **Chrome:** no title bar, the ambient ground, the canon header and the corner close button. Drag any empty area to move it, resize it from any edge, and close it with Esc or Ctrl+W. The footer **Close** button is gone. - **Kind:** `CanonWindow::Kind::Workspace`, an independent top-level window with its own taskbar entry. It no longer follows **View → Frameless Window**, so `showGpsLocationDialog()` creates it directly instead of through `showOrRaisePersistent()`, which would set frameless mode on it. - **Geometry:** persists under its old key (`GpsLocationDialogGeometry`), so saved positions carry over. - **Size:** the minimum grows from 860×600 to 860×640, and the first-open size from 1080×720 to 1080×760, to make room for the header. **Look:** it sits under the `canon/aetherGps` theme scope, with a canon sheet in AetherMap's vocabulary: - the top status strip and each section are raised cards; the metric tiles, the NTP tip and the clock-sync group are nested; - section titles are cyan, values ink and field titles muted; - buttons and the NTP server field are canon controls, and check boxes use the canon indicators; - the satellite bar keeps its green fill, now the `color.background.success` token instead of the `#17663f` and `#ffffff` literals. The object name `gpsLocationDialog` is unchanged because `IcomControls.cpp` refers to it. **Docs:** - The user guide, menu reference, Radio Setup page, in-app help and the Tools-menu alt text in `docs/user/screens.json` use the new name. - The GPS page has a "Using AetherGPS" section that describes the window. - `docs/style/*` list AetherGPS among the canon windows. - **Stale screenshot, to recapture after merge:** `menu-tools.png` still shows "GPS Dashboard...". ## Constitution principle honored Principle VIII (Evidence Over Assertion) covers the checks below. The visual direction is RFC #6226's. ## Test plan - [x] Local build passes - [x] Rendered offscreen with the app on scratch settings and the radio disconnected, opened via **Tools → AetherGPS…** over the automation bridge: the canon header, the cards, the cyan section titles and the corner ✕ draw. - [x] The static-check scripts pass locally. The touchpoint manifest is unchanged. - [x] `embed_screenshots.py --check` and `gen_reference.py --check` pass. - [ ] Not exercised: the window with live GPS data, since no radio was connected. No test covers this window, and this PR adds none. ## Checklist - [x] Commits are signed - [x] No new flat-key `AppSettings` calls. The existing geometry key is reused. - [x] Code is clean-room - [x] All meter UI uses `MeterSmoother` (unchanged) - [x] Documentation updated: `docs/user/docs/aetherclock-and-gps.md` and the pages that name the window, `docs/style/*` - [x] Security-sensitive changes: n/a 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…remote operation over CGNAT (#6389) ## Summary Release prep for **v26.10.2**: the six release files plus the Stable docs snapshot. Metadata and docs only; no source, test or CI changes. Cutoff: `origin/main` at `46943eb3c` (#6083, merged 2026-10-11). Anything that merges after it is folded in before the tag. **106 merged changes** from **15 human contributors**, AetherClaude (2) and Dependabot (0). First-time contributor: **@dacacioa**. This was confirmed by checking each login for a merged PR before the previous tag, not from `author_association`. ## Changelog The range `v26.10.1..46943eb` was mapped through the commit-to-PR API (`repos/…/commits/<sha>/pulls`), not from commit subjects. It holds 106 PRs and no direct commits, and every one is cited in the section. `v26.10.1` is an ancestor of `main`, so nothing is excluded. #6083 merged before the branch was cut, so it is inside the range and needs no unmerged-PR banner. The notes lead with: - the unified 52 px title bar (#6198); - docs.aethersdr.com (#6286, #6298, #6294); - remote operation over CGNAT through the in-radio tailnet shim (#6273, #6318, #6325, #6319); - the canon windows (#6227, #6334, #6339, #6340, #6341, #6364, #6372, #6386). Then come DVK import, FFT Smooth/De-comb, TCI tune and receiver events, RTL-SDR multi-receiver delivery, HL2 and ANAN-G2 state memory, the IC-7300MK2 fixes, the first clean sanitizer runs (stated beside #6350's TSan red) and Authenticode signing. Numbers come from the PR bodies. "On upgrading" callouts cover the behavior changes the bodies flagged: - HL2 receive audio is 12.04 dB quieter (#6254); - HL2 host CW is +6.02 dB at a fixed drive (#5975); - ANAN AGC-T is remapped (#6282); - sleep prevention is on by default (#6195); - the Tailscale MTU cap has no opt-out (#6319); - IC-7300MK2 Auto SQL is withdrawn (#6184). DeepFist (#5953, #6103) is described as applying only to `ENABLE_DEEPFIST_EXPERIMENT` builds. ## Docs refresh, not just version strings - **README**: - Highlights: FFT Smooth/De-comb on the spectrum line; AetherMap naming; remote over CGNAT and TCI tune/receiver events on the SmartLink/TCI line; pre-NR Copy Assist; a new docs.aethersdr.com line. - Supported Hardware: the KPA1500, marked as not yet validated on hardware. - Other radio families: HL2 state restore extended; ANAN-G2 gains AGC-T, NB2, supply voltage/link and per-radio memory; RTL-SDR now "bundled in every official package", with WFM stereo, PPM, per-receiver SQL/meters and DAX/TCI feeds, and multi-receiver explicitly an evaluation. - Roadmap section: the CTR2 line advanced and a visual-canon line added. - Maturity labels are unchanged. - **ROADMAP**: - In flight: a v26.10.2 sentence each for aetherd, Icom, ANAN-G2, RTL-SDR (remaining is now production multi-receiver qualification and digital FM, RFC #6079), Hermes-Lite 2 and the CTR2 relay (RFC #6332 extensions). A new entry for the visual canon (RFC #6226 / #6239). - Queued: RFC #4214 (approved 2026-10-09) added. - Open RFCs, checked against `label:rfc`: #4214 leaves (approved); #4764 and #4861 leave (shipped by #6198 and #6216); #6346, #6232, #6212 and #6209 join. #6332 is approved and folds into the CTR2 entry. - Recently shipped: nine new `(v26.10.2)` entries, still within the v26.10.x cycle, so no month-rollover trim. - Backends (six: anan, flex, hl2, icom, rtl, sim) and NR engines (seven) were checked against the tree; both are unchanged. ## Judgment calls flagged for the maintainer All of these were decided through AskUserQuestion: - Version **26.10.2**, date **2026-10-11**, opened ready for review. - **#6350**, the TSan red in the weekly sanitizer lane: proceed, and name it below. - RFCs **#4764** and **#4861** come off Open RFCs as shipped. The issues themselves are left open for you to close. - RFC **#4214** goes under Queued (next cycle). - A **visual canon** In-flight entry, plus a README Roadmap line. - The **KPA1500** is listed in README Supported Hardware with "not yet validated on hardware". - Including **#6083**: you asked for it, and it merged before the cutoff, so it ships as an ordinary part of the range. ## Known failing on `main` - **`ctr2_hid_thread_port_test`** fails in the weekly TSan lane (runs of 2026-10-08 and 2026-10-10; #6350). `main`'s CI and Full Suite were green at `414082197`. The CHANGELOG headline and intro say so: the lanes achieved their first clean runs this cycle, and the test remains failing under TSan. This prep does not fix the test, and the release ships with it red in that lane. ## Scope | file | change | |---|---| | `CMakeLists.txt` | `project(AetherSDR VERSION 26.10.2)` | | `README.md` | Current version line, plus the prose refresh above | | `AGENTS.md` | Current version line | | `CHANGELOG.md` | new `[v26.10.2] — 2026-10-11` section; `[Unreleased]` stays and stays empty; history below byte-identical | | `packaging/linux/io.github.aethersdr.aethersdr.metainfo.xml` | `<release version="26.10.2" date="2026-10-11"/>` first in `<releases>` | | `ROADMAP.md` | cycle heading → post-v26.10.2, plus the prose refresh above | | `docs/user/versioned_docs/version-stable/**`, `docs/user/versioned_sidebars/version-stable-sidebars.json`, `docs/user/stable-version.json`, `docs/user/src/components/LogAnalyzer/rules.json` | docs snapshot (`tools/docs/snapshot_stable.py 26.10.2`), in its own commit. This is the first snapshot since #6286. | ## Validation Output of `check_release_prep.py --mapping mapping.json` at the branch head: 30 checks, 1 FAIL (explained below), 1 WARN (the snapshot files, listed in the scope table). ``` PASS v26.10.1 is an ancestor of origin/main PASS all six version spots say 26.10.2 PASS CHANGELOG heading date equals metainfo release date PASS appstreamcli validate PASS metainfo <release> list is date-descending with unique versions PASS `## [Unreleased]` stays and stays empty PASS `## [v26.10.2]` sits directly under `[Unreleased]` PASS the previous section is `[v26.10.1]` PASS CHANGELOG diff is exactly the inserted section PASS new section opens with a `### <headline>` PASS intro sentence has the house shape PASS new section has a `### Contributors` block PASS release_contents.py --check (citations, intro and Contributors counts) PRs in range: 106 cited: 106; intro 106/15 matches; Contributors (16 entries) matches; first-time ['dacacioa'] citations outside the range: #4764, #4861, #5468, #6192, #6226, #6271, #6332 (RFCs), #6238, #6350 (issues) PASS code fences balance / no duplicated table headers or doubled `---` (README, ROADMAP, new section) PASS every local link in README.md and ROADMAP.md resolves PASS no heading removed from README/ROADMAP is still linked elsewhere PASS Recently shipped `(vX)` markers run newest-first and lead with the new version PASS Recently shipped intro names the current cycle (v26.10.x) FAIL only the four current-version lines lose the `26.10.1` string outside CHANGELOG historical mentions must stay: "label": "26.10.1" PASS diff touches no src/, tests/, third_party/ or .github/workflows/ path PASS all six release files are in the diff WARN files beyond the six (the docs snapshot) PASS git diff --check ``` - **The one FAIL is the snapshot's version label, not a historical mention.** `docs/user/stable-version.json`'s `"label"` is the docs site's current Stable version, and `snapshot_stable.py` rewrites it by design. The validator predates #6286 and knows only the four current-version lines. Run on the six-file commit alone, the same check passes (29 checks, 0 FAIL, 0 WARN). - The docs build passes: `npm ci && npm run build` in `docs/user`. `node tools/docs/test_log_rules.mjs` reports 707 checks passed for 45 rules. The line moves it reports were already on `main`. - Four merged PRs (#6340, #6341, #6364, #6372) named screenshots to recapture after merge (`psk-reporter-sidebar.png`, `menu-tools.png`, `connect-dialog.png`, `rx-agc-calibration.png`), and #6386 named the panadapter shots (`panadapter.png` and the main-window captures, which still show the old title strip). They were not recaptured, so the Stable snapshot carries the older images. - No configure, build or test was run for this prep; it is metadata only, and CI builds it. - `main` CI at the cutoff `46943eb3c`: Docs and CodeQL - Code Quality are green; CI, Full Suite, CodeQL and the System Libraries Canary were still running when this was opened. At `414082197`, every one of them was green. After merge: tag the resulting **main** commit, not this branch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… 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>
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:CanonWindow::Kind::Workspace: an independent top-level window with its own taskbar entry, where Return in a field never clicks a default button.Ax25HfPacketDecodeDialogGeometry), so saved positions carry over.canon/aetherModemtheme scope. Its stylesheet and the D-STAR page's are rewritten incolor.canon.*tokens:2. Canon check boxes and radio buttons (
gui/CanonIndicators.h), now the style guide's building block:onAccenttick, 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.gui/ThemedImageCache.h, whichComboStyle.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 inurl("...").applyCanonSheet()rebuilds the sheet on a theme change, so the images follow the theme.ModemChrome gains a canon look (
ModemChrome::Look::Canon):main(13,481 bytes at each scale, compared against the merge base).AetherDspWidgetin dialog mode). Those had kept the old navy-and-green chrome.3. The AetherRX and AetherTX Settings dialogs (the profile library behind the gear) are short-lived
CanonWindowdialogs. 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}.mdadd the canon check box and radio button, ModemChrome's canon look, and the new canon windows.Touchpoints:
core/ThemeManager.hincluders go 171 → 173, fromDStarModemPage.cppand the newCanonIndicators.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 shotsaetherrx.png,aethertx.png,aethertx-eq.png,aethertx-compressor.pngandfirst-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 onmain.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_testsamples 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%Nargument and@slot@, only the canon look carries the canon indicators, and the canon look's only indicator rules are the painted ones.canon_window_testalso covers the cache:aquaorlinechange;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.cppbuilt 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_testandcomp_makeup_fader_testpass locally.The static-check scripts pass locally. The touchpoint manifest is regenerated.
The user docs site builds (
npm ci && npm run build), andgen_reference --checkpasses.Rendered offscreen with the demo app on scratch settings:
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
AppSettingscalls. Existing geometry keys are reused.MeterSmoother(unchanged)docs/user/docs/aethermodem-packet-radio.md,docs/style/*🤖 Generated with Claude Code