Skip to content

feat(gui): the multiFLEX Dashboard is a canon dialog that stays open beside the main window. Principle VIII. - #6768

Merged
ten9876 merged 4 commits into
mainfrom
feat/multiflex-canon-window
Oct 25, 2026
Merged

ten9876 merged 4 commits into
mainfrom
feat/multiflex-canon-window

Conversation

@ten9876

@ten9876 ten9876 commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Moves the multiFLEX Dashboard (Settings → multiFLEX...) from PersistentDialog to CanonWindow (Kind::Dialog, style guide RFC #6226). The maintainer chose how it looks and behaves:

  • Non-modal. MainWindow creates the dashboard once (QPointer, WA_DeleteOnClose) and raises it on a second open; it used to be exec(). It now stays open beside the main window. It belongs to one radio session: losing the radio closes it (connectionStateChanged(false)), and so does a radio connecting without multi-client sessions (applyCapabilitiesToUi()). Its controls therefore never act on a radio that is gone, and no pending disconnect outlives the session.
  • Canon header. "multiFLEX Dashboard" replaces the in-body "multiFLEX Stations" title. The corner close button, Escape and Ctrl+W replace the bottom Close button.
  • Theme tokens. Every color is now a token under canon/multiFlex:
    • Enabled / Disabled keeps green and red, through color.toggle.success.* and color.accent.danger.
    • PTT tick: color.accent.success.
    • Our own station: color.canon.cyan.
    • Disconnect: color.accent.danger, with the button.danger.*.disabled tokens while a disconnect is pending.
    • Table and header rules come from a new shared canonTableRules() (src/gui/CanonTableStyle.h), which SpotHub now uses too. SpotHub appends its own sizes and selection ink. An offscreen render of SpotHub's table with the old and new sheets matches pixel for pixel in both default themes.
    • A selected row keeps its item colors: a delegate keeps the green tick and our cyan station on the selection band.
    • Hovering the Enabled / Disabled toggle brightens its border to color.canon.aqua.
    • A theme change recolors items in place instead of rebuilding the rows.
  • Return no longer clicks the default button (setReturnClicksDefault(false)). Before this change, with a station row current, Return in the table clicked the Enabled / Disabled toggle and sent mf_enable=0 to the radio.
  • PTT tick is U+2713. Inter has no U+2714, so on Linux it fell back to Noto Color Emoji and drew a fixed gray glyph whatever its color; the old hard-coded green never showed there either.

Size and position still persist under MultiFlexDialogGeometry, so saved geometry carries over. FramelessResizer makes the window resizable from every edge.

Docs updated:

  • docs/user/docs/multi-flex.md: window behavior; Enabled / Disabled sits above the table, not below it.
  • docs/user/screens.json: alt text.
  • The canon window lists in dialog-patterns.md, aethersdr-style-guide.md and theme-style-guide.md, including the new scope.

Intent

After this merges, the multiFLEX Dashboard is a canon window like SpotHub and AetherControl. It is theme-correct in Light and stays open while you work, and Return can no longer turn multiFLEX off by accident.

Out of scope:

  • Refreshing multiflex-dashboard.png. The screenshot needs a radio with multi-client sessions; recapture it after merge.
  • The auto-selected first row. Qt still selects the first row when the table first fills, but the row now keeps its colors.

Constitution principle honored

Principle VIII (Evidence Over Assertion): every behavior claimed here has a test that goes red when it is broken (below). Principle XI applies too: the demonstration is the CI re-run on main.

Test plan

  • Local build passes (Qt 6.12, RelWithDebInfo)
  • New tests/multiflex_dashboard_window_test, 12 of 12 passing. It pins:
    • the canon chrome: frameless Qt::Dialog, canon/multiFlex scope, no Close button;
    • non-modality;
    • geometry saved under the old key and restored by a second window, including PersistentDialog's base64 saveGeometry() form that existing installs hold;
    • Enabled green and Disabled red, in Default Dark and Default Light;
    • in the table: the tick foreground and the tick drawn green on screen, our own station cyan with no Disconnect, and Disconnect drawn red, requesting once, then disabled;
    • Return and Enter on a current station row leave multiFLEX unchanged;
    • selected rows keep the tick and station colors;
    • a theme change recolors without rebuilding (the same Disconnect button object survives);
    • the aqua hover border;
    • closing when the radio disconnects.
  • Mutation checks. Each break turned the test red; each restore was sha256-identical and green:
    • remove the Enabled / Disabled repolish;
    • rename the geometry key;
    • draw the off state green;
    • put back U+2714;
    • setModal(true);
    • remove setReturnClicksDefault(false);
    • remove the item delegate;
    • recolor through a full refresh;
    • remove the hover rule;
    • remove the close on disconnect.
  • Related tests pass: canon_window_test, theme_manager_test, theme_seed_test, aethercontrol_window_test.
  • Static gates pass:
    • check_engine_boundary --strict, gen_touchpoint_manifest --check, check_capability_records --strict, check_command_plane --strict;
    • check_test_registration, check_theme_seed, check_a11y (0 findings), gen_reference --check.
    • embed_screenshots --check flags 15 pages that this PR doesn't touch; multi-flex.md is in step.
  • Live run on the demo radio (workspace 7, audio muted):
    • The demo declares no multi-client sessions, so the dashboard was opened disconnected.
    • Checked: empty-state layout in Dark and Light, the green Enabled state in both themes, and one window after two opens.
  • Not exercised live: the populated table and Disconnect. These need a radio with multi-client sessions; they were checked offscreen by feeding client 0x… status into a bare RadioModel. The live FLEX-8600 wasn't used.

Checklist

  • Commits are signed
  • No new flat-key AppSettings calls (geometry uses the existing MultiFlexDialogGeometry key through CanonWindow::setGeometryKey)
  • Code is clean-room
  • All meter UI uses MeterSmoother (no meters touched)
  • Documentation updated (docs/user, docs/style); no CHANGELOG.md entry
  • Security-sensitive changes reference a GHSA (n/a)

🤖 Generated with Claude Code

…beside the main window. Principle VIII.

The dashboard moves from PersistentDialog to CanonWindow (Kind::Dialog),
with the maintainer's choices:

- Non-modal: MainWindow creates it once (QPointer, WA_DeleteOnClose) and
  raises it on a second open, instead of exec(). A radio without
  multi-client sessions closes it, so it never drives one.
- Canon header "multiFLEX Dashboard" replaces the in-body "multiFLEX
  Stations" title; the corner close button, Escape and Ctrl+W replace the
  bottom Close button.
- Every color is a canon token under canon/multiFlex. The Enabled /
  Disabled button keeps green and red through color.toggle.success.* and
  color.accent.danger; the PTT tick is color.accent.success, our own
  station color.canon.cyan, Disconnect color.accent.danger.
- Return and Enter no longer click the default button
  (setReturnClicksDefault(false)): with a station row current, Return in
  the table clicked the Enabled / Disabled toggle and turned multiFLEX off.
- The PTT tick is U+2713, which Inter carries: U+2714 fell back to Noto
  Color Emoji on Linux and drew a fixed gray glyph whatever its color.

Size and position still persist under MultiFlexDialogGeometry, so saved
geometry carries over; FramelessResizer makes it resizable from every edge.

tests/multiflex_dashboard_window_test pins the canon chrome, the geometry
carry-over, non-modality, the button's colors in both default themes, the
table's tick, station and Disconnect colors, and Return on a station row.
Breaking the repolish, the geometry key, the off-state color, the tick
glyph, the modality or the Return guard each turns it red.

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 24, 2026 23:23

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

Reviewed head 73486c660608095d7936324942e26b7792b5ffe0.

Issue fit: yes. No closing issue is linked; reviewed against the stated intent and RFC #6226, including its approval and per-surface migration ruling. This converts the multiFLEX Dashboard to a non-modal canon dialog, preserves its geometry key, replaces hard-coded colors, and prevents Return in the table from activating the multiFLEX toggle. These are the explicitly disclosed choices of the maintainer/author, rather than preferences bundled into an unrelated fix. No further maintainer design decision is needed for this scope.

Scope

Files Change / disclosed? Verdict
src/gui/MultiFlexDialog.{cpp,h} CanonWindow, token styles, resizer, geometry, Return guard and tick glyph; yes In scope
src/gui/MainWindow.{cpp,h} One reusable non-modal dashboard and closure on unsupported connected radios; yes In scope
tests/multiflex_dashboard_window_test.cpp, tests/tests.cmake Registered offscreen, socket-free regression coverage; yes In scope; registration-maintenance nit below
docs/user/docs/multi-flex.md, docs/user/screens.json Window interaction, control position and alt text; yes In scope; unchanged screenshot explicitly disclosed as stale
docs/style/{dialog-patterns,aethersdr-style-guide,theme-style-guide}.md Canon adoption and scope lists; yes In scope; stale reference nit below

Everything in the diff is explained by the stated migration. One commit authored October 24; no unrelated files, new wire commands, settings keys, theme seed/default changes, or engine-boundary baseline edits. The existing screenshot and first-row selection behavior are explicitly excluded in the body.

Blockers

None found.

Non-blocking nits

  1. Add the new test to the settings-consumer list — tests/tests.cmake:8172–8186 and 9728. It directly uses AppSettings and TestSettingsProfile, but multiflex_dashboard_window_test is absent from AETHER_SETTINGS_CONSUMERS. docs/agents/tests-ci.md says: “A test that touches AppSettings also needs its target name in the AETHER_SETTINGS_CONSUMERS list.” Insert this entry in that list:

     multiflex_dashboard_window_test

    This is maintenance of the documented direct SQLite-link declaration; the target currently links and passes in the review configuration, so I am not treating it as an observed build blocker.

  2. Remove the old reference example — docs/style/dialog-patterns.md:228. MultiFlexDialog is still listed under “Existing dialogs that follow this pattern” for PersistentDialog, immediately before the new CanonWindow exception that now correctly names it. Delete this bullet from the former list:

  • src/gui/MultiFlexDialog.{h,cpp}
    
    

What I verified and tried to break

  • Built the actual desktop app plus multiflex_dashboard_window_test and canon_window_test at this head, in an isolated scratch worktree using the lean Linux shared-core review configuration. Both registered CTests passed: 2/2, 0.23 seconds. An inherited GTK platform-theme setting initially tried to open the unavailable display; clearing QT_QPA_PLATFORMTHEME and selecting Fusion allowed the intended offscreen runs. The unchanged CanonWindow test showed the same environment issue; no PR code change was needed.

  • Independently changed setReturnClicksDefault(false) back to true in a temporary source copy and linked it into the same dashboard test. returnInTheTableDoesNotToggleMultiFlex failed with actual model.multiFlexEnabled() == 0, expected 1. The delivered test detects this accidental toggle. I did not independently rerun all six mutations listed in the body.

  • Additional socket-free probes fed legacy base64 geometry into the existing key and changed Dark -> Light -> Dark with a populated, disconnected model. The legacy value was read (Qt constrained the oversized width to the offscreen screen); all three live theme transitions retained two rows and the PTT success-token foreground. The supplied tests cover per-item colors, rendered tick/button colors, self-row exclusion, disconnect signaling/disable, and Return/Enter separately. These prove presentation and injected-model behavior, not convergence with Flex firmware.

  • Drove the exact head app through /tmp/aethersdr-pr6768, identity pr6768-review, offscreen, with a fresh isolated settings store and AETHER_AUTOMATION_NO_TX=1. Initially confirmed connected:false. Invoked the real multiFLEX... menu action twice, inspected Dark and Light dashboard captures, closed and reopened it, then connected only to DEMO-0001 and confirmed model AetherSDR Demo. Extracted bridge evidence:

    {"afterTwoOpens":{"dashboardCount":1,"visible":1},
     "afterClose":{"visible":0},
     "afterReopen":{"dashboardCount":1,"visible":1},
     "afterDemoConnect":{"dashboardCount":0,"visible":0},
     "afterDemoDisconnectReopen":{"dashboardCount":1,"visible":1}}

    The demo has no multi-client sessions, so the unsupported-radio closure is exercised. Disconnected and closed this owned review instance; it exited successfully. No live radio was connected and no transmit action was used. Populated live multi-client sessions, real client eviction, native platform placement/resizing and Windows/macOS rendering remain unverified. The body already discloses its populated-table hardware limitation and stale screenshot.

  • All four engine gates pass on base and head with identical outputs; no touchpoint growth or retagging. Test registration, theme seed, generated references and accessibility checks also pass (four touched GUI files, zero a11y findings). The dedicated automated code-review skill was unavailable; this was a manual adversarial pass with executed tests and bridge checks.

At the final status check, Static checks, the docs-site build and completed auxiliary checks passed; Linux, Windows and macOS build jobs were still running. Pull-request CI/Docs/Static runs were created at 23:23:51 UTC; current main remains 3b4931f087ca0f9b4976e3720af51cfab6ab6aa4 from 22:26:36 UTC, so no newer-main gap was present. This is not a claim that the full suite or native-platform behavior has been verified.

Recommendation: Approve with nits. The intended migration and Return protection survived the focused tests, mutation and real-app lifecycle checks. Clean up the two small omissions and let the remaining CI builds finish. Posted as COMMENT under the review workflow; approval remains the operator's call.

…stentDialog list drops the dashboard. Principle VIII.

The #6768 review's two nits:

- multiflex_dashboard_window_test uses AppSettings and TestSettingsProfile,
  so it joins AETHER_SETTINGS_CONSUMERS (docs/agents/tests-ci.md).
- docs/style/dialog-patterns.md still listed MultiFlexDialog among the
  PersistentDialog reference dialogs; it is a CanonWindow now.

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

ten9876 commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Both nits from the review of 73486c660 are fixed in 72b934aba:

  1. AETHER_SETTINGS_CONSUMERS: multiflex_dashboard_window_test is now listed in tests/tests.cmake, right after aethercontrol_window_test. After a clean rebuild, ctest -R multiflex_dashboard_window passes (1/1), and tools/check_test_registration.py reports OK.
  2. docs/style/dialog-patterns.md: I removed the src/gui/MultiFlexDialog.{h,cpp} bullet from "Existing dialogs that follow this pattern", the PersistentDialog list. grep -rn MultiFlexDialog docs/ now finds the dashboard only in screens.json (its capture target) and in the CanonWindow lists.

No code changed beyond these two lines.

🤖 Generated with Claude Code

…olors on selection, and shares the canon table rules with SpotHub. Principle VIII.

From the landing pass on #6768, with the maintainer's calls:

- Losing the radio closes the dashboard (connectionStateChanged(false)), so
  its toggle, PTT and Disconnect buttons never act on a radio that is gone
  and no pending disconnect outlives the session.
- A selected row keeps its own item colors: a delegate keeps the green PTT
  tick and our cyan station on the selection band instead of Qt's
  highlighted-text color.
- Hovering the Enabled / Disabled toggle brightens its border to
  color.canon.aqua in both states; its on/off rules had outranked the
  generic hover rule.
- A theme change recolors the tick and station in place
  (applyItemColors()) instead of rebuilding every row and Disconnect button.
- The canon table rules move to canonTableRules() in CanonTableStyle.h.
  SpotHub and the dashboard both use it and append their own sizes; SpotHub
  keeps its selection ink. An offscreen render of SpotHub's table with the
  old and new sheets matches pixel for pixel in both default themes.

multiflex_dashboard_window_test gains a slot for each: selected-row colors,
recolor without rebuild (same Disconnect button object), hover border and
close on disconnect. Removing the delegate, the in-place recolor, the hover
rule or the close each turns its slot red.

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

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Issue fit

No linked issue; the PR is reviewed against its own stated intent and RFC #6226. It does what it says: MultiFlexDialog now derives from CanonWindow (Kind::Dialog), the call site at src/gui/MainWindow.cpp:753-772 becomes the canon show/raise pattern, the sheet is token-resolved under canon/multiFlex, FramelessResizer::install(this) is present (dialog-patterns.md:275-276 asks for it), and geometry keeps the MultiFlexDialogGeometry key. Both entry points route through the one slot — the Settings menu (MainWindow_Menus.cpp:846) and the title-bar multiFLEX button (MainWindow.cpp:5460) — so there is no sibling call site left on exec(). The maintainer pick is the author's own, so the #6239 question other PRs in this wave carried does not arise here — maintainer-authored under an approved RFC and its tracker, so GOVERNANCE.md owes no separate issue for it.

Scope

File / group What it changes Claimed in the body? Verdict
src/gui/MultiFlexDialog.{h,cpp} PersistentDialog → CanonWindow, canon header, token sheet, setReturnClicksDefault(false), resizer Yes In scope
src/gui/MainWindow.{h,cpp} QPointer member + fwd-decl; exec() → show/raise; capability-driven close() Yes In scope
MultiFlexDialog.cpp:237 PTT glyph U+2714 → U+2713 Yes (with the Inter/Noto reason) In scope
MultiFlexDialog.cpp:142 drops the m_pttLabel text.secondary sheet (now canon.inkSoft via the body QLabel rule) Covered by "every color is now a token" In scope
tests/multiflex_dashboard_window_test.cpp, tests/tests.cmake new registered test + AETHER_SETTINGS_CONSUMERS entry Yes In scope
docs/style/*.md, docs/user/docs/multi-flex.md, docs/user/screens.json canon lists, scope list, window behavior, alt text Yes In scope

Everything in the diff is explained by the stated change. No CHANGELOG.md entry (correct). No new settings key, protocol verb, capability field or public surface: the geometry key is the pre-existing one, and check_capability_records / check_command_plane have nothing to see. No removed guard — the deleted Close button is replaced by the canon corner button, Escape and Ctrl+W, and the deleted hard-coded sheet is the point of the change.

Blockers

None.

Nits (all non-blocking)

  1. src/gui/MainWindow.cpp:759-767 — the comment and WA_DeleteOnClose disagree, and one guard is the cost:

        // A canon dialog that stays open beside the main window: created once,
        // raised if it is already open.
        if (!m_multiFlexDialog) {
            auto* dlg = new MultiFlexDialog(&m_radioModel, this);
            dlg->setAttribute(Qt::WA_DeleteOnClose);

    Since Qt 6.3 QDialog::done() routes through QDialogPrivate::close(), which runs the close machinery — so Escape / Ctrl+W destroys the instance and the next open builds a new one; "created once" is true per open-cycle only. This does not contradict CanonWindow.cpp:128-130 ("Escape and Ctrl+W go through QDialog::reject(), which hides the window without a close event"): no QCloseEvent reaches a closeEvent() override, and the object is still deleted. Two different claims; the comment only makes the first. The pattern itself is the house one — MainWindow.cpp:3242,3566,4640,4669 and MainWindow_Menus.cpp:667 (SpotHub) all pair WA_DeleteOnClose with a QPointer, and MainWindow.cpp:3426 spells the contract out.

    The reachable consequence is MultiFlexDialog.h:34-39's own promise: m_pendingDisconnects keeps a row disabled "so a second click can't queue a duplicate disconnect for a handle already on its way out". Close the dashboard while a disconnect is in flight, reopen before the radio evicts the client, and the Disconnect button is enabled again for that handle. Not a regression — the old exec() on a stack object lost the set too — but this dialog is the one in that group that keeps state worth carrying, and some canon surfaces do keep their instance (FreeDvReporterDialog is held in a raw MainWindow pointer; CanonWindow sets the attribute nowhere itself). Dropping the attribute here would make the comment literally true and carry the guard across opens.

  2. Contrast, resolved from the bundled themes under the canon scope (manual pass — check_a11y has no contrast rule, docs/a11y.md:143 is the 4.5:1 / 3:1 floor):

    Pair Dark Light
    Enabled, on: toggle.success.foreground.checked on …background.checked 4.16 3.78
    Disabled, off / Disconnect: accent.danger on canon.control 4.84 3.95
    Disconnecting: button.danger.*.disabled pair 3.13 2.48 (disabled — exempt)
    PTT tick accent.success on canon.nested 9.52 3.92
    Table canon.ink on canon.nested 15.50 11.49
    Header canon.cyan on canon.raised 12.43 7.06

    The Enabled-on button is 13px bold, so 4.5:1 applies, and in dark it is down from 5.74 (old #40ff80 on #1a6030). But the pair is the ratified canon toggle idiom, already used by DxClusterDialog, RadioSetupDialog and FlexControlDialog, so it is a shared-token residue and not this PR's invention — and against it, the old sheet painted a fixed dark dialog in Light at all, which this fixes. Named, not charged to the PR.

  3. tests/multiflex_dashboard_window_test.cpp:6-10 — the header claims "geometry saved before the migration carries over", but the slot only round-trips the new x,y,w,h form. What makes the claim true is CanonWindow.cpp:181, the restoreGeometry(QByteArray::fromBase64(...)) fallback for a PersistentDialog-era value. One extra assertion — pre-seed MultiFlexDialogGeometry with a base64 saveGeometry() blob, show, and check the size came back — would pin the migration itself rather than the format that replaces it.

  4. docs/user/docs/multi-flex.md:44 / docs/user/screens.json:2242 — the body discloses the recapture as out of scope, which is fair (the harness needs a multi-client radio), but the shipped page now describes a round corner close button while docs/user/static/img/screens/multiflex-dashboard.png still shows the retired "multiFLEX Stations" header and the bottom-right Close button. Sighted and screen-reader readers get different windows until it is recaptured; worth a follow-up issue so it does not sit on the page indefinitely.

  5. #6239's Picked list is not updated. The three style docs are the only registry of canon windows (no test or generator enumerates them), so the tracker drifting from the docs is the one place a future reviewer looks first.

Security audit

Ran the required security-audit subagent over docs/user/screens.json, src/gui/MainWindow.{cpp,h} and the new test. No confirmed findings. All 52 CodeGuard hits are pre-existing code swept in because the 12k-line MainWindow.cpp is in the changed set; the audit pinned each to an identical line on main (50000053 → :5535 is a Flex file-server error code, not a bank account; /proc/self/status → :6300 is a constant read-only path; CS-INJ-role_override → :11677 is the word "override" in the comment // CNN override:; the 17 CMDI-BACKTICK hits are Markdown backticks inside C++ comments naming firmware verbs, with no shell anywhere near them; 127.0.0.1 / 224.0.0.1 in screens.json and MainWindow.cpp:6936 are documentation prose and the WSJT-X multicast default). The one in-PR trigger, CS-VLN-SSRF-HEX on QStringLiteral("client 0x7594C952") at multiflex_dashboard_window_test.cpp:191,237, is a Flex client handle, not a hex-encoded host. The only in-PR filesystem write is window.grab().save(...) at :252-254, gated on the developer's own MULTIFLEX_TEST_GRAB_DIR. Nothing in the diff crosses a trust boundary.

What I tried to break

  • Lifetime. WA_DeleteOnClose on a parented, QPointer-held dialog: Escape → QDialog::done() → close() fires hideEvent, which flushes geometry before deleteLater, so the save is not lost to the delete; the capability path at MainWindow.cpp:8266 is guarded by the same QPointer in the same if; the Disconnect lambda's emit is its last statement, so a handler that destroyed the dialog could not return into a dead this; and the attribute is set at the call site only, so the five stack-allocated MultiFlexDialogs in the new test are not double-freed (the trap #6759 hit). No finding.
  • The capability gate. connected && !caps.hasMultiClientSessions closes the window, but plain disconnect does not — I expected a stale station list to survive there. It does not: RadioModel.cpp:8473,8489 clears m_clientInfoMap and emits otherClientsChanged, so the open dashboard refreshes to an empty table. The Enabled toggle stays clickable while disconnected, which matches the menu action's own setVisible(!connected || …). No finding.
  • The test, against the unfixed code. returnInTheTableDoesNotToggleMultiFlex would be vacuous if the table swallowed Return — it does not: MultiFlexDialog.cpp:135 sets QAbstractItemView::NoEditTriggers, so Return propagates to CanonWindow::keyPressEvent and only setReturnClicksDefault(false) stops the toggle. The body's mutation list (remove the repolish, rename the key, draw the off state green, restore U+2714, setModal(true), remove setReturnClicksDefault(false)) names a red for each of the five slots.
  • The token sheet. All sixteen tokens the new sheet references exist in both bundled themes, so no {{…}} resolves to nothing and no rule block is silently dropped; the bool property selector [multiFlexOn="true"] matches because QVariant(bool).toString() is "true"/"false", and the unpolish/polish pair runs on every flip. The body-level bare QLabel rule does not steal the canon header title: makeCanonHeader applies its own QLabel#canonHeaderTitle sheet to the header widget (CanonWindow.cpp:550-555), which wins on both proximity and specificity.
  • The CMake block. It names no ThemeManager.cpp and links only aethercore Qt6::Gui Qt6::Widgets Qt6::Test, where every other CanonWindow test compiles ThemeManager.cpp/ThemeSeedGenerated.cpp or links aetherdesktop_support — those symbols live in aetherdesktop_support, not aethercore, so this looked like a link failure. It is not: aether_retrofit_tests() (tests/tests.cmake:10340-10344) links aetherdesktop_support into any test target that links aethercore and Qt6::Widgets, and aether_assert_tests_retrofitted() fails configure if it ever misses. The block is correct as written, and the compile lanes agree — build, check-macos and check-windows are all green on the head 72b934a (they are cancelled on 73486c6, the superseded first commit).
  • "Table and header styling matches SpotHub's." True for the header: DxClusterDialog.cpp:491-499 (the canon/spotHub surface) carries the same QHeaderView::section tokens — canon.cyan on canon.raised, border: none with a canon.line bottom rule — differing only in padding and font size.
  • Could not check. Nothing was built or run here, and no GUI was driven, so every statement above is reasoned from the code in the PR head checkout; the pixel-probe slots (pixelsNear(..., 6) on a 1px antialiased border, and other != 0 demanding zero pixels of the opposite token) are the ones I would most expect to be platform- or font-sensitive, and only a real run settles them. The U+2714 → U+2713 rationale (Inter lacks U+2714, so Linux fell back to Noto Color Emoji and drew a fixed gray glyph) is a font-coverage claim I cannot verify headless, and the test's tick probe exercises whatever font the offscreen platform resolves, not Inter specifically. The contrast figures hold for the two bundled themes only — a user theme with no canon scope resolves the base tokens from root and the table above does not apply.

Recommendation

Approve with nits. This is the most complete PR of the canon wave: the call-site conversion, the resizer, the geometry carry-over and the setReturnClicksDefault(false) fix are all present and each has a named mutation that reddens its test, and the body discloses its own gaps (the screenshot, the auto-selected first row, the untested populated table) rather than papering over them. All three compile lanes are green on the head. The concrete next step is a maintainer read of nit 1 (WA_DeleteOnClose versus the m_pendingDisconnects guard) and a follow-up issue for the screenshot; neither needs a round trip of changes to this branch. Thanks for the mutation-check table — it made the test-strength half of this review checkable instead of a guess.


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

@aethersdr-agent

Copy link
Copy Markdown
Contributor

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

  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6768/docs/user/screens.json:2543
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6768/docs/user/screens.json:4183
  • [MEDIUM] CS-PII-EMAIL — PII detected: email in docs/user/screens.json /tmp/aetherclaude/pr-6768/docs/user/screens.json:3841
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6768/docs/user/screens.json:1124
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6768/docs/user/screens.json:2543
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6768/docs/user/screens.json:2589
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6768/docs/user/screens.json:2623
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6768/docs/user/screens.json:2720
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6768/docs/user/screens.json:2760
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6768/docs/user/screens.json:4183
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:9734
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:9846
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:9968
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:9969
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:9970
  • [HIGH] CS-VLN-PATH-PROC — Vulnerability: CS-VLN-PATH-PROC in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:6229
  • [HIGH] CS-VLN-PATH-PROC — Vulnerability: CS-VLN-PATH-PROC in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:6308
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:521
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:3585
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:519
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:1227
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:1710
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:1798
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:1875
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:1917
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:1952
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:2248
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:3518
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:3618
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:6171
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:8048
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:8122
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:8207
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:8395
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:8598
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:9835
  • [LOW] CS-PII-IP — PII detected: ip_address in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:6936
  • [HIGH] CS-PII-BANK — PII detected: bank_account in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:5543
  • [CRITICAL] CS-INJ-role_override — Prompt injection: role override attempt in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:11690
  • [CRITICAL] CS-INJ-role_override — Prompt injection: role override attempt (detected after NFKC normalization) in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6768/src/gui/MainWindow.cpp:11690

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

…y-over, and the open path's comment states the per-open lifetime. Principle VIII.

From aethersdr-agent's review of #6768:

- geometrySavedByThePersistentDialogCarriesOver seeds MultiFlexDialogGeometry
  with a base64 saveGeometry() blob, the form PersistentDialog wrote, and
  checks the dashboard opens at that size. A corrupted blob falls back to
  the default 760x320 and turns it red.
- MainWindow::showMultiFlexDialog's comment said "created once";
  WA_DeleteOnClose destroys the dashboard on close, so it is created per
  open and raised while open. The maintainer kept the lifetime and asked
  for the comment to say so.

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

ten9876 commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Replies to the aethersdr-agent review (posted against c3d8d6ec5), one per nit. Fixes are in bb09d8491.

  1. WA_DeleteOnClose against "created once". The maintainer kept the per-open lifetime, the house pattern for canon windows, and asked for the comment to say so. src/gui/MainWindow.cpp now reads "raised if it is already open, otherwise created. Closing it … destroys it, so each open starts fresh." If you close and reopen while a disconnect is in flight, that row's button can be clicked again. The radio is simply asked a second time to drop a client it is already dropping; the old exec() dialog behaved the same way. Separately, as of c3d8d6ec5 the dashboard also closes when the radio disconnects (connectionStateChanged(false), pinned by closesWhenTheRadioDisconnects), so no pending disconnect outlives a session. Your "What I tried to break" note that plain disconnect leaves it open describes the earlier head.
  2. Contrast. No change in this PR. The Enabled-on and danger pairs are the shared canon toggle and danger tokens that SpotHub, Radio Setup and AetherControl already use, as your review notes. Raising them is a theme-token change across every canon window, not part of this migration.
  3. The old saved-geometry format. New slot geometrySavedByThePersistentDialogCarriesOver. It seeds MultiFlexDialogGeometry with a base64 QDialog::saveGeometry() blob (what PersistentDialog wrote) at 720×360, and checks that the dashboard opens at 720×360. As a control, a corrupted blob falls back to the default 760×320 and turns the slot red (Actual (window.size()): QSize(760x320)). The suite is 12/12.
  4. Stale screenshot. Filed as docs(user): recapture the multiFLEX Dashboard screenshot after the canon migration (#6768) #6770, at the maintainer's request.
  5. Tracking: AetherSDR style guide follow-ups (RFC #6226) #6239 Picked list. The maintainer asked for it to be updated once this merges.

The CodeGuard findings in the other comment are all on unchanged lines (your security audit pinned each to the same line on main). The one hit in this PR, client 0x7594C952 in the test, is a Flex client handle.

🤖 Generated with Claude Code

@ten9876
ten9876 merged commit 09dc626 into main Oct 25, 2026
14 checks passed
@ten9876
ten9876 deleted the feat/multiflex-canon-window branch October 25, 2026 00:29
ten9876 added a commit to w5jwp/AetherSDR that referenced this pull request Oct 25, 2026
…ntries. Principle VIII.

aethersdr#6768 (multiFLEX Dashboard) and this branch each added an entry to the same
canon-window and theme-scope lists in docs/style/aethersdr-style-guide.md,
dialog-patterns.md and theme-style-guide.md; the resolution keeps both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
w5jwp added a commit to w5jwp/AetherSDR that referenced this pull request Oct 25, 2026
…the import default and a small legacy rectangle. Principle VIII.

The window's 760x420 minimum was an explicit floor measured on macOS
fonts. An explicit minimum overrides the layout's own, so on a font with
wider metrics the seven buttons would clip at the minimum, and the
per-button fit test would go red with nothing regressed. The minimum is
now 760x420 or the layout's minimum size hint, whichever is larger,
recomputed on a theme change. The fit test replaces its fixed-number
check with the layout-relative one the review suggested, and runs it
under the application font and a 160% letter-spaced font (layout needs
929 px natively, 949 px offscreen; the 760 floor alone fails that row).

New cases:
- a 640x380 legacy base64 rectangle opens at the 760x420 minimum
  (Qt's restoreGeometry() clamps to it; the base64 branch has no
  expandedTo of its own);
- Return in the Import box takes Skip existing: the existing net keeps
  its name and only the new one is added (Qt's own file picker via
  AA_DontUseNativeDialogs);
- the Ctrl+W / Cmd+W row activates the window first, since the close
  shortcut is window-scoped; a desktop session that refuses the
  activation skips the row instead of failing it.

Also: the disabled-row ink is resolved once per fill, not per row; the
lifetime audit row goes back to its baseline line (173); the
CanonWindow hideEvent comment and dialog-patterns row say that reject()
closes the window without a closeEvent(); and the two Net Scheduler
screenshot recipes target the editor by its objectName
(netEditorDialog), with alt text naming the canon header and corner
close button.

Rebased onto main (aethersdr#6735, aethersdr#6745, aethersdr#6768 add windows to the same
style-doc lists; every entry is kept). The touchpoint manifest counts
this branch's ThemeManager include on top of main's: 184 -> 185.

Co-Authored-By: Claude Opus 5.5 <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