Skip to content

feat(gui): Net Scheduler is a CanonWindow workspace, built like AetherRX and AetherTX, and its questions are CanonMessageBoxes. Principle VIII. - #6762

Open
w5jwp wants to merge 4 commits into
aethersdr:mainfrom
w5jwp:feat/net-scheduler-canonwindow
Open

w5jwp wants to merge 4 commits into
aethersdr:mainfrom
w5jwp:feat/net-scheduler-canonwindow

Conversation

@w5jwp

@w5jwp w5jwp commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #6761. Part of #6239 and approved RFC #6226. Waiting on the maintainer's pick: this full CanonWindow move is not on #6239's CanonWindow lists. #6239's "Persistent tool dialogs (one PR per dialog)" candidate covers only moving a dialog's colours to color.canon.*. #6761 lays out both options, and its triage asks for one ruling across the same-shape PRs; please don't merge before the pick. Merging is that pick: aethersdr-style-guide.md and dialog-patterns.md list Net Scheduler among the tool windows the maintainer has moved to CanonWindow, which is true only once this lands.

Net Scheduler (NetSchedulerDialog, Tools → Net Scheduler...) moves from a PersistentDialog to a CanonWindow::Kind::Workspace, built the way AetherRX and AetherTX are. Its Add / Edit editor becomes a canon dialog, and its seven QMessageBoxes become CanonMessageBoxes.

  • Window:
    • Canon header and ground, corner close button, Esc / ⌘W close, and edge resizing through FramelessResizer.
    • Its own theme scope, canon/netScheduler. Its editor and message boxes are its children, so they resolve through it too.
    • Geometry under the existing key (NetSchedulerDialogGeometry). CanonWindow reads the old base64 rectangles, so the saved size and position carry over. A rectangle narrower or shorter than the window's minimum opens at the minimum: the base64 branch is a plain restoreGeometry(), and Qt clamps the restored size to minimumSize() (a 640×380 legacy rectangle opens at 760×420, measured under both cocoa and offscreen).
  • Controls:
    • The canon controls sheet the AetherRX and AetherTX Settings dialogs use (ModemChrome::styleSheet(Scale::Dialog, Look::Canon)), applied through applyCanonSheet().
    • Window-local rules add what that sheet does not style: QDoubleSpinBox / QDateTimeEdit (matching its QSpinBox rule), the combo list and the date's calendar popup, the intro line (canon.muted), the next-firing line (canon.cyan) and the editor's hairline divider (canon.line).
    • Right-click menus: that sheet clears every descendant to transparent, and a field's standard menu (Undo/Cut/Copy/Paste) is a child of the field, so the window sheet (shared by the editor and the message boxes) gives QMenu its canon panel back, as on TX Band Settings (8caa5a42).
    • The whole table is monospace. The canon sheet sets "SF Mono", "Menlo", "Consolas", monospace on QTableWidget itself, not on a numeric column, so the Name column ("Tuesday County ARES Net") is monospace too. That is most of the table's width growth.
    • Both palette(...) inline style sheets go. A disabled net's row is drawn in canon.muted, and the table refills on ThemeManager::themeChanged so that ink follows a theme change.
  • Add / Edit Net editor:
  • Popups, all seven QMessageBoxes:
    • Remove Net: Cancel / Remove. Remove is destructive (gold) and has the objectName netRemoveConfirm, so the bridge can tell it from the window's own Remove. Cancel is the default.
    • Import Net Schedule: Cancel / Import as new / Overwrite (gold) / Skip existing. Skip existing stays the default.
    • Five OK notices through a small showNotice() helper: capture with no slice, import open failure, import parse failure, empty import, and export write failure.
  • Opening it (MainWindow_Nets.cpp): showNetSchedulerDialog() creates the window directly with WA_DeleteOnClose, like SpotHub. showOrRaisePersistent() would apply View → Frameless Window, which a canon window ignores. The entriesChanged / tuneRequested wiring is unchanged. The wasFresh guard stays: triggering the menu while the window is open raises the existing window instead of building a second one.
  • Unchanged:
    • the nested-loop lifetime handling (scoped children, QPointer guards, ID relookup after each loop);
    • the button texts and their single row;
    • the table's accessible name;
    • NetScheduleStore and the schedule file.
  • Docs:
    • docs/style/dialog-patterns.md, aethersdr-style-guide.md and theme-style-guide.md list the window and its scope.
    • docs/user/docs/net-scheduler.md says how the window moves, resizes and closes, that Remove defaults to Cancel, and now lists Tune Now, which it had left out.
    • docs/architecture/aetherd-touchpoints.md is regenerated for the new core/ThemeManager.h include (ui-support, 184 → 185).
    • dialog-patterns.md's geometry row and the CanonWindow::hideEvent() comment say that Escape and ⌘W / Ctrl+W close the window through QDialog::reject() (so WA_DeleteOnClose applies) without a close event, instead of "hide it".
    • docs/user/screens.json: the two Net Scheduler recipes target the editor by its objectName, netEditorDialog (it is no longer a plain QDialog, so the QDialog/... targets matched nothing), and both alt texts name the canon header and the corner close button.

Intent

After this merges, Net Scheduler, its editor and its questions look and behave like the other canon windows, with the same controls doing the same things. Out of scope: the reminder banner, NetScheduler, the schedule format, and the other persistent tool dialogs (one PR each, per #6239).

Visible changes for the maintainer:

  • Remove Net defaults to Cancel, so Return keeps the net on every platform (the CTR2 Restore precedent, feat(ctr2): the CTR2 window's questions in the canon style, and a restart that beats its answer is not an error. Principle VIII. #6736).

    • What Return did before depended on the platform. A probe of QMessageBox with Yes | No on the Qt 6.12 build found No the default under the native macOS (cocoa) platform and Yes under offscreen. Windows and Linux were not checked.
    • The same probe confirmed Skip existing as the import box's default on both.
  • Size: the window minimum grows from 640×380 to 760×420, and the editor minimum from 440 to 560 px.

    • 760×420 is a floor, not a fixed number: the minimum is the larger of it and the layout's own minimum size hint, recomputed on a theme change, so a font with wider metrics grows the minimum instead of clipping a button. The layout needs 713 px under macOS fonts (728 px offscreen), so on this machine the floor is what applies.
    • The canon buttons are wider than the native ones: the seven need about 700 px. At 640 they were clipped, and every table column was elided.
    • On / Frequency / Mode now size to their contents. Next still elides at the minimum width; a wider window shows it whole.
  • Contrast, resolved from the bundled themes (a user theme without a canon scope falls back to the root colours). Every pair clears 4.5:1 except one:

    Text Tokens Dark Light
    Intro line muted on ground 6.71:1 5.94:1
    Next-firing line (600 weight) cyan on ground 13.05:1 6.49:1
    Table text inkSoft on control 10.50:1 5.60:1
    Table text, alternate row inkSoft on raised 12.46:1 8.59:1
    Disabled net row muted on control 5.39:1 4.22:1
    Disabled net row, alternate muted on raised 6.39:1 6.47:1
    Column headers muted on raised 6.39:1 6.47:1
    Editor field text ink on control 14.01:1 9.54:1
    Button text cyan on control 10.47:1 4.61:1
    Combo list / calendar popup inkSoft on nested 11.61:1 6.75:1
    Right-click menu inkSoft on raised 12.46:1 8.59:1
    Menu / list selection onAccent on cyan 12.71:1 7.06:1

    A disabled net's row on a non-alternate row in the light theme is 4.22:1, under the 4.5:1 docs/a11y.md asks for. The muted ink is how a disabled net reads as off (with no ✓ and "—" for Next), and WCAG's exemption for inactive controls arguably doesn't cover a data row. Maintainer call: keep canon.muted, or take canon.inkSoft and let the ✓ and "—" carry the state alone.

  • Stale screenshots, to recapture: net-scheduler-window.png and net-scheduler-add-net.png in docs/user/docs/net-scheduler.md. Their recipes and alt texts are updated; the images are not re-shot.

Overlap: #6753 and the other canon PRs add windows to the same lists in the three style docs, so whichever lands later needs a small rebase there. This branch is rebased on main at be216ed3 (#6735, #6745 and #6768 are in; every list entry is kept).

Constitution principle honored

Principle VIII — Evidence Over Assertion: the workspace shape, the canon scope, the geometry carry-over, the button fit, the Remove default, the theme refresh and the editor default each have a check that fails when that part of the change is reverted (see the test plan).

Test plan

  • Local build passes on the rebased branch (cmake --build build --target AetherSDR net_scheduler_dialog_test gui_nested_lifetime_test, macOS arm64, Qt 6.12.0, RelWithDebInfo), with no warnings in the touched files.
  • Behavior verified on a real radio if applicable
    • Not on a radio. Capture current VFO and Tune Now were not exercised against a live slice.
    • Through the automation bridge, offscreen, with isolated settings (AETHER_SETTINGS_DIR), AETHER_AUTOMATION_NO_TX=1, and no radio connected (get radio → "connected": false):
      • opened the window from Tools → Net Scheduler... and grabbed it empty and with rows;
      • opened Add…, grabbed the editor, saved two nets with OK, and confirmed both in NetSchedule.json;
      • Disable drew the row in canon.muted, with no ✓ and "—" for Next;
      • Remove → Cancel kept both nets (2); Remove → Remove (netRemoveConfirm) deleted one (2 → 1).
    • Ran the branch build visibly on macOS and looked at the window.
  • Existing tests pass (CI)
    • New net_scheduler_dialog_test (socket-free; it never touches a radio): 12 cases (15 rows), all passing 12 / 12 runs natively (cocoa) and 12 / 12 under QT_QPA_PLATFORM=offscreen:
      • it is a CanonWindow, a frameless Qt::Window, in canon/netScheduler;
      • a legacy base64 rectangle under NetSchedulerDialogGeometry restores at its size;
      • a 640×380 legacy rectangle opens at no less than 760×420;
      • the minimum is at least what the layout needs, and every button keeps its whole label there, under the application font and under a 160% letter-spaced font (a stand-in for wider metrics; the layout then needs 929 px natively, 949 px offscreen);
      • Return in the Remove box takes Cancel, the default: both nets stay and entriesChanged does not fire;
      • netRemoveConfirm removes the selected net and fires entriesChanged once;
      • Return in the Import box takes Skip existing: an imported net whose id exists keeps the local name, and only the new one is added (the test drives Qt's own file picker, AA_DontUseNativeDialogs);
      • a disabled row's ink follows a change to color.canon.muted;
      • Add… opens a CanonWindow titled "Add Net" with OK the default;
      • every spin, date and time field in the editor starts its text at least 12 px in from its border;
      • an editor field's right-click menu is parented inside the canon body and draws an opaque background;
      • Esc, Ctrl+W / ⌘W and the corner button each destroy the WA_DeleteOnClose window (Qt 6.12's QDialog::done() closes the window, though it delivers no closeEvent(); a standalone Qt 6.12 probe: reject() gives one hide, zero close events, and the window is destroyed). The Ctrl+W row activates the window first, since the shortcut is window-scoped, and skips if a desktop session refuses the activation.
    • Mutation-checked, one at a time, each failing the named case:
      • removing theme::setContainer();
      • Kind::Dialog;
      • removing setGeometryKey();
      • Remove as the box's default;
      • removing the themeChanged connection;
      • the 760×420 floor without the layout's minimum ("minimum 760x420, layout needs 929x226" on the wide-font row);
      • no minimum at all (the 640×380 legacy case opens under 760×420);
      • Skip existing not the import default (the default reads "Import as new");
      • OK not default;
      • dropping insetSpinText() (the case lists all six flush fields);
      • renaming the QMenu selectors so they match nothing (the menu grabs at alpha 0).
    • The test drives each box from inside its exec(). Its helper closes the box even when a check there fails, and gives up after 2 s if no box opens, so a failure ends the case rather than hanging it. With netRemoveConfirm renamed, the case fails in about 1 s. The ctest block also sets TIMEOUT 60.
    • gui_nested_lifetime_test: 10 / 10 offscreen (as ctest registers it), including netSchedulerEditorReturnsAfterOwnerDeletion (add-net and doubleclick-edit-net). That case closes the delete-on-close window while the canon editor's exec() is running. Its sources now link CanonWindow, CanonMessageBox and ModemChrome in place of PersistentDialog and FramelessWindowTitleBar.
    • CI does not run the new test before merge. It is not in ci.yml's frozen per-PR ctest -R list (docs/agents/tests-ci.md); it runs post-merge in full-suite.yml. The local runs above are the evidence.
    • Static checks steps run locally on the rebased branch pass: engine boundary, touchpoint manifest (regenerated), generated user-docs reference, test registration, frozen per-PR test gate, capability records, command plane, QSettings, theme seed, build options. User docs: npm ci && npm run build succeeds, and test_log_rules.mjs passes.
    • Colour-audit ratchet (tools/audit_colours.py --compare-src <main> --strict): unique colours 470 (+0), references 1811 (+0), setStyleSheet 563 (−2).
    • tools/check_a11y.py on the touched sources: no findings.
    • Not run: the five notices, which need a failure or no slice. They were checked by reading the code.
  • Reproduction steps documented if user-reported bug
    • n/a: not a bug report.

Screenshots: the window and the editor from this branch follow in a comment below.

Checklist

  • Commits are signed (docs/COMMIT-SIGNING.md)
  • No new flat-key AppSettings calls — use nested-JSON-under-one-key
    (Principle V)
    • The geometry key is the existing NetSchedulerDialogGeometry.
  • Code is clean-room — not decompiled, disassembled, or
    reverse-engineered from a proprietary binary (Principle IV)
  • All meter UI uses MeterSmoother (AGENTS.md convention)
    • No meter UI touched.
  • Documentation updated if user-visible behavior changed — docs/ and the
    affected READMEs. Not CHANGELOG.md, which is a release-prep file a
    PR must not add to (AGENTS.md); describe it in the Summary above instead
    • The three style docs and docs/user/docs/net-scheduler.md; the two user-guide screenshots are listed above to recapture.
  • Security-sensitive changes reference a GHSA if applicable

🤖 Generated with Claude Code

@w5jwp
w5jwp requested a review from a team as a code owner October 24, 2026 21:22
@w5jwp

w5jwp commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

After:

Image

@w5jwp

w5jwp commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Following up the #6761 triage. Pushed 232e36e6 and updated the body.

Fixed: right-click menus with no panel. The triage didn't raise this one, but it's the same defect #6735 and #6753 hit. The canon sheet's transparent QWidget rule reaches a field's standard menu, and the editor has 17 editable fields. The shared window sheet now gives QMenu its canon panel. The new editorFieldMenuIsOpaque case fails without it; the mutant's menu grabs at alpha 0.

Added: close paths. everyCloseDestroysTheWindow checks that Esc, Ctrl+W / ⌘W and the corner button each destroy the WA_DeleteOnClose window. The test is now 10 cases (12 rows), all passing.

Triage items, in the body now:

  • Monospace table: stated. The canon sheet sets the mono stack on QTableWidget as a whole, so the Name column is monospace too.
  • Geometry: stated that a saved rectangle under 760×420 is widened on first restore, with its position kept.
  • Contrast in both themes: the body now has a 12-pair table. One pair falls short: a disabled net's row on a non-alternate row in light is 4.22:1. That's flagged as a maintainer call (keep canon.muted, or use inkSoft and let the ✓ / "—" carry the state).
  • Spin-box inset duplicated with feat(gui): MQTT Settings is a CanonWindow dialog, re-skinned to the canon palette. Principle VIII. #6751: noted. Whichever lands second moves both onto one shared helper.

Already true at 701b5a3f:

  • the disabled row's ink is ModemChrome::colour("{{color.canon.muted}}"), not the OS palette;
  • Remove Net defaults to Cancel, with Remove in gold;
  • Import defaults to Skip existing, with Overwrite in gold;
  • the five notices each have a default OK;
  • the editor is Kind::Dialog;
  • FramelessResizer::install;
  • the fit test compares each button with its own sizeHint, with no fixed pixel width.

One triage point I didn't take: "wasFresh is always true, so drop the guard." It isn't. If the window is already open, triggering Tools → Net Scheduler... again finds the QPointer live, so the guard raises the existing window instead of building a second one. The body now says so.

@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

Partially — the implementation matches #6761 almost exactly, but the authorization it claims does not exist yet.

Every mechanical item #6761 asks for is delivered and I verified each against the head checkout: Kind::Workspace + canon/netScheduler + FramelessResizer::install (src/gui/NetSchedulerDialog.cpp:561-575), geometry under the existing key, the canon sheet with the window-local QDoubleSpinBox/QDateTimeEdit/popup rules, the merged editor form with the divider as a spanning row, the editor correctly left as Kind::Dialog so setDefault(true) still means something, all seven QMessageBoxes converted (I counted them: 5 × showNotice + Remove + Import merge), and both palette(...) sheets gone. The triage comment's three extra asks are all honoured too — the :586 QPalette::Disabled brush moved onto ModemChrome::colour(), and the contrast table covers both themes.

The PR also correctly declined the triage suggestion to drop the wasFresh guard. The triage reasoned it is always true; it isn't — the window is non-modal, so re-triggering the menu while it is open leaves m_netSchedulerDialog non-null. Keeping the guard is right.

What's missing is the ruling. See Blockers 1.

Scope

File / group What it changes Claimed? Verdict
src/gui/NetSchedulerDialog.{h,cpp} PersistentDialog → CanonWindow::Kind::Workspace; canon sheet; 7 CanonMessageBoxes; editor → canon dialog Yes In scope
src/gui/MainWindow_Nets.cpp Direct create + WA_DeleteOnClose instead of showOrRaisePersistent() Yes In scope — mirrors the SpotHub precedent at MainWindow_Menus.cpp:656-669 line for line
tests/net_scheduler_dialog_test.cpp, tests/tests.cmake New 10-case target; gui_nested_lifetime_test swaps PersistentDialog.cpp/FramelessWindowTitleBar.cpp for CanonMessageBox.cpp/CanonWindow.cpp/ModemChrome.cpp Yes In scope. I checked all 21 sources of gui_nested_lifetime_test: the only remaining PersistentDialog/FramelessWindowTitleBar hits are in comments, so the source swap doesn't break the link
docs/style/* (3 files) Adds Net Scheduler to the canon window / scope / geometry lists Yes In scope, but see Blockers 1 — these edits assert the maintainer decision as already made
docs/user/docs/net-scheduler.md Chrome paragraph, Remove-defaults-to-Cancel, and adds the previously-undocumented Tune Now button Yes (body calls out the Tune Now omission) In scope
docs/architecture/aetherd-touchpoints.md ThemeManager includers 183 → 184 Yes Correct — I counted 184 include "core/ThemeManager.h" in the head vs 183 on main
docs/gui-nested-loop-lifetime-audit.md Line anchor 173 → 212 Yes Wrong number — see Nits

No file is unexplained, no new protocol/settings surface, no removed guard. No CHANGELOG.md entry — correct.

Socket-test disclosure: tests/net_scheduler_dialog_test.cpp greps clean for QTcp|QUdp|QLocalServer|QWebSocket|listen(|bind(|connectToHost|Fake — genuinely socket-free, no peer. Nothing to flag.

Blockers

1. docs/style/dialog-patterns.md:273-277 — this is an unlisted pick, and the PR body states the opposite. The body opens "Part of #6239 and approved RFC #6226." The triage ruling on #6761 (still maintainer-review, still unanswered) says otherwise:

In #6239 the line "Persistent tool dialogs (one PR per dialog)" sits under "Candidates: surfaces whose widgets move from base tokens to color.canon.*" — a re-skin that keeps PersistentDialog chrome. The CanonWindow candidate list is a separate, closed set…

And the sentence this PR edits says so itself:

Use `CanonWindow` only for windows the
style guide covers: short-lived windows, and the tool windows the maintainer
has moved to it (… Radio Setup, Net Scheduler). … Other tool
and workspace dialogs keep the persistent pattern above.

Adding "Net Scheduler" to that parenthetical asserts a move the maintainer has not made. Merging the PR is the ruling. Sibling #6753 handles this honestly — "Waiting on the maintainer's pick … please don't merge before the pick." #6762 should carry the same banner. GOVERNANCE.md puts the decision with the maintainer; this is one of eight same-shape PRs opened 2026-10-24, and the triage note asks for one ruling to settle all eight.

Not a code defect — a disclosure one. Concretely: change "Part of #6239" to state it is an unlisted pick awaiting the #6761 ruling, as #6753 does.

2. tests/net_scheduler_dialog_test.cpp:98-114 — buttonsFitAtMinimumSize is the font-metric assertion the triage explicitly asked you not to land.

QVERIFY2(button->width() >= button->sizeHint().width(),
         qPrintable(QStringLiteral("%1 is %2 px, needs %3")

Triage on #6761:

760×420 is fine as a minimum, but it is font-dependent (canon mono metrics + canon button padding). CI renders on DejaVu Sans with themes unloaded, so the PR must not land a minimumWidth() <= N style assertion — report minimumSizeHint() before/after at a stated font instead (the 260 px rail lesson, #6131).

Because setMinimumSize(760, 420) is an explicit minimum, it overrides the layout's own, so the check is meaningful rather than tautological — which is exactly why it is font-sensitive: the seven canon buttons' sizeHint() is a function of the rendering font, and 760 is a hand-picked number validated on macOS arm64. On a CI image with different metrics this reds out with nothing regressed. This isn't hypothetical for this repo (#6131's rail-fit tests, and the exact-pixel probes that went the same way). Worse, net_scheduler_dialog_test is not in ci.yml's frozen per-PR ctest -R gate, so the failure lands post-merge in full-suite.yml, attributed to this PR after it is unrevertable in place.

Suggested shape — assert the layout fits, not that a hand-picked number happens to equal a font's metrics:

        // The minimum must fit what the layout needs, whatever the font:
        // compare the pinned minimum to the layout's own, rather than to a
        // number measured on one machine's fonts.
        dlg.layout()->activate();
        QVERIFY2(dlg.minimumSize().width() >= dlg.bodyWidget()->minimumSizeHint().width(),
                 qPrintable(QStringLiteral("minimum %1 px, layout needs %2 px")
                                .arg(dlg.minimumSize().width())
                                .arg(dlg.bodyWidget()->minimumSizeHint().width())));

If you'd rather keep the per-button loop, gate it on a stated font (set one in the test) so a CI font swap can't red it.

Needs maintainer decision

3. src/gui/NetSchedulerDialog.cpp:679 — the disabled-net row is 4.22:1 in the light theme, under docs/a11y.md:143's "4.5 : 1 for normal text against its background."

            const QColor muted = ModemChrome::colour("{{color.canon.muted}}", this);

Credit where due: the body discloses this and asks for the call. I reproduced the figure independently from resources/themes/default-light.json rather than taking it — canon.muted #506070 on canon.control #c8d2dc is 4.22:1; canon.muted on canon.raised #ffffff is 6.47:1, and dark is 5.39:1 / 6.39:1. So only the non-alternate light row fails, exactly as the table says. The whole contrast table checks out.

The body's alternative (canon.inkSoft, 5.60:1 on control) clears the bar but is the same ink as an enabled row, leaving the missing ✓ and the "—" in Next as the only disabled cue. Both options are defensible; it's your call, and whatever you pick applies to the rest of the wave. Nothing automated gates this — check_a11y.py has no contrast rule.

4. Disclosed visual consequences, flagged because AGENTS.md puts visual design with you, not because they look wrong: the window minimum grows 640×380 → 760×420 and the editor 440 → 560 px; the canon sheet makes the whole table monospace including the Name column; and the import box's button row reverses (main: Skip | Overwrite | Import as new | Cancel → now Cancel | Import as new | Overwrite | Skip existing, default unchanged at Skip existing).

Remove-defaults-to-Cancel is a fix, not a preference — it has authority: #6736 (merged, FirmwareInstallDialog) is the precedent the triage itself named, it moves a destructive action off Return, and CanonMessageBox.h:35-37 requires an explicit default anyway, which is what makes Return's target the same on all three platforms. No preference smuggling found anywhere in the diff.

Nits

All non-blocking.

  • docs/gui-nested-loop-lifetime-audit.md:48 — the anchor NetSchedulerDialog.cpp:212 matches nothing, at any baseline. editNetEntry is at 234 in the head, 225 at this branch's first commit (701b5a3), 173 on main, and 170 at the audit's own stated baseline c692c75e. Head line 212 is a bare break; inside the recurrence-rule switch. The doc's preamble also says "Line numbers refer to the baseline, so they remain reproducible after this patch," so arguably the row should have stayed at 173 untouched. Either way, 212 is wrong:

    | `NetSchedulerDialog.cpp:234` | Scoped editor, copied capture callback and stable schedule IDs across loops. Double-click opening is deferred until the table event returns. Scheduler is delete-on-close. |
    
  • The body attributes the legacy-geometry clamp to the wrong branch. It says "CanonWindow reads the old base64 rectangles … A rectangle narrower or shorter than the new 760×420 minimum is widened to it on the first restore (expandedTo(minimumSize()))". That call is only on the x,y,w,h branch, src/gui/CanonWindow.cpp:169; the base64 branch — the one every existing Net Scheduler user is on — is a bare return restoreGeometry(QByteArray::fromBase64(saved.toLatin1())) at :181, with no expandedTo and no screen-intersection re-check. And legacyGeometryCarriesOver seeds 780×520, already above the 760×420 minimum, so the below-minimum legacy rectangle the body claims to handle is the one case the test doesn't cover. Seeding a 640×380 legacy rect would close it in one line.

  • docs/style/dialog-patterns.md:243 propagates a stale claim. The row you appended "Net Scheduler" to still reads "(flushed — Escape and ⌘W / Ctrl+W hide it without a close event)", and src/gui/CanonWindow.cpp:129 says the same. That contradicts everyCloseDestroysTheWindow and the body's "Qt 6.12's QDialog::done() closes, it does not only hide" — and if it were true, WA_DeleteOnClose would never fire and the window would never be destroyed. Pre-existing text, not introduced here, but you're editing the line.

  • src/gui/NetSchedulerDialog.cpp:679 resolves the token inside the row loop, once per disabled row. Hoist it above the for (int row …); populateTable() runs on every commit() and on every theme change.

  • The import merge box has no test. setDefaultButton(skipBtn) at :845 is the one remaining place where Return's target could silently regress to a destructive action, and the body lists it under "Not run." removeDefaultsToCancel already has the harness — an import case is ~15 lines, and onNextModal would drive it the same way. Worth it precisely because the per-PR CI gate never runs this target.

  • docs/user/screens.json:2427 alt text isn't updated to name the canon header and corner close button, which sibling #6751 did in-PR for mqtt-settings-dialog. The body lists both shots as "stale, to recapture", so this is a consistency point only.

What I tried to break

Findings below are reasoned from the head checkout at /tmp/aetherclaude/pr-6762; I'm headless, so nothing here was reproduced at runtime, and I say so where it matters.

  • Link breakage from the tests.cmake source swap. Grepped all 21 sources of gui_nested_lifetime_test for PersistentDialog and FramelessWindowTitleBar symbols after both .cpps were dropped — three hits, all inside // comments. Also checked the new target against aether_assert_tests_retrofitted (tests/tests.cmake:10379): it links aethercore + Qt6::Widgets without naming aetherdesktop_support or aether_test_wisdom_isolation, but neither does gui_nested_lifetime_test and configure is green on all three platforms, so aether_retrofit_tests() supplies them. CMAKE_AUTOMOC is ON globally (CMakeLists.txt:14), so the #include "…moc" resolves.
  • Lifetime, the recurring minefield here. Walked onRemove, onImport and onExport post-exec(): if (!self || !boxOwner || box.clickedButton() != removeBtn) short-circuits on the owner before touching the dangling box reference, the ID relookup survives a list mutated during the loop, and onImport's final else return still catches both Cancel and the closed-another-way null, preserving main's QMessageBox behaviour. delete dlg.data() on a null QPointer in everyCloseDestroysTheWindow is a no-op. Found nothing.
  • The reopen path. MainWindow_Nets.cpp:63-84 — with WA_DeleteOnClose the QPointer nulls on close, so wasFresh is true again and the connections are remade exactly once per instance; entries come from m_netScheduler->entries(), which persistNetSchedule() keeps current, so no stale snapshot can be committed back. The ->show() at :75 is reached only after a fresh new or a non-null QPointer.
  • The themeChanged refill. populateTable() + setCurrentCell(row, 1) inside the lambda: setRowCount() with an unchanged count keeps rows, setItem replaces items, selection is restored and itemSelectionChanged re-runs updateButtons(). No reentrancy into onEdit, no entriesChanged emission (that's only in commit()), so a theme switch can't write the schedule file.
  • Whether the tests bite. removeDefaultsToCancel fails on main twice over (qobject_cast<CanonMessageBox*> is null, and the default reads Yes); disabledRowFollowsTheTheme can't pass against a QPalette-sourced brush; editorSpinTextIsInset goes red the moment insetSpinText() is removed, since the editor then sits ~1 px in. None of them assert the implementation back to itself. The one I don't trust is buttonsFitAtMinimumSize — Blockers 2.
  • Verified rather than taken: the 184 ThemeManager includers, the 4.22 / 5.39 / 6.47 contrast ratios recomputed from the theme JSONs (the nested-token indirection, so a flat grep would have missed them), the seven-popup count, and the audit-doc line number at four separate commits.
  • CodeGuard's 124 findings went through the security-audit subagent: no findings, all lexical false positives. NetSchedulerDialog.cpp:343 (CS-VLN-CMDI-AMP, CRITICAL) is for (const QByteArray& id : ids) — a C++ reference declarator over QTimeZone::availableTimeZoneIds(); the file has no QProcess, system(), openUrl or network call at all. The PanadapterStream.h backticks are Markdown in // comments, the SSRF-HEX hits are Flex wire constants and ESP32 MMIO register addresses, the test "credentials" are zero-filled buffers handed to crypto_ed25519_key_pair(), and the ctr2_proxy_model_test addresses are RFC 5737 TEST-NET-1. Credit: security-audit (CodeGuard). One genuine item it surfaced — the R2 credential in curl's argv at tools/publish_update_manifest.py:88 — is outside this diff and already fixed on main by #6746 (fe9e6b8ff); the branch is just behind. No action for this PR.
  • Couldn't check: anything at runtime. Specifically unverified by me — whether Qt 6.12's QDialog::done() really delivers a close event (the body's claim, which everyCloseDestroysTheWindow exists to pin, and which CanonWindow.cpp:129 contradicts); the actual rendered contrast; and whether 760 px really fits the seven buttons on CI's fonts, which is the substance of Blockers 2. All eight checks are green on 232e36e, but the per-PR ctest -R gate is frozen and does not include this target, so green here means it compiled.

Recommendation

Needs maintainer decision. The engineering is genuinely good — it answers every item in #6761 and all three of the triage's extra asks, the nested-loop lifetime handling survived a hostile read, and the contrast table and mutation list are the kind of evidence Principle VIII is asking for. But #6761 is still open and unruled, and this is a full CanonWindow move where #6239's tool-dialog line authorizes only a token re-skin; the PR's own edit to dialog-patterns.md:273-277 asserts the move as already made. That ruling covers eight PRs at once, not just this one.

Concrete next step: relabel the body as an unlisted pick awaiting #6761, the way #6753 does, and replace the per-button fit assertion in Blockers 2 with a layout-relative one so a CI font swap can't red full-suite post-merge. The nits are cheap and can ride the same push. Thanks for the contrast figures in both themes and for naming the 4.22:1 pair yourself instead of letting it through quietly — that's what made this reviewable.

Review event is COMMENT: every merge here needs a human, and severity is in the labels above, not in a state change.


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

@aethersdr-agent

Copy link
Copy Markdown
Contributor

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

  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/EspFlasher.cpp /tmp/aetherclaude/pr-6762/src/core/EspFlasher.cpp:32
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/EspFlasher.cpp /tmp/aetherclaude/pr-6762/src/core/EspFlasher.cpp:34
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/EspFlasher.cpp /tmp/aetherclaude/pr-6762/src/core/EspFlasher.cpp:35
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/EspFlasher.cpp /tmp/aetherclaude/pr-6762/src/core/EspFlasher.cpp:36
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/EspFlasher.cpp /tmp/aetherclaude/pr-6762/src/core/EspFlasher.cpp:37
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/PanadapterStream.cpp /tmp/aetherclaude/pr-6762/src/core/backends/flex/PanadapterStream.cpp:242
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/backends/flex/PanadapterStream.cpp /tmp/aetherclaude/pr-6762/src/core/backends/flex/PanadapterStream.cpp:907
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/backends/flex/PanadapterStream.cpp /tmp/aetherclaude/pr-6762/src/core/backends/flex/PanadapterStream.cpp:1574
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6762/src/core/backends/flex/PanadapterStream.h:280
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6762/src/core/backends/flex/PanadapterStream.h:300
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6762/src/core/backends/flex/PanadapterStream.h:145
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6762/src/core/backends/flex/PanadapterStream.h:153
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6762/src/core/backends/flex/PanadapterStream.h:155
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6762/src/core/backends/flex/PanadapterStream.h:164
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6762/src/core/backends/flex/PanadapterStream.h:202
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6762/src/core/backends/flex/PanadapterStream.h:376
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/NetSchedulerDialog.cpp /tmp/aetherclaude/pr-6762/src/gui/NetSchedulerDialog.cpp:343
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:77
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:78
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:1679
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:3017
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:5533
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:5534
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:6841
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:6847
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:6883
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:7122
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:9840
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:10233
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:10743
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:11339
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:11340
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:11654
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:11662
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:11827
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:12014
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:12021
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:12099
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:13230
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6762/src/models/RadioModel.cpp:13296

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

w5jwp and others added 3 commits October 24, 2026 22:07
…rRX and AetherTX, and its questions are CanonMessageBoxes. Principle VIII.

Net Scheduler moves from a PersistentDialog to a CanonWindow::Kind::Workspace
in its own canon/netScheduler scope, keeping NetSchedulerDialogGeometry so the
saved size and position carry over. The Add / Edit Net editor is a canon dialog
with Cancel / OK, and all seven QMessageBoxes are CanonMessageBoxes. Remove Net
defaults to Cancel; Import keeps Skip existing as its default.

The canon controls sheet covers the window, with window-local rules for the
fields and popups it does not style. Spin, date and time fields get a text
margin, since a stylesheet-framed spin box drops its left padding on Qt 6.12.
The minimum grows to 760x420 so the canon buttons and table text fit.

New net_scheduler_dialog_test covers the workspace shape and scope, the
geometry carry-over, the button fit, the Remove default, the theme refresh,
the editor default and the spin text inset; each fails with its part reverted.

Closes aethersdr#6761.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ns it and the close paths. Principle VIII.

The canon controls sheet clears every descendant to transparent, and Qt
parents a field's standard right-click menu (Undo/Cut/Copy/Paste) to the field,
so the menus on the editor's name, frequency, spin, date and time fields drew
with no panel: the mutant's menu grabs at alpha 0. The window sheet, which the
editor and the message boxes share, gives QMenu its canon panel back, as on
TX Band Settings.

net_scheduler_dialog_test gains editorFieldMenuIsOpaque, which fails without
the QMenu rules, and everyCloseDestroysTheWindow: Esc, Ctrl+W / Cmd+W and the
corner button each destroy the WA_DeleteOnClose window, so the next open
starts from the schedule MainWindow passes in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
@w5jwp
w5jwp force-pushed the feat/net-scheduler-canonwindow branch from 232e36e to 927d085 Compare October 25, 2026 03:33
@w5jwp

w5jwp commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Pushed 927d0856 on top of a rebase onto main at be216ed3. #6735, #6745 and #6768 had added windows to the same style-doc lists; every entry is kept. The body is updated.

1. Unlisted pick (disclosure). Done. The body now opens with the same banner as #6753: this full CanonWindow move isn't on #6239's CanonWindow lists, #6761 lays out both options, please don't merge before the pick, and merging is the pick, because the style docs then list Net Scheduler among the windows the maintainer has moved.

2. buttonsFitAtMinimumSize is font-dependent. Agreed, and the window was too: an explicit setMinimumSize(760, 420) overrides the layout's minimum, so a wider font would have clipped the buttons. So this is fixed in the code, not only in the test:

  • The minimum is now QSize(760, 420).expandedTo(minimumSizeHint()), recomputed on themeChanged. 760×420 stays the floor. The layout needs 713 px under macOS fonts and 728 px offscreen, so nothing visible changes here.
  • The test (minimumFitsTheLayout) uses your layout-relative assertion: the minimum is at least minimumSizeHint(). It keeps the per-button check at that minimum. It runs two rows: the application font, and a 160% letter-spaced font standing in for wider metrics (the layout then needs 929 px natively, 949 px offscreen).
  • Mutation: with the plain 760×420 floor, the wide-font row fails with "minimum 760x420, layout needs 929x226" (cocoa) / "…949x227" (offscreen). Restored, it passes.

3. Disabled row at 4.22:1 (light). Still the maintainer's call, unchanged. Options as in the body: keep canon.muted, or use canon.inkSoft (5.60:1) and let the ✓ and "—" carry the state.

4. Visible consequences. Still listed for the maintainer: 760×420 / 560 px minimums, the monospace Name column, and the import box's button order (default unchanged at Skip existing).

Nits:

  • Audit anchor: you're right that 212 matched nothing. The row is back to 173, its baseline value, as the doc's preamble asks. At baseline c692c75e, line 173 is QDialog dlg(parent);, the scoped editor the row describes; 170 is the function signature. The branch no longer changes that file.
  • Legacy-geometry attribution: right, expandedTo is only on the x,y,w,h branch. The body was wrong; the behaviour is right. The base64 branch is a bare restoreGeometry(), and Qt clamps the restored size to minimumSize(). New case legacyGeometryBelowMinimumIsWidened seeds a 640×380 base64 rectangle, and the window opens at 760×420 under both cocoa and offscreen. The body now says Qt does the clamp. With the updateMinimumSize() call removed from the constructor, the case fails.
  • "Hide it without a close event": half true. A standalone Qt 6.12.0 probe: on a WA_DeleteOnClose QDialog, reject() gives 1 hide event, 0 closeEvent() calls, and the window is destroyed. close() gives 1 and 1. So "without a close event" is correct, but "hide" undersells it. dialog-patterns.md's row and the CanonWindow::hideEvent() comment now say Escape and ⌘W / Ctrl+W close the window through reject(), so WA_DeleteOnClose applies, without a close event. That's also why the flush lives in hideEvent().
  • Token in the row loop: hoisted. canon.muted is resolved once per populateTable(). disabledRowFollowsTheTheme still passes.
  • Import box untested: added importDefaultsToSkipExisting. It uses Qt's own file picker (AA_DontUseNativeDialogs) and fills its file-name field. Return in the box: the default reads "Skip existing", the existing net keeps its name, and only the new net is added (2 → 3). Mutation: with setDefaultButton(dupBtn), it fails with the default reading "Import as new", natively and offscreen.
  • screens.json alt text: both Net Scheduler alt texts now name the canon header and the corner close button, and the page's <img> tags were re-embedded with embed_screenshots.py. The same check turned up a real breakage: both recipes targeted the editor as QDialog/..., and it's now a CanonWindow named netEditorDialog, so those targets matched nothing. They now use netEditorDialog/.... I haven't re-run capture_screenshots.py; the two images are still listed as stale.

Also fixed: the Ctrl+W / Cmd+W row of everyCloseDestroysTheWindow was flaky natively. With the test as pushed in 232e36e6 it failed 3 of 8 full native runs, while it passed in isolation. The close shortcut is window-scoped, so the row now activates the window first, and skips (rather than fails) if a desktop session refuses the activation. Offscreen, where ctest runs it, activation always succeeds.

Test runs on the rebased branch: net_scheduler_dialog_test is now 12 cases (15 rows), and passed 12 / 12 runs natively and 12 / 12 with QT_QPA_PLATFORM=offscreen. gui_nested_lifetime_test passed 10 / 10 offscreen, which is how ctest registers it. AetherSDR links. Static checks run locally pass (engine boundary, touchpoint manifest regenerated 184 → 185 for this branch's include, generated reference, test registration, frozen gate, capability records, command plane, QSettings, theme seed, build options). check_a11y.py has no findings on the touched files. The colour ratchet is +0 / +0 / −2 setStyleSheet. The user docs build passes.

One thing I saw that this PR doesn't touch: run natively on macOS, gui_nested_lifetime_test's customFilterDialogAppliesOnlyToOriginalSlice fails both rows ("menu" / "openedDialog"). It passes offscreen. I didn't check whether it fails the same way natively on main.

Resolves the conflicts in the three docs/style/* canon-window lists, where
main added FFTW Wisdom / canon/nr2Wisdom. Every list keeps main's entries
and this branch's.

Regenerate docs/architecture/aetherd-touchpoints.md: main and this branch
each added a core/ThemeManager.h include, so the merged count is 186.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

Net Scheduler on CanonWindow (a #6239 persistent tool dialog)

1 participant