Repository navigation
ui(peripherals): organize device settings with Add/Remove - #6027
Conversation
…ciple XI. Address aethersdr#6026 triage for aethersdr#6027: allow closing after a 15-second unconfirmed deletion timeout, retain the vault-owned reconnect lease, and ignore late UI completion. Document that legacy endpoint-key migration is out of scope.
There was a problem hiding this comment.
Issue fit
Partially — against RFC #6026 and #6026's acceptance criteria, the UI-only commits (0063b943, d8aad51e) do what they claim, and I could not break the two hardest parts: the reconnect lease and the bounded close. Every connectTo* entry point is gated (three funnels, no bypass — see "What I tried to break"), and checkRemovalTimeout is a real regression, not a tautology. One acceptance criterion does not hold as written: "Remove reports pending, successful, failed and unconfirmed credential cleanup accurately" and "Removing ShackSwitch preserves a live AG session" — the ShackSwitch row never asks the vault to delete anything, and the AG row deletes a slot the ShackSwitch may own (finding 1).
The layout/workflow decision itself is correctly routed: #6026 carries maintainer-review and states approval is outstanding. I am not treating the redesign of the existing Peripherals table as smuggled preference — it went through an RFC, which is exactly what GOVERNANCE.md asks for. It is a maintainer call, not a blocker.
Scope
| Group | What it changes | Claimed by this PR? | Verdict |
|---|---|---|---|
docs/peripherals-settings-ui.md, docs/architecture/aetherd-touchpoint* |
new workflow doc; manifest 230→232, AppSettings 103→105, ThemeManager 156→157 | yes | In scope; counts match the tree |
src/core/PeripheralSettings.h |
VisibleDevices, DiscoveryDismissed, clearDeviceConnection in the existing nested Peripherals doc |
yes | In scope. No new flat keys — Principle V satisfied |
src/core/PeripheralRemovalGuard.h, guard checks in TgxlConnection.cpp:82 / PgxlConnection.cpp:81 / AntennaGeniusModel.cpp:316, m_connectTransport seams |
transient reconnect lease + socket-free injection | yes | In scope |
src/gui/RadioSetupDialog.{cpp,h} (+1139 lines) |
list/detail, Add/Remove, pending + 15 s timeout, AG reconciliation | yes | In scope, pending the #6026 ruling |
src/gui/PeripheralAuthStore.* |
delete-completion / cache-retention handling | yes | In scope |
src/models/AntennaGeniusModel.* |
deferred ShackSwitch request/retry | yes | In scope |
src/gui/MainWindow_Wiring.cpp |
discoveredTarget() on the three discovery fallbacks |
yes | In scope |
tests/peripheral_auth_dialog_test.cpp (+1349), peripheral_auth_keychain_test.cpp, fakes/PeripheralAuthStoreFake.* |
socket-free coverage | yes | In scope |
Inherited #6008 commits 120ed439…2e58e6f4: .gitignore, CMakeLists.txt, AutomationSensitiveLineEdit.h, AutomationServer.cpp, LogManager.cpp, PeripheralAuthCode*.h, the TGXL/PGXL/AG auth state machines, AmpApplet/AntennaGeniusApplet/TunerApplet/ShackSwitchApplet, tools/probe_4o3a_auth.py, 6 test files |
no — disclosed in the body as the prerequisite | Needs maintainer sequencing. 7,179 insertions / 49 files land if this branch merges as-is. The disclosure is honest and unusually clear, but the unbundling is "merge #6008 first", not something the diff does | |
AmpApplet source-indicator moved to bottom-right + tests/pgxl_panel_test.cpp assertions rewritten |
visual relocation of an existing applet widget | neither title claims it | Belongs to #6008's review, not this one. Flagging so it does not slip through on a UI-titled PR |
No CHANGELOG.md edit — correct.
Socket tests (surfaced, not a finding): this PR adds no new socket-owning test; the eight focused tests are all injection-based (I grepped the added sources for QTcpServer/QUdpSocket/listen/connectToHost — clean). It does modify one existing loopback test, tests/pgxl_panel_test.cpp (binds QTcpServer::listen(QHostAddress::LocalHost, 0) at line 121), inside the inherited auth commits; the change is layout assertions only. Neither that test nor any of the eight focused targets appears in ci.yml's -R filters, so see the CI note below.
Blockers
None that I can confirm at this severity. One finding is a genuine contradiction of a stated invariant but its blast radius depends on firmware I cannot observe, so I have labelled it plausible rather than certain — see inline comment 1 and the request below.
Findings
-
Remove is asymmetric across the shared AG credential slot (inline,
RadioSetupDialog.cpp:10749).device.row >= 1 && device.row <= 3puts ShackSwitch (row 4) on theelse { finishRemoval(device); }path — noPeripheralAuthStore::clear, noPeripheralRemovalGuard. There is exactly one AG slot (SettingsCredentialPolicy.h:39,antenna_genius_auth_code) andMainWindow_Wiring.cpp:6524savesauthCodeAcceptedunderDevice::AntennaGeniusregardless of target, while the AUTH trigger atAntennaGeniusModel.cpp:581keys only on the greeting, not the device name. So removing ShackSwitch leaves its code in the vault, and removing AG deletes whichever target owns the slot. The #6026 invariant holds for the socket, not for the credential. Reasoned from code — I cannot confirm ShackSwitch firmware sendsAG AUTH. Either gate row 4 the same way, or state in the doc that ShackSwitch never authenticates. -
The removal timeout discards unrelated pending edits (inline,
RadioSetupDialog.cpp:10789).m_peripheralRowSavers.clear()drops every row's saver, not the removed row's. Clear the ACOM address, then Remove TGXL, then time out and close: the ACOM clear is silently lost. The doc says "closing does not save abandoned field edits" — this drops edits that were never part of the abandoned Remove.
Nits (non-blocking)
listGroup->setFixedWidth(250)/deviceList->setFixedHeight(340)(RadioSetupDialog.cpp:10244,:10266) are hard pixels. The evidence doc records a prior clipped-control problem at the default dialog size; fixed extents at 125 % scaling or a larger UI font are how that comes back.setMinimumWidth+ a stretch would be safer.docs/peripherals-settings-ui.md:7lists seven supported pages and omits LP-100A Meter, whichdevicesincludes (row 9,RadioSetupDialog.cpp:10232) and whichfinishRemovalhandles.- Two id conventions now coexist in the same
PeripheralsJSON root:VisibleDevices/DiscoveryDismisseduse lowercase ("tgxl","acom"), whileclearDeviceConnection/deviceStringuse"Acom"/"Vkamp"/"Lp100a". Functional, but a future endpoint migration will have to reconcile them. PeripheralRemovalGuard.h:33:std::array<unsigned, 3>non-atomic, with the main-thread requirement carried only in a comment. Nothing enforces it and the header sits incore/.
What I tried to break
- Guard bypass. Grepped every connect path:
connectToTgxl,connectToPgxl,AntennaGeniusModel::connectToDeviceare the only three places reachingconnectToHost, and bothconnectToAddressoverloads funnel throughconnectToDevice. The gate is at the funnel, not at optional call-site wrappers. No bypass found. - Guard leak on teardown. The
clear()callback is registered againstqApp, capturesremovalby value, and releases on lambda destruction on every exit — includingif (!self || *timedOut) return;and therequest.context-null branch instartNextWrite. Destroying the parent window mid-delete does not free the lease early. Held. - Double Remove after timeout. The timeout sets
m_peripheralRemovalPending = false, which looked like it re-opened Remove — butremoveButtonis a child ofcontent, which stayssetEnabled(false), andPeripheralRemovalGuard::pending()early-returns anyway. Two independent stops. Held. - Test quality.
checkRemovalTimeout(tests/peripheral_auth_dialog_test.cpp:433) assertsconnects == 0while the lease is held andconnects == 1after late completion, across 3 devices × success/failure × dialog-alive/destroyed, with the deadline stopped and itstimeoutinjected rather than slept. Emptying the gate or the release would fail it. It is not asserting the implementation back to itself. finishRemovalordering. Checked that SS_ManualIp is removed beforedisconnectFromDevice(), so theMainWindow.cpp:2266handler sees an empty key and hides the SS button. Correct order. Also verifiedkPortKeys[4] == "SS_ControlPort"matchesbuildRow(4, …)at:9297— no stale-port leak.- Could not check: anything at runtime. No build, no test run, no GUI. Every finding above is reasoned from the head checkout at
/tmp/aetherclaude/pr-6027, not reproduced. Windows/Linux behavior, native accessibility, real OS-vault semantics and ShackSwitch AUTH behavior are all unverified here — as the PR body itself states.
CI note: 5/5 green on d8aad51e (build, check-macos, check-windows, sanitizer configure, static checks). But none of the eight focused tests — peripheral_auth_dialog_test, peripheral_auth_handshake_test, peripheral_auth_keychain_test, the two automation-redaction tests, tgxl_docked_parity_test, tgxl_applet_ports_test, amp_applet_test — appears in any ctest -R filter in ci.yml. Green means the tree compiles on three platforms; it does not mean this PR's coverage ran. The contributor's local run is currently the only evidence they pass.
Recommendation
Needs maintainer decision, on two independent axes, then request changes on finding 1.
The sequencing is the first call: as it stands this branch carries all of #6008, including an AmpApplet indicator relocation that neither PR title mentions. The contributor has been scrupulous about disclosing this and is not asking for a merge — the ask is to review 0063b943 + d8aad51e on top of #6008, which is a reasonable request and what I did. The second call is #6026 itself: layout, Remove semantics, TGXL/PGXL discovery dismissal, and the blocking-close behavior are all yours to approve.
Concrete next step: rule on #6026, then ask for finding 1 (gate row 4 the same as rows 1–3, or document that ShackSwitch never authenticates) and finding 2 (scope the saver clear to the removed row) before the post-#6008 replay. Findings 3–6 are cleanup that can ride along. This is careful, well-instrumented work with an unusually honest evidence boundary — the credential asymmetry is the one place the implementation and the stated invariant have drifted apart.
🤖 aethersdr-agent · cost: $13.7761 · model: claude-opus-5
|
Cisco CodeGuard — static analysis of this PR (6 finding(s))
Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them. 🤖 aethersdr-agent · cost: $13.9818 · model: claude-opus-5 |
… XI. Address PR aethersdr#6027 review: endpoint-scoped AG/ShackSwitch removal, queued replacement protection, row-scoped timeout cleanup, flexible list layout, and documented settings/thread contracts. Add socket-free regressions and mutation evidence.
|
@aetherbot @aethersdr-agent — pushed 5c07925 addressing this review.
Validation on the restored final code: desktop build, all eight focused tests (17.81 s), scaled dialog runs, registration, generated manifest and strict engine/capability/command-plane checks passed. Mutations that restored whole-page saver clearing, bypassed endpoint ownership, or excluded ShackSwitch from removal guarding each failed their regression test. The demo reported one slice, one panadapter, TX disabled/transmitting=false; the review-owned instance was stopped. No live firmware or actual OS-vault evidence is claimed. #6027 remains ready for review, must merge after #6008, and still needs the maintainer's #6026 UI ruling. The final post-#6008 replay and CI are still required; these fixes do not imply approval or request automatic merge. |
ten9876
left a comment
There was a problem hiding this comment.
Issue fit
Partially. This was reviewed as the increment over #6008 (git diff 71d5bcd7 0ef90af5, 22 files), against RFC #6026. The list/detail page, Add/Remove, the bounded close, and the reconnect lease all work as described. The previous review's two findings are fixed in 5c079250: the shared AG/ShackSwitch credential is now endpoint-scoped, and the timeout no longer drops unrelated edits.
Two #6026 acceptance points fail at this head:
- "Remove reports … failed and unconfirmed credential cleanup accurately" fails for AG and ShackSwitch when no keychain backend is available. I reproduced it (blocker 1).
5c079250regressed ShackSwitch removal (blocker 2).
The RFC itself has no maintainer ruling yet, and #6008 has not merged. The body discloses both, so I list them as sequencing, not code findings.
Scope
| File / group | What it changes | Claimed? | Verdict |
|---|---|---|---|
src/gui/RadioSetupDialog.{cpp,h} |
list/detail page, Add/Remove, 15 s bounded wait, AG reconciliation | yes (#6026) | in scope, pending the #6026 ruling |
src/core/PeripheralRemovalGuard.h + gates in Tgxl/PgxlConnection.cpp, AntennaGeniusModel.cpp |
transient reconnect lease | yes | in scope |
src/core/PeripheralSettings.h |
VisibleDevices, per-device DiscoveryDismissed in the existing Peripherals blob |
yes | in scope; no new flat keys |
src/gui/PeripheralAuthStore.*, PeripheralAuthConnectFlow.h |
endpoint-scoped delete, configuredEndpoint, delete-completion handling |
yes | in scope; see blocker 1 |
src/gui/MainWindow_Wiring.cpp |
discoveredTarget() honours dismissal |
yes | in scope; see M1 |
docs/peripherals-settings-ui.md, touchpoint manifest (+PeripheralRemovalGuard.h, peripheral(4o3a); AppSettings 104→105) |
doc + manifest | yes | in scope; additions only |
docs/4o3a-remote-auth-hardware-evidence.md (+181/−10) |
rewrites part of #6008's evidence doc | partly | nit N5 |
tests: peripheral_auth_dialog_test (+1430), peripheral_auth_keychain_test, fake store |
socket-free coverage | yes | in scope; see blocker 1 on the fake |
| everything else (≈28 files) | inherited from #6008 | disclosed | reviewed on #6008 |
No CHANGELOG.md edit. Against main this branch is still 50 files, +7586/−268, until #6008 lands and it is replayed.
Socket tests: none added. The dialog test's Connect clicks are all refused before connecting (invalid code, empty address), and lifecycle tests inject m_connectTransport. The two pre-existing loopback PGXL tests are inherited from #6008, and the operator approved running them for this review.
Blockers
1. AG and ShackSwitch can never be removed when the keychain backend is unavailable, and the error says a code "remains in keychain" when none may exist.
src/gui/PeripheralAuthStore.cpp:397, in clearForEndpoint:
if (!entry.loaded || entry.status == LoadStatus::Unavailable) {
callback(ClearResult::Failed);load() leaves loaded = false and sets Unavailable for every read error except EntryNotFound (:270-283), including NoBackendAvailable and NotImplemented.
TGXL and PGXL go through clear(), which maps the same backend loss to SessionCleared and lets removal proceed. Your own keychain test states the invariant: "No OS backend must not prevent local removal, even if this device never had a stored code."
Reproduced. I added a temporary probe to tests/peripheral_auth_keychain_test.cpp, which uses the production adapter with the QtKeychain double. It calls clearForEndpoint(AntennaGenius, …) and fails the read with NoBackendAvailable. Then it retries the same call.
aether.peripheral.auth: keychain read failed: "no backend"
PROBE called=1 result=2 (Cleared=0 SessionCleared=1 Failed=2)
aether.peripheral.auth: keychain read failed: "no backend"
PROBE retry called=1 result=2
So Linux without Secret Service/KWallet, on a QtKeychain build, can never remove AG or ShackSwitch. "Retry Remove" fails forever.
The dialog test can't see this. Its fake (tests/fakes/PeripheralAuthStoreFake.cpp:144-155) falls through to clear() whenever the endpoint matches, so production's Unavailable → Failed branch is never exercised.
Fix: on Unavailable with NoBackendAvailable/NotImplemented, return SessionCleared as clear() does (keep Failed for AccessDenied). Add a keychain-test case through clearForEndpoint, plus a fake that models the read failure.
2. Removing a connected ShackSwitch leaves its applet button on screen (regression from 5c079250).
RadioSetupDialog.cpp:10789callsm_ag->disconnectFromDevice()when Remove starts, whileSS_ManualIpis still saved.disconnectFromDevice()emitsdisconnectedsynchronously. The handler atMainWindow.cpp:2276-2285hides the SS button only ifSS_ManualIpis already empty, so it keeps the button.finishRemovalcase 4 (:10661-10665) now only callssetAuthCode({}). Atd8aad51eit calleddisconnectFromDevice()after the keys were removed, and that seconddisconnectedis what hid the button. Nothing emits it now.- The button for a removed device stays until restart or rediscovery.
Traced in source, not driven: the demo has no ShackSwitch. A fix is to hide the SS button explicitly in finishRemoval for row 4, or to re-emit through a model call once the keys are gone.
Maintainer decisions (#6026)
- M1. Remove used as "undo" permanently dismisses discovery.
- Bridge evidence: on this head, in demo mode with an isolated store, I ran Add → Tuner Genius XL, then Remove without entering anything. The store then held:
Peripherals|{"VisibleDevices":[],"tgxl":{"DiscoveryDismissed":true}} RadioSetupDialog.cpp:10639sets the flag unconditionally for TGXL/PGXL. From then on,discoveredTarget()suppresses direct auto-connect to a discovered TGXL/PGXL on every radio connect, across restarts. The applets stay visible on the relay, so nothing tells the operator.- TGXL loses manual relay control (#469) and PGXL loses direct telemetry (Vac/Vdd/HL/fan/MEffA). Add or an explicit Connect undoes it, but only an operator who knows the flag exists will think to.
- At minimum, only dismiss when the device was actually configured or discovered.
- Bridge evidence: on this head, in demo mode with an isolated store, I ran Add → Tuner Genius XL, then Remove without entering anything. The store then held:
- M2. The RFC ruling itself: the layout, Remove semantics, discovery dismissal, and the blocking close.
Nits (non-blocking)
- N1. ShackSwitch credential can be orphaned while Remove reports success. The SS connect path saves under port 9007, but offline Remove computes
configuredEndpoint(SS_ManualIp, SS_ControlPort)(:10772-10776). If the port spin ever differed, the endpoints don't match andclearForEndpointreturnsClearedwithout deleting (PeripheralAuthStore.cpp:405-407). The doc's "fails closed" covers only an empty host. Reasoned from code. - N2. a11y (#4896).
- None of the Removing / timed-out / failed / succeeded notices calls
QAccessible::updateAccessibility, so screen readers don't announce them (docs/a11y.md, live value updates). - Observed on the bridge: after typing a code, the field's
accessibleDescriptionstill reads "Code blank. A saved code will be checked when connecting.":11092-11098keys only on the saved status, never on the field's text. - Accessible names are inconsistent on one page (
TGXL authorization code/Tuner Genius XL show authorization code/TGXL clear saved authorization code), and the port spin box has no accessible name.
- None of the Removing / timed-out / failed / succeeded notices calls
- N3. i18n.
:11026comparesdetailStatus.startsWith(tr("Connecting"))against the untranslatedsetText("Connecting...")at:8871. In a translated build that shows "Offline" while connecting. Accessible names are also built by concatenation (device.label + tr(" address")), which breaks word order in translation. - N4. After a timeout, close and reopen, Remove and Connect do nothing while the lease is still held. Remove returns silently at
:10759, and Connect leaves "Connecting..." stuck. A message naming the pending deletion would help. - N5. This PR rewrites #6008's own evidence-doc sentences (+181/−10), which will conflict with any further #6008 edit. It also still says Setup "prevents editing or closing until the vault completes", which contradicts the 15 s bound. A separate evidence doc for #6027 would avoid both.
What I verified vs. read
- Built
0ef90af5on Linux and ran the 13 focused targets: 13/13 pass, includingperipheral_auth_dialog_test(6.7 s) and the two inherited loopback PGXL tests (operator-approved). - Reproduced blocker 1 with the production adapter (output above). The temporary probe was reverted.
- Bridge: offscreen, isolated
AETHER_SETTINGS_DIR/HOME, explicit socket,AETHER_AUTOMATION_NO_TX=1, attached toDEMO-0001only (get radio→"model":"AetherSDR Demo","serial":"DEMO-0001"). LAN discovery listed the operator's FLEX, which was never connected.- Verbs:
menu list,invoke "Radio Setup..." trigger,invoke radioSetupNavigation setCurrentText Peripherals,invoke peripheralAddButton click,invoke "Tuner Genius XL" trigger,invoke peripheralAuth_tgxl_4 setText, Show,peripheralRemoveButton click,dumpTree,grab, then a sqlite read of the store. - The empty-state page, Add menu (8 devices), detail page, redaction and Remove all behaved as described, apart from M1 and the N2 description. The instance was closed.
- Verbs:
- Gates: all four checks are flat versus #6008 (102 findings; EB3 rows in
RadioSetupDialog.cppshift lines only). The manifest check passes, and the tag sidecar has one addition. - Attacks that held (reasoned from code, read-only second pass):
- every
connectTo*funnel is gated by the lease; - the lease is released on every path, including owner teardown;
- a dialog destroyed mid-delete is a safe no-op;
- Remove twice, and Add during a pending delete, are both blocked;
- late completion after a timeout never applies the abandoned Remove;
- keychain callbacks stay on the main thread, as the non-atomic guard requires.
- every
- CI is stale: the earliest run on this head was created 2026-10-01 22:26Z, before main's 2026-10-02 19:07Z tip. The branch now conflicts with
mainin the same three files as #6008, all inherited. The focused tests are not in anyci.yml -Rfilter. - Not verifiable here: a real OS vault, ShackSwitch/AG hardware, and Windows/macOS accessibility.
… state the 15 s removal bound. Principle XI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the review. Pushed Blocker 1 ( Blocker 2 ( M1 mitigation ( Nits
One test change you should look at: #6008's N1 means a keychain outage no longer blocks, so Built and ran the focused targets locally; |
|
@ten9876 the updates from your review are pushed ( |
… connections (#6008) **Merge order for the PGXL / 4O3A PRs** (the same map is in each of them; #6030 has merged): ``` Stage 1: #6008 authenticate TGXL, PGXL and Antenna Genius Stage 2 (either order, after what each needs): #6027 Peripherals settings UI needs #6008 #6033 PGXL drain current (ID meter) needs #6008 Stage 3: #6036 PGXL docked layout needs #6008 and #6033 ``` ## Summary Fixes #2313. Authenticate TGXL, PGXL, and Antenna Genius direct TCP sessions when a device's greeting requests `AUTH`; keep ordinary commands gated until the device accepts the code. Use the existing manual host/port fields in **Setup → Peripherals** and restore distinct, endpoint-bound codes from QtKeychain on reconnect. Builds without QtKeychain retain a code for the current session only. Keep TGXL/PGXL radio-relay fallback available when the direct path cannot authenticate, and show the actual connection path in the accessory applets. Target `main` at `v26.9.5`. This branch started at the preceding commit; the sole overlapping release file is `CMakeLists.txt`, and this branch's CMake change applies cleanly to the release version. `docs/PR-WORKFLOW.md` does not require a rebase for a stale PR branch. The auth command and success-reply forms are device-specific. A live probe confirmed TGXL and PGXL forms on the firmware listed in `docs/4o3a-remote-auth-hardware-evidence.md`; the AG syntax follows 4O3A's public API documentation and uses CRLF, which the contributor capture confirms reaches the AG auth parser. New code is saved only after an AUTH success reply. A code typed for another target, rejected, or never challenged cannot silently replace a working saved code. ### Implementation details - Gate the TGXL, PGXL, and AG initialization/status paths on AUTH completion; reject malformed/oversized lines and invalid code characters before constructing a line-oriented command. - Keep one Keychain record per device type with the accepted IP/port and code together. Never write a new plaintext password field into AppSettings; retain the existing manual IP/port settings. A Keychain read failure stays visible, and a failed Clear operation does not claim the code was deleted. - Bound authentication attempts and suppress automatic retries after a blocking failure. Manual AG/ShackSwitch retries reset only the selected target's failure budget. An attempt token ignores late Keychain replies from an abandoned connection. - Redact credentials from PGXL setup logging, sensitive automation text extraction, and UI grabs. Register the new auth logging category and add focused socket-free credential, handshake, automation, and applet coverage. - Regenerate the aetherd touchpoint manifest for the new peripheral validation header; add the credential-store source to the desktop-support target. The new GUI→engine validator touchpoint keeps one input rule in the Keychain adapter and TCP clients before a line-oriented command is built. The manifest's AppSettings includer count grows by one for the session-only credential fallback in the Keychain adapter. ThemeManager grows by one because the extracted dialog connect-flow header uses the same token-based error style as the dialog; that header exposes a socket-free regression seam for target-switch ordering. - Add `tools/probe_4o3a_auth.py`, a permanent one-shot, non-TX hardware handshake probe; it prompts for the code without echo and records no credentials. ## Constitution principle honored - **Principle IV — Every Contribution Is Clean-Room:** the TGXL and PGXL handshake code is grounded in a redacted live probe and published 4O3A documents; AG follows the published 4O3A API plus the contributor capture cited below. No proprietary binary was disassembled or translated. - **Principle V — Each Feature Owns Its Configuration As A Single Object:** no new flat AppSettings credential writes were added. Existing manual connection settings remain as they were; each stored credential is a single Keychain record for its device type, bound to the hostname and port the operator typed, or to the connected peer IP and port for a literal address. - **Principle VII — Untrusted Input Is Validated At The Boundary:** codes are checked before being interpolated into commands, incoming line buffers are bounded, and malformed/failed AUTH replies do not make a direct connection available. - **Principles VIII and XI — Evidence Over Assertion / Fixes Are Demonstrated:** the probe output, focused tests, and the pre-change Tune routing record are in `docs/4o3a-remote-auth-hardware-evidence.md`. The report separates observed hardware behavior from test-only and still-unverified behavior; CI and operator confirmation remain gates after submission. ## UI Changes - **Setup → Peripherals, TGXL/PGXL/AG rows:** added separate masked Authorization code fields, Show/Hide buttons, Clear code buttons, endpoint-specific reuse/session-only placeholder text, and screen-reader names and descriptions. A blank field reuses the matching saved code when an AUTH greeting arrives; a new accepted code replaces that device type's saved record. - **Setup status cells:** show connecting, rejected/required code, Keychain deletion failure, and session-only save warnings. They distinguish an unchallenged connection (code not saved) from a code discarded before verification, and clear stale errors after a recovered connection. The AG and ShackSwitch rows display their own connection errors. - **TGXL applet:** added a bottom-right `● DIRECT` / `● RADIO` / `● OFFLINE` indicator in docked and floating layouts. DIRECT uses the theme success color, RADIO the warning color, and OFFLINE the disabled-text color. A blocked direct-connection reason appears in the tooltip and accessible description, and clears on manual retry or successful direct connection. - **PGXL applet:** moved its connection indicator below the controls to the same bottom-right placement and control-to-indicator spacing as TGXL. It uses the same three states, colors, tooltip, and accessibility behavior; RADIO requires a connected radio and PGXL relay handle. - **Antenna Genius applet:** added a bottom-right `● DIRECT` / `● OFFLINE` indicator with success/disabled theme colors and accessible text. It remains OFFLINE before a direct AG connection; the shared ShackSwitch model does not make it display DIRECT. - **Setup layout:** each device keeps address/port/connect on its first line, authorization controls on the next, and wrapped status across the available width. Clear code and errors fit at the default 960×680 size without horizontal scrolling. ShackSwitch Web UI occupies its own line; its authentication errors are named as ShackSwitch in the main status bar. - **Automation-visible UI:** masked the code field's value and its grab output so remote automation does not expose a shown code. **Maintainer rulings:** the central Peripherals tab (rather than per-applet fields), the compact Setup layout, the applet source indicators, and credential binding (configured host+port for names, peer IP+port for literal addresses) are accepted as built. So is a saved manual host taking precedence over the radio-reported endpoint, with the fallback described under **Automatic connection behavior**. ## Test plan - [x] Local build passes (`cmake --build build --parallel 6`) on macOS 27.0/arm64 using the branch's 26.9.4 source snapshot. - [ ] Behavior verified on a real radio if applicable — a prior operator Tune action showed the FlexRadio accepting a relayed TGXL autotune command; a full in-app saved-code restore has not yet been observed. - [ ] Existing tests pass (CI) — rerun required for the latest review-fix commit. - [x] Reproduction steps documented for the pre-change direct-TGXL Tune routing problem in `docs/4o3a-remote-auth-hardware-evidence.md`. Earlier submission verification: **37 passed, 1 skipped** across the peripheral auth dialog, handshake, and Keychain tests; automation redaction tests; TGXL/PGXL applet tests; and AppSettings safety scenarios. `pgxl_panel_test` skipped because this sandbox could not bind loopback; its result is not a pass claim. PR-REVIEW preflight identified that the earlier dialog regression clicked Connect against reserved IPs and opened outbound `QTcpSocket`s. That check now uses an injected callback at the production connect-ordering seam and its focused test passes socket-free. A mutation that kept the old pending flag set during the callback made the revised test fail; restoring the fix made it pass. The modified `pgxl_panel_test` is an existing default-graph loopback `QTcpServer` test; it was not rerun during this review after operator notification. Its bind failure returns 77. `git diff --check`, test registration, the aetherd touchpoint manifest, engine-boundary, capability-record, and command-plane baselines completed with no new blocker; the accessibility scan reports existing warning-only findings. An isolated, TX-disabled offscreen app launched, but its automation bridge did not expose a socket, so no demo GUI claim is based on bridge output. **Live evidence and limits:** the one-command TGXL and PGXL probes received accepted AUTH replies on firmware 1.2.17 and 3.9.1, respectively. A later app build established direct TCP sockets and showed bidirectional traffic, but that socket snapshot did not capture the in-app AUTH exchange. AG hardware AUTH and the full in-app Keychain restore/clear cycle remain unverified. ## Checklist - [x] Commits are signed (`docs/COMMIT-SIGNING.md`) — feature commit `120ed439` has a locally verified SSH signature registered to `w5jwp`. - [x] No new flat-key `AppSettings` calls — existing manual IP/port keys are reused and credentials use endpoint-bound Keychain records (Principle V). - [x] Code is clean-room — vendor/public API documents and observed wire behavior, with no decompiled or disassembled proprietary code (Principle IV). - [x] All meter UI uses `MeterSmoother` — not applicable to this PR; it changes connection labels/layout, with no new meter data or smoothing path. - [x] Documentation updated for user-visible behavior in `docs/4o3a-remote-auth-hardware-evidence.md`; `CHANGELOG.md` remains untouched. - [x] Security-sensitive changes reference a GHSA if applicable — no GHSA applies to this feature request. **Review routing:** source/tests/docs need Tier-3 code-owner review; `CMakeLists.txt` needs Tier-2 infrastructure review. The issue carries `maintainer-review`, and the UI/UX decisions above need the project maintainer's approval. The required issue-claim attempt was rejected by GitHub because `w5jwp` lacks upstream assignment permission; no placeholder assignee was created. ## Current review dispositions - TGXL/PGXL socket-free regression coverage now enables automatic reconnect at a nonempty target and asserts that the named reconnect timers stop after explicit AUTH rejection or three timeouts/handshake disconnects. Late disconnect/error delivery must not re-arm them. - AG requires sequence 1, zero result, and an omitted/empty or exactly `OK` body. Unknown/rejection bodies and extra fields cannot accept a credential. The test writer is private test-access plumbing; the production constructor accepts only its QObject parent. - [The contributor capture on #2313](#2313 (comment)) reports `V<version> AG AUTH\n`, LF from the vendor utility, and CRLF from AetherSDR reaching the auth parser. AUTH now uses CRLF. The report also says the utility silently truncates passwords to eight characters; operators must enter the actual device code. AetherSDR does not silently truncate passwords. This is attributed contributor evidence, not a new successful AG hardware test; the report names no firmware version. - Failed Keychain deletion retains the cache; a completed older deletion cannot erase a newer save. AG retry history ignores empty targets and is bounded to 128 targets without evicting existing blocks. - The earlier six CodeGuard advisories concern a protocol comment and unchanged MainWindow code; they do not identify a new path operation requiring a fix. **Automatic connection behavior:** saved TGXL/PGXL manual host/port settings take precedence over radio-discovered endpoints. Loss of radio-reported accessory presence does not disconnect the direct session while a manual override is configured. This also affects LAN users with a saved override; clearing it restores discovered-address selection. When an automatic connect (discovery, presence, startup, or their reconnects) to the saved address never reaches the device over TCP, the app tries the radio-reported address once, on the default port, as an alternate attempt. Reconnects keep aiming at the saved host, a code saved for the saved host is never sent to the alternate address, and a missing code there fails that try without blocking reconnects. An explicit Setup → Connect, a deliberate disconnect, or an attempt whose socket connected never triggers it. `checkUnreachableAndAlternate` in `peripheral_auth_handshake_test` pins each rule. **Credential identity (`907a88e8`):** a saved code is bound to the name the operator typed. A hostname (typically DDNS) binds the lowercased name and port, so a change of the residential IP behind it no longer asks for the code again, which is the main remote use case of #2313. A literal IP binds the connected peer IP and port, as before. Nothing binds until a socket has a real peer address, and a different name, port, or a literal IP is a different identity, so a code saved for one is never offered to another. The trade is deliberate: whatever the name resolves to receives the saved code, so a code should only be saved for a name the operator controls; the store header and `docs/4o3a-remote-auth-hardware-evidence.md` say so. A code saved earlier for a hostname was stored in the IP form and is not reused, so those operators enter it once more. Tests cover name equality across IPs, case and whitespace, the empty-peer and zero-port refusals, and that another name, port or literal IP does not match. This was raised as a question on the review; if the maintainer prefers peer-IP-only binding, it reverts by dropping this one commit. Central-tab placement, the compact layout, and applet indicator design still await maintainer direction. **Latest local verification (2026-09-29):** the desktop app builds, and all eight focused auth, automation-redaction, TGXL and amp applet tests passed. Mutation checks removed TGXL/PGXL auth-block reconnect guards, reverted AG AUTH to bare CR, and restored the wide Setup table: each failed at its intended regression assertion. All fixes were restored and the eight tests passed again. Test registration, touchpoint manifest, engine-boundary, capability-record, command-plane, colour ratchet, and whitespace checks found no new blocker. No new synthetic device peer, live-hardware, OS-vault, or TX test was introduced or run. The existing loopback `pgxl_panel_test` was not rerun. **Verification of the credential-identity commit `907a88e8` (2026-09-30):** the desktop app builds, and the ten focused tests (peripheral auth dialog, keychain and handshake; automation redaction; TGXL and amp applet) pass. The new handshake-test assertions cover name equality across IPs, case and whitespace, the empty-peer and zero-port refusals, and that another name, port or literal IP does not match. I did not run a mutation check against it, and it has not been tried against a live device or a real DDNS name. Self-assignment was attempted again; GitHub denied it because `w5jwp` lacks upstream assignment permission. Review threads are left for the reviewers to resolve. ## Review follow-up (2026-10-03) - A keychain outage no longer blocks auto-reconnect: `setAuthCodeForAttempt` passes `blockReconnect=false` when the credential store is unavailable (TGXL, PGXL, Antenna Genius). A rejected or missing code still blocks. - `setup` appends ` authcode=` only when `setup read` contained the `authcode` key and it was empty. The firmware 3.9.8 `R4|50000013|` probe result is in `docs/4o3a-remote-auth-hardware-evidence.md`. - The PGXL status debug log omits `authcode`. - The authcode-omission assertion is a socket-free case in `peripheral_auth_handshake_test`, through a command-writer seam on `PgxlConnection`. - Manual-address fallback: see **Automatic connection behavior** above. - Merged `origin/main` (`732b99a9`). --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
**Merge order for the PGXL / 4O3A PRs** (the same map is in each of them; #6030 has merged): ``` Stage 1: #6008 authenticate TGXL, PGXL and Antenna Genius Stage 2 (either order, after what each needs): #6027 Peripherals settings UI needs #6008 #6033 PGXL drain current (ID meter) needs #6008 Stage 3: #6036 PGXL docked layout needs #6008 and #6033 ``` **How to review:** this PR's own commits are [`ec0d432a`](ec0d432) and [`590a798e`](590a798). `d473b183` merges #6008 in (one conflict in `AmpApplet.cpp`, resolved as described under "Merging with #6008") and `b41ed522` merges #6030 in (no change to the tree; this branch used to carry #6030's last commit as a cherry-pick, which conflicted with the original). `92fd1aa5` merges #6008 in again for its hostname-bound credential (`907a88e8`). Everything else is carried from #6008 and #6030, which are reviewed in their own PRs. ## Summary Fixes #6032. **Stacked on #6008 and #6030, not a replacement for either.** It merges both in until they merge; this PR will then be rebased onto `main` and show only its own. When PGXL status is relayed through a FlexRadio, the Id gauge did not move. The radio sends drain current as its `ID` meter, which AetherSDR never decoded, and the relay path instead waited for amplifier-status keys the radio never sends. This PR reads each PGXL reading from where the radio actually sends it, and picks between the radio and the PGXL by measured refresh rate. **Verified on a FLEX-8600 (SmartSDR 4.2.20.41343) and a PowerGeniusXL running firmware 3.9.8.** The captures, rates and method are in `docs/pgxl-telemetry-source-evidence.md`. **The rule for each reading** - Available only from the radio: use the radio (drive). - Available only from the PGXL: use the PGXL (Harmonic Load heatsink temperature, Vdd, Vac, fan mode, MEffA). - Available from both: use the faster source. The radio is faster for forward power and SWR. Drain current and PA heatsink temperature are a tie, and a tie goes to the radio. In every case the radio's value is used while fresh (1500 ms), and the PGXL's value otherwise. **Changes** - `MeterModel` decodes the PGXL's `ID` meter. `RadioModel` passes the amplifier's handle to `MeterModel::setAmpHandle`; once it is known, an AMP meter (FWD, RL, DRV, TEMP, ID) reaches the PGXL only when its source index matches, as FlexLib `Radio.FindMetersByAmplifier` does, so neither a tuner's nor a second amplifier's meters land on it. Before the handle is known, any AMP meter that is not the tuner's goes to the PGXL, and a handle change re-routes the meters and withdraws Id and PA temperature until the next sample. A new `ampVitalsChanged` signal carries drain current and PA heatsink temperature, each with a valid flag. - `AmpApplet` gains `setRadioDrainCurrent` and `setRadioPaHeatsinkTemp`. The radio's values win while fresh, the same rule forward power and SWR already use. - Removed the radio amplifier-status handler in `MainWindow_Wiring`. None of the keys it read (`temp`, `id`, `vdd`, `vac`, `meffa`) arrive. Also removed `AmpApplet::setMeff`, which only that handler called, and its test. - Corrected the `applyMeters` comment: it said the PGXL is polled at 5 Hz, and now cites the measured rates. - Added `tools/probe_pgxl_telemetry_sources.py`, the read-only probe that produced the evidence, and `docs/pgxl-telemetry-source-evidence.md`. **Merging with #6008:** `src/gui/AmpApplet.cpp` conflicts in one place. #6008 adds `setRadioConnected`, `updateSourceIndicator`, `setDirectFailureReason` and `hasRadioRelay` just before `setMeff`, which this PR removes. Keep #6008's four functions and drop `setMeff`. Tested that way (see below). ## Constitution principle honored Principle VIII (Evidence Over Assertion). The relay handler rested on an untested claim about what the radio sends (#2444). Every choice here is taken from captures of the radio and the PGXL, and the evidence page records how to repeat them. ## Test plan - [x] Local build passes - [x] Behavior verified on a real radio: FLEX-8600 (SmartSDR 4.2.20.41343) with a PowerGeniusXL on firmware 3.9.8 - When PGXL status is relayed through a FlexRadio: Id read 19 A during a TUNE and dropped after unkey; PA temperature updated; HL, Vdd and Vac blank. - Direct connection (built together with #6008, which this PGXL firmware needs for authentication): Id 19 A, Vdd 52.0 V, Vac 117 V, both heatsink temperatures; no flicker between sources. - [x] Tests pass: `amp_applet_test`, `meter_model_test`, `health_applet_test`, `pgxl_panel_test`, `amp_model_test` - [x] New tests: `testAmplifierVitalsAreRouted` and `testTunerDrainCurrentIsNotRoutedToTheAmplifier` (`MeterModel`), `testRadioVitalsWinOverTheDeviceWhileFresh` (`AmpApplet`) - [x] Reproduction steps documented in the linked issue ## Checklist - [x] Commits are signed (`docs/COMMIT-SIGNING.md`) - [x] No new flat-key `AppSettings` calls - [x] Protocol behavior comes from captures of the radio and the PGXL; the meter-to-amplifier match follows FlexLib `Radio.FindMetersByAmplifier` (Principle I) - [x] All meter UI uses `MeterSmoother` (no meter UI changed) - [x] Documentation updated: `docs/pgxl-telemetry-source-evidence.md` - [x] Security-sensitive changes reference a GHSA if applicable (not applicable; the probe reads the authorization code without echoing it and never records it) 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Review follow-up (2026-10-03) - `ampVitalsChanged` carries a per-reading "updated in this packet" flag, so an `ID` packet no longer restamps `TEMP`'s freshness and the reverse. A new `meter_model_test` case sends only `ID` and checks `TEMP` is not marked updated. - `testAmpAndTunerMetersSplitByHandleAfterALateHandle` now includes a tuner `ID`/`TEMP`, pinning the rescan after a late handle. - AMP meters match the amplifier's handle (maintainer ruling on N3). `testOnlyTheAmplifiersOwnMetersReachIt` and `radiomodel_amp_meter_routing_test` cover it. - Merged #6008's branch. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
#6036) **Merge order: #6008 and #6033 must be merged before this PR.** This branch is built on #6008 and merges in #6033 (with the conflicts resolved), so it carries their commits until they merge; it will then be rebased onto `main`. #6030 has merged. **Merge order for the PGXL / 4O3A PRs** (the same map is in each of them; #6030 has merged): ``` Stage 1: #6008 authenticate TGXL, PGXL and Antenna Genius Stage 2 (either order, after what each needs): #6027 Peripherals settings UI needs #6008 #6033 PGXL drain current (ID meter) needs #6008 Stage 3: #6036 PGXL docked layout needs #6008 and #6033 ``` **How to review:** this PR's own work is [`b4dcd8c9`](b4dcd8c) (the layout change) and [`4cc897c2`](4cc897c2) (the review findings: a neutral OPERATE placeholder with an accessible name, the temperature column measured in the drawn font, and two test fixes). `1b0de46e` merges #6030 in, and `3827ffea`, `4ef42eae` and `2186ef8e` merge #6033 in (the last also brings #6008's hostname-bound credential); open them only to check the resolutions. Everything else in the diff is carried from #6008, #6030 and #6033, which are reviewed in their own PRs. ## Summary Fixes #5903. Fixes #4885. #6008 puts the connection indicator on its own line and provides the direct connection that shows all of the readings and controls. In the docked (rail) view, the PGXL panel clipped its controls and readings: the 260 px rail had one row holding a stacked column of readings plus the MEffA, Fan and OPERATE controls. This PR gives the controls and the readings their own lines, and keeps the panel's shape whether the amplifier is offline or connected. Docked, the panel now reads: ``` [ MEffA ] [ Fan: Standard ] [ OPERATE ] PA 89.9 F Vac 122 V HL 89.2 F Vdd 51.9 V ● DIRECT ``` **Changes** - **Controls.** MEffA, Fan and OPERATE have the row to themselves and share its width, at one height (22 px). MEffA keeps its three state colours but takes OPERATE's 10 px bold lettering. - **Readings in a 2×2 grid**, in both presentations: PA over HL, and Vac over Vdd in a second column 10 px to the right. Docked, the connection indicator ends the HL/Vdd row; expanded, it keeps its own line below. Each is its own readout (`label value unit`) with its own screen-reader name. - **Vac and Vdd never move.** A grid column is as wide as its widest cell, so they start at one x; and the temperature column is held at the width of its widest possible reading, measured in the face it is drawn in, so they also stay put as the temperatures change. Fixed-length text alone was not enough: where no true fixed-width face is installed the fallback is proportional, and `9.9` and `106.8` differ in width. - **Temperature units.** PA and HL are both clickable, and a click on either switches both between Celsius and Fahrenheit and saves the choice, as the single readout did. HL shows `HL —` in grey until the direct connection sends a value, rather than disappearing, so the rows never move. The hover outline is gone (the pointing-hand cursor already says it can be clicked); the keyboard-focus outline stays. - **The connection indicator shares Vdd's line exactly.** Docked, it takes Vdd's height, so both centre the same way. At one pixel shorter, centring it needed a half-pixel offset that widget geometry rounds away, which lifted its text above Vdd's. - **Padding.** Every readout keeps a fixed length, but the spare spaces go at the end instead of in front of the number (`HL 89.8`, not `HL 89.8`). The temperature readouts have no left padding, so they line up with the controls above them. - **Spacing.** Docked, an 8 px pad separates the controls from the Id gauge, and the readings sit with equal space above and below. - **Fan labels** are spelled out: `Fan: Standard`, `Fan: Contest`, `Fan: Broadcast`. The pull-down reserves room for the longest. - **OPERATE's colours come from the theme.** It used a hardcoded green (`#6030`) under the theme's primary text colour, which is near-black under Default Light and unreadable there (#5903, item 4). It now uses the lit MEffA key's tokens, so the dark theme looks as before and the light theme gets its own green with white text. - **Before the amplifier reports.** The docked controls stay in place instead of hiding: MEffA is grey and disabled, OPERATE reads `—` and is grey and disabled (with the accessible name "Amplifier state not reported"), and Fan shows `Fan: —`, disabled. None of them claims a state. Clearing the fan selection is done with signals blocked, so it never sends a fan-mode command. PA and HL are grey until a reading arrives, like Vac and Vdd. The expanded panel's keys still wait for a state before appearing. **Maintainer rulings:** accepted as built. That covers the expanded (floating) panel's 2×2 grid, the removed hover outline, the right-justified readouts, and showing the controls disabled before the amplifier reports instead of hiding them. On a station without a direct PGXL connection, MEffA and fan stay dimmed, and their descriptions say they need a direct PGXL connection. **Against #5903's triage:** items 1 (controls on their own line), 2 (the rail MEffA button at 10 px) and 4 (OPERATE contrast) are done as proposed. Item 3 (the fan pull-down too wide when docked) is solved the other way: the controls share the full row, so the full labels fit, and they are spelled out rather than shortened to Std/Bcast. The docked-width test it asks for is added (below). **#4885** was left open until the docked `Fan: Contest` label could be checked on a current build: it reads in full here, and the new test checks all three fan labels at the rail's width. **Merging with #6030, #6033 and #5984:** #6030 is merged into this branch (commit 1b0de46), because the two conflicted in 10 places across `AmpApplet.cpp`, `AmpApplet.h` and `tests/amp_applet_test.cpp`: #6030 renames the temperature setters and members (`setTemp` → `setPaHeatsinkTemp`, `m_hasTempA` → `m_hasPaHeatsinkTemp`, …) and changes `updateTempLabel` and `setDirectConnected`, all code this PR restructures into two readouts. The resolution keeps this PR's structure with #6030's names and behaviour: HL clears (and turns grey) when the direct connection drops, a late HL write is ignored, and each readout's tooltip names only its own label. #6030's fallback test is rewritten for the two readouts and now also checks the grey tone. #6033 is merged in too (`3827ffea`, `4ef42eae`, `2186ef8e`). It deletes the amplifier-status relay handler and `AmpApplet::setMeff`, and adds `setRadioPaHeatsinkTemp` and `setRadioDrainCurrent` beside the PA setter this PR restructures, so it conflicted here in three files. The resolution keeps this PR's two-readout layout and tooltips, takes #6033's deletions and evidence comment, and keeps the grey-to-live style change on the PGXL's own first PA reading. Its single-button tooltip assertions are dropped in favour of this PR's per-readout ones. #5984 (spelling out MEffA on the expanded panel's key) edits panel-key code this PR leaves alone. ## Constitution principle honored Principle XI (Fixes Are Demonstrated). `pgxl_panel_test` lays the panel out at the rail's real width (260 px) and checks that no control or readout is narrower than its text needs, and `testDockedCaptionsFitTheRail` repeats that for every caption the controls can show. Both fail against the old layout. The layout was also checked in the running app on the hardware below, offline and connected. ## Test plan - [x] Local build passes - [x] Behavior verified on a real radio: FLEX-8600 (SmartSDR 4.2.20.41343) with a PowerGeniusXL on firmware 3.9.8. Docked, offline and connected with the direct connection up; `Fan: Broadcast`, the longest label, fits beside MEffA and OPERATE. - [x] Tests pass: `pgxl_panel_test`, `amp_applet_test`, `pgxl_direct_protocol_test`, `amp_model_test`, `peripheral_auth_handshake_test`, `health_applet_test` - [x] Test changes: - `pgxl_panel_test`: in both presentations, PA sits over HL and Vac over Vdd, with Vac and Vdd at one x. Docked at 260 px, the indicator ends the HL/Vdd row at the right edge, at Vdd's height and top edge; nothing is narrower than its text; the controls share one height; the space above and below the readings matches; the controls clear the Id gauge. Expanded keeps the indicator on its own line below, and the layout survives docked → expanded → docked. - `amp_applet_test`: new `testDockedCaptionsFitTheRail` shows every caption the rail's controls can take (each operate state word, all three fan labels, all three MEffA states, and the before-report placeholders) at 260 and 300 px, and checks each control is at least as wide as its caption needs. It compares against the control's own size hint, so a CI host that falls back to other fonts cannot fail it on a caption that fits. Against the old layout it fails at both widths: the fan pull-down got 70 px of the 130 its label needs. New `testVoltagesStayAlignedWithTheTemperatures` runs PA and HL through 9.9, 106.8, 100.4, 120.0 and -5.0 and checks that Vac and Vdd stay at one x and never move; without the pinned column width they moved by up to 7 px. `testDualSensorToggle` checks that a click on either temperature switches both and that the unit chosen from HL is saved. Also the new readout text; fixed length across digit changes; the before-state controls are shown but disabled with no mode; clearing the fan selection sends no command; the PA placeholder tone. - [x] Reproduction steps documented in the linked issues ## Checklist - [x] Commits are signed (`docs/COMMIT-SIGNING.md`) - [x] No new flat-key `AppSettings` calls - [x] Code is clean-room - [x] All meter UI uses `MeterSmoother` (no meter changed) - [x] Documentation updated if user-visible behavior changed (no `docs/` page describes this panel) - [x] Security-sensitive changes reference a GHSA if applicable (not applicable) 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Review follow-up (2026-10-03) - The column-pin test forces a proportional face on the temperature buttons, drives 9.9 → 106.8, and asserts the column minimum covers the widest reading plus the border. With the `setColumnMinimumWidth(0, ...)` line removed it fails. - **Justification (see #5983):** only the numeric field is right-justified (`label + rightJustified(value, 5) + unit`), so neither the unit nor the number's right edge moves as the value changes. - When there is no direct connection, the MEffA and fan descriptions say they need a direct PGXL connection. - OPERATE gets an accessible description while disabled; Vac/Vdd say "not reported" before the first reading; each readout's name change is announced once (`setAccessibleName` posts `NameChanged` itself); with a direct connection and no fan mode yet, the fan pull-down says the amplifier has not reported one; `placeReadings()` skips re-flow when nothing changed. - Comments describe current behaviour; the caption check uses the combo's edit-field rect; placeholder-tone checks compare against the resolved disabled colour. - **Known:** `pgxl_panel_test` fails at `:553-554` (`source->height() == vdd->height()`) in my sandbox, where the theme resources fail to load. It fails identically at the previous head `f5eafa06`, so it predates this change. It passes for you per your review. - Merged #6033's branch. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
…6008. Principle III. Also stops every route to a socket while a removal is pending: the guard now sits in openSocket(), so automatic, alternate and reconnect-timer attempts are covered, not only the explicit Connect. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…er dismisses. Principle VI. Remove asks for confirmation, then clears the row's configuration and resets its toggle. Stopping auto-connect is a separate, visible per-device toggle for TGXL, PGXL, AG and ShackSwitch that blocks startup, presence/discovery, alternate and reconnect attempts; an explicit Connect always works. The DiscoveryDismissed key is gone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s; isolate toggle keys in tests. Principle VIII. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…riven refresh. Principle XI. Moves the Peripherals tab into RadioSetupDialog_Peripherals.cpp. Each kind of device (TGXL/PGXL/AG/ShackSwitch, ACOM/SPE/LP-100A, VK3AMP) has its own builder that lays out its own detail page; the hidden grid, reparenting, itemAtPosition and the row-number switches are gone. Status is carried explicitly (PeripheralDeviceStatus) and the list reads DIRECT / RADIO / OFFLINE through the helper the applets now share. The detail status label is always visible and is the accessible state. Nothing searches text for "Error:" or a dash. The 250 ms presentation timer is replaced by the connection and model signals that change what is shown. A disabled Add entry says "(already added)". Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ture as it is. Principle XI. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…early returns; trim docs. Principle XI. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ple XI. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ten9876
left a comment
There was a problem hiding this comment.
Issue fit
Partially. #6026 is approved with conditions (ruling). The list/detail direction, the 15 s bound and lease, the excluded flat-key migration and AG/ShackSwitch isolation all stand.
At 74bf2e89 the branch doesn't meet the ruling's conditions, and it can't merge as is:
- #6008 has landed on
main(4d5b9893), with a fallback rework that this branch hasn't seen. git merge-tree origin/main 74bf2e89conflicts in about 15 files.- CI ran at 15:25Z, before #6008 merged, so the green describes a merge that no longer exists.
Reviewed as the increment over #6008's old head: git diff 22b1496f...74bf2e89, 24 files.
Scope
| File / group | What it changes | Claimed? | Verdict |
|---|---|---|---|
src/gui/RadioSetupDialog.{h,cpp} (+1123/−157) |
device list, detail stack, Add/Remove, bounded close, recovery rows | yes | in scope; layout does not meet ruling item 1 |
src/core/PeripheralSettings.h |
VisibleDevices, DiscoveryDismissed in the Peripherals document |
yes | in scope; DiscoveryDismissed is superseded by ruling item 4 |
src/core/PeripheralRemovalGuard.h, {Tgxl,Pgxl}Connection.*, AntennaGeniusModel.* |
removal lease checked at connect, m_connectTransport test seam |
yes | in scope; see blocker 2 |
src/gui/PeripheralAuthStore.*, PeripheralAuthConnectFlow.h, MainWindow{,_Wiring}.cpp |
configuredEndpoint, endpoint-scoped clear, deletion completion, discovery wiring |
yes | in scope |
docs/architecture/aetherd-touchpoint* |
+PeripheralRemovalGuard.h (2 GUI includers), AppSettings.h 105→106 via PeripheralAuthConnectFlow.h |
yes (numbers stale: body says 231→232, now 235→236) | in scope; additions only, no retag |
docs/peripherals-settings-ui.md, docs/peripherals-removal-evidence.md |
user doc + evidence | yes | in scope; see nit N3 |
tests/peripheral_auth_{dialog,keychain}_test.cpp, tests/fakes/PeripheralAuthStoreFake.* |
1,690 test lines | yes | in scope |
Nothing in the diff is unexplained. CHANGELOG.md is untouched.
Socket preflight: the new dialog cases call connectToTgxl("192.0.2.9x"), connectToPgxl and connectToDevice, but every one goes through the injected m_connectTransport (installed via PeripheralConnectionTestAccess::injectConnect). No socket opens, and the tests are socket-free as the body says.
Blockers
1. Replay onto main, and keep the removal lease on every connect path.
On main, TgxlConnection::connectToTgxl is one of three thin wrappers. autoConnectToTgxl and tryAlternateTgxl call openSocket() without going through it (same in PgxlConnection). This branch puts the PeripheralRemovalGuard::pending() check and the m_connectTransport seam only inside connectToTgxl (src/core/TgxlConnection.cpp:85-87, src/core/PgxlConnection.cpp:84-86). Kept there through the merge, the lease no longer covers:
- the presence and startup handlers, which call
autoConnectTo*onmain; - the reconnect timer when its target came from an automatic attempt;
- the queued
unreachable→tryAlternate*fallback. Anunreachablequeued before the Remove click is delivered after it, and reconnects while the keychain delete is still running.
checkPendingRemoval drives only connectToTgxl, so it stays green while those three paths reconnect.
Fix: check the guard before begin*AttemptAt in all three entry points, or in one shared pre-check, and move m_connectTransport into openSocket(). Then add a case per entry point. Also, src/gui/RadioSetupDialog.cpp:11107 fills the recovery row from lastHost()/lastPort(). On main, an alternate attempt deliberately leaves the reconnect target (reconnectHost()) on the manual host while auth was refused at the alternate address, so the "authorization blocked" row would show the wrong endpoint. Use attemptHost(), and drop lastHost() in favour of main's reconnectHost().
2. Ruling item 1: the detail pages are the old grid, reparented.
buildPeripheralsTab still builds the nine-row grid and moves its cells into detail pages (src/gui/RadioSetupDialog.cpp:10353), with six itemAtPosition lookups left in place. The ruling asks for a per-device builder. The list also doesn't use the accepted wording. Driven on the demo backend after Add:
→ cell peripheralDeviceList 0 0
← "text": "Tuner Genius XL\nAddress not set\nOffline"
Use DIRECT / RADIO / OFFLINE, as the applets do.
3. Ruling item 2: Add writes connection intent.
src/gui/RadioSetupDialog.cpp:10733 calls setDiscoveryDismissed(device.id, false) on Add. Driven on the demo backend: an Add with nothing else done persisted
app_settings Peripherals = {"VisibleDevices":["tgxl"],"tgxl":{"DiscoveryDismissed":false}}
Add should create and select the row and write nothing about intent. Only Connect restores it.
4. Ruling item 3: Remove has no confirmation and no outcome table.
Driven on the demo backend: I selected the TGXL row and clicked peripheralRemoveButton. The device went immediately, with no confirmation window, and the list read rows: 0. The destructive credential delete (src/gui/RadioSetupDialog.cpp:10889) needs a confirmation first. The body and docs/peripherals-settings-ui.md also need a table giving the configuration, credential, reconnect and recovery result for success, failure, unconfirmed/timeout and no backend.
5. Ruling item 4: replace DiscoveryDismissed with a per-device "Stop auto-connecting" toggle.
src/core/PeripheralSettings.h keys dismissal by device type only ("tgxl"/"pgxl"), so removing a TGXL on one radio suppresses TGXLs on every radio, and an AG is never dismissed. Remove should no longer imply dismissal. Drop the key in favour of a visible per-device setting.
6. Ruling item 7: drive the presentation from model signals, not the 250 ms timer.
presentationTimer->start(250) (src/gui/RadioSetupDialog.cpp:11268) re-derives the list, re-sets accessible descriptions and writes the Peripherals document from the timer. Driving it from model signals also removes the visibility check that searches the label text for tr("Error:") or an em dash (:11181-11182).
7. A completed AG Remove deletes an address saved meanwhile from the AG applet.
src/gui/RadioSetupDialog.cpp:10795-10800 removes AG_ManualIp/AG_ManualPort without checking them. Sequence:
- Remove AG while the keychain delete is pending (for example, a macOS prompt is open).
- The AG applet stays usable, so
AntennaGeniusApplet::tryManualConnectwritesAG_ManualIp=B(src/gui/AntennaGeniusApplet.cpp:272). The guard silently suppresses its connect. - The delete finishes inside the 15 s window, and
finishRemovalerases B.
That breaks the body's claim that stale Setup values never overwrite newer settings. Compare-and-clear against the values captured at the Remove click. Reasoned from code; the demo backend has no AG.
Should fix
- S1. An unconfigured AG or ShackSwitch row can't be removed while the other device's code is saved. In the Remove handler (
src/gui/RadioSetupDialog.cpp:10933-10947) andsrc/gui/PeripheralAuthStore.cpp:415-421, the empty-endpoint check returnsUnknownOwnerbefore the different-owner check returnsCleared. Add an AG with no address while the ShackSwitch's code is in the shared slot, then Remove the AG: the notice tells the operator to connect a device that has no address, and connecting would tear down the live ShackSwitch.checkSharedCredentialRemovalalways sets the IP key, so nothing covers it. Treat "no resolvable endpoint, slot bound to the other device" asCleared. Reasoned from code. - S2. The
src/core/PeripheralSettings.h:20-21header note still says TGXL/PGXL/AG/ShackSwitch "still use their own flat keys, not this class". That's no longer true now thatVisibleDeviceslives in the document (ruling item 5).
Nits (non-blocking)
- N1. Disabled Remove looks enabled. With an empty list,
peripheralRemoveButtonreportsenabled: false, but it draws exactly like the enabled Add button (grabbed on the demo backend). Give its disabled state the disabled-text token. - N2. a11y.
- The detail page hides its status label except for errors, so a screen-reader user on the detail form has to go back to the list to hear state.
- Disabled Add-menu entries give their reason only through
setStatusTip(:10750,:10774), and a dialog has no status bar. - (
docs/a11y.md, #4896.)
- N3. Docs.
docs/peripherals-removal-evidence.mdnarrates PR history ("PR #6008 reflowed…"). Move that to the PR body and keep the docs to the current behaviour. - N4. Body. It still says "UI approval is pending", has the manifest at 231→232 and calls the PR draft-dependent. Update it after the replay.
- N5. After a timeout,
contentstays disabled and the late completion returns early, so the page stays dead until Setup is reopened. That's acceptable given the notice, but the user doc should say so.
What I verified vs. read
-
Built
74bf2e89on Linux.peripheral_auth_dialog_test,peripheral_auth_keychain_testandperipheral_auth_handshake_testpass. -
Mutations: each of these fails
peripheral_auth_dialog_test, then restored green:- removing the TGXL removal guard (
TgxlConnection.cpp:85-87); - removing Add's
setDiscoveryDismissed(false); - removing Remove's
setDiscoveryDismissed(true).
The tests pin the behaviour as built. Ruling items 2 and 4 change that behaviour, so those assertions change with it.
- removing the TGXL removal guard (
-
Gates: engine-boundary, manifest, capability-records and command-plane are flat against #6008's head. The manifest change is the additions above, with no retag.
-
Bridge: offscreen, isolated
AETHER_SETTINGS_DIR, attached toDEMO-0001only (get radio→ "AetherSDR Demo"; the relaunch connected to nothing).- Radio Setup → Peripherals → Add "Tuner Genius XL" → the row read "Offline", and
{"VisibleDevices":["tgxl"],"tgxl":{"DiscoveryDismissed":false}}was persisted. - The row survived a relaunch (
rows: 1). - Remove went through with no confirmation (
rows: 0). - The instance was closed.
- Radio Setup → Peripherals → Add "Tuner Genius XL" → the row read "Offline", and
-
Attacks that held (read):
- Removal lease lifecycle: the guard is RAII and owned by one
shared_ptr. No path double-decrements it, and QtKeychain'sfinishedfires on the main thread. - Bounded close:
done()andcloseEvent()both check the flag, and a late completion checks aQPointer. - Endpoint isolation:
clearForEndpointkeeps the other endpoint's record;peripheral_auth_keychain_testcovers the production path. - Credential exposure:
<hidden>holds even after Show, and nothing new is written toAppSettingsor logged.
- Removal lease lifecycle: the guard is RAII and owned by one
-
Not verified: a live TGXL/PGXL/AG, a real OS vault, and the AG/ShackSwitch paths at runtime (the demo backend has neither).
…an unaddressed shared row; show the refusing endpoint. Principle XI. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
74bf2e8 to
37baf0e
Compare
|
@ten9876 #6027 is replayed onto Not verified: the live app through the bridge on this head, a real OS vault, device hardware, native accessibility. 🤖 Generated with Claude Code |
|
@ten9876 separate from this PR, found while replaying it: after the 🤖 Generated with Claude Code |
…d toggle. Principle XI. - Remove treats an empty saved address as no newer target, so an AG Remove finishes when a ShackSwitch beacon clears AG_ManualIp meanwhile. - An unaddressed AG or ShackSwitch row checks the other device's live session for the shared code, so it can be removed beside a discovered device. - TGXL and PGXL label the toggle "Connect directly when available"; off uses the radio's relay. Antenna Genius and ShackSwitch keep "Connect automatically". Help, the confirmation and the user doc follow, and the confirmation names only what the device has. - Tgxl/PgxlConnection and AntennaGeniusModel emit attemptStarted() from every path that opens a socket; Setup refreshes on it, so automatic attempts show Connecting…. - The authorization-blocked recovery row shows the endpoint that refused the code unless the operator has edited the field. - Drops the DiscoveryDismissed clean-up (the key never reached main), new banner dividers and the VK3AMP history note; close savers are cleared with the devices they point into. New and extended cases in peripheral_auth_dialog_test and peripheral_auth_handshake_test fail with each change reverted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Since #6008 merged (`4d5b9893`), a TGXL reached by IP only refuses OPERATE / STANDBY / BYPASS with no notice. `MainWindow` no longer connects `TunerModel::relayedCommandRefused` to `showUnsupportedControlNotice()`, the hookup #6044 added. **Cause.** My fallback rework on #6008 (`071ddd0b`) replaced the wiring block between two comment markers in `MainWindow_Wiring.cpp`, and #6044's connect sat inside that block. The #6027 author found it while replaying their branch. ## Change - `src/gui/MainWindow_Wiring.cpp`: the #6044 connect is restored verbatim, right after the fallback block. - `tests/tgxl_refused_notice_wiring_test.cpp` (new, registered in `tests/tests.cmake`): a source pin that the connection exists and raises the notice. `MainWindow` can't be constructed in a test, so this is the one claim no behavioural seam reaches. TunerModel's emission is already covered by `tuner_model_test`. ## Proof - The pin fails on `main`'s current `MainWindow_Wiring.cpp` ("MainWindow connects TunerModel::relayedCommandRefused", "the refusal raises the unsupported-control notice") and passes with this change. - `AetherSDR` builds on Linux. ## Constitution principle honored - [x] Principle XI (Fixes Are Demonstrated): the regression is reproduced by the new test and shown fixed. - [x] Commits are signed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ten9876
left a comment
There was a problem hiding this comment.
Approving at 6ed6f868.
Ruling on #6026, all conditions met:
- per-device-kind builders, with no grid and no reparenting;
- DIRECT / RADIO / OFFLINE wording;
- Add only creates the row;
- Remove confirms first and is covered by the outcome table;
- a per-device toggle replaces
DiscoveryDismissed; - signal-driven refresh, with no 250 ms timer;
- the header note and the a11y items.
Fixed in this pass (da7df5c7), each with a test that fails when the fix is reverted:
- An AG Remove finishes when a ShackSwitch beacon clears
AG_ManualIp(checkRemovalIgnoresClearedAddress). - An unaddressed shared row checks the other device's live session (
checkRemovalBesideLiveSharedDevice). attemptStarted()from every socket-opening path, so automatic attempts show Connecting… (checkRemovalGuardStopsEveryConnectPath).- The recovery row shows the endpoint that refused the code (
checkRecoveryShowsRefusingEndpoint). - Toggle wording per the maintainer's ruling; dead
DiscoveryDismissedcode, banners and a history note removed; close savers cleared with their devices.
Maintainer rulings:
- TGXL/PGXL Connect directly when available (off uses the radio's relay), AG/ShackSwitch Connect automatically;
- app-global per device type;
- a live session continues until it drops, and explicit Connect still works;
- the status wording, applet hiding on Remove, Remove resetting the toggle, and the extraction behaviour are accepted;
- the includer growth from the file split is accepted.
Verified empirically:
- Linux build of the head merged with
main. - The 9 focused targets pass.
- Mutations as listed, plus the earlier guard, Add and Remove mutations.
- The four static gates match
main, andcheck_a11yreports 0 findings. - On the demo backend: Add persists across a restart; the toggle wording is right; with the AG toggle off, a discovered Antenna Genius stayed OFFLINE through repeated beacons; Remove confirms, and Cancel keeps the row.
Not verified: a real OS vault, TGXL/PGXL/AG/ShackSwitch hardware sessions, and native accessibility on Windows/macOS. pgxl_panel_test (loopback) was not run on this PR.
main gained aethersdr#6138, aethersdr#6115 and aethersdr#6027 since this branch was cut. No textual conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aethersdr#6027 moved RadioSetupDialog::buildPeripheralsTab() into RadioSetupDialog_Peripherals.cpp and added that file to every test that compiles the dialog. radio_setup_show_tx_waterfall_test is new on this branch, so the merge with main left it without the file and it failed to link: undefined RadioSetupDialog::buildPeripheralsTab(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained aethersdr#6138, aethersdr#6115 and aethersdr#6027 since this branch was cut. No textual conflict; the touchpoint manifest is regenerated in the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lay, Qt 6.12 (#6159) ## Summary Six-file release prep for **v26.10.1**, metadata and docs only. No source, test or CI changes. Cutoff: `origin/main` at `18c5245e5` (#6155, merged 2026-10-04). Anything that merges after it is folded in before the tag. **95 merged changes** (95 PRs, no direct commits) from **12 human contributors**, plus AetherClaude (2) and Dependabot (0). There are no first-time contributors this cycle. That was checked for each login by looking for a merged PR before v26.9.5's tag, not from `author_association`. ## Changelog Built from `v26.9.5..18c5245` mapped through the commit-to-PR API (`repos/…/commits/<sha>/pulls`), not from commit subjects. All 95 PRs are cited in the section. The notes lead with three things: - the sweep that makes every control on a radio without a Flex command plane either work or refuse with a reason (#6044, #6049, #6015, #6111); - the CTR2 relay (#6090, #6114, #6155) and authenticated 4O3A connections with a Peripherals page (#6008, #6027); - the Hermes-Lite 2's squelch, APF, AGC-off, RIT/XIT and FFT AVG. The structural half covers Qt 6.12 for every binary and source build (#5842, #6055) and the slice receive controls moving behind the seam (#5892, #5907, #5919; raw Flex commands above the seam 413 → 369, confirmed with `check_command_plane.py` at the cutoff). Upgrade-visible changes are called out with **On upgrading** inline: the Ulanzi default and removed MOX/TUNE pills (#5964), auto RF gain arming and law (#5976, #6062), DIGU/DIGL opening with AGC off (#6119), HL2 squelch calibration and the Auto SQL fallback (#6127), and the macOS 14.4 floor (#5842). The previous tag v26.9.5 is an ancestor of `main`, so nothing is excluded from the range. ## Docs refresh, not just version strings - **README:** Highlights "Broad hardware control" gains the CTR2 relay. Supported Hardware says direct TGXL/PGXL/Antenna Genius connections authenticate. The Other radio families paragraphs gain the HL2's squelch, APF, RIT/XIT, FFT averaging and CL1 reference, the ANAN-G2's receiver audio and radio speaker, and RTL-SDR's confirmed receive state, capture browsing and zoom FFT. In the Roadmap section, the aetherd line gains the receive controls and a CTR2 relay line is added. No maturity label changed. The six backends and seven NR engines were re-checked against `src/core/backends/` and `AetherRxProfiles.cpp`. - **ROADMAP, In flight:** - aetherd: the receive controls behind the seam. - Icom: TUNE power live, no false drop notices, the one-panadapter correction. The stale "VOX and CW break-in" leaves Remaining: `IcomCivBackend` has implemented both since #4799/#5113. - ANAN-G2: receiver audio and radio speaker. - RTL-SDR: M1, with Remaining restated from #5924's body. - Hermes-Lite 2: the v26.10.1 control parity, and the CW latency cost updated to 4096 taps. - A new **CTR2 controller relay** entry. - Nothing left In flight as finished. - **ROADMAP, Open RFCs:** diffed against `label:rfc`. #5893 leaves (closed). #6106 and #6079 join under Radios, protocol and devices. - **ROADMAP, Recently shipped:** 8 new `(v26.10.1)` entries. The list is trimmed to the v26.10.x cycle at the month rollover, and its intro names v26.10.x. Everything trimmed is still in CHANGELOG. ## Judgment calls flagged for the maintainer - **Recently shipped trimmed to v26.10.x at the rollover:** maintainer's call. This makes the validator's "only the four current-version lines lose `26.9.5`" check FAIL. The lines it lists are exactly the trimmed `(v26.9.5)` entry markers, and there is no historical mention elsewhere. That is the one FAIL below. One side effect: the trim removes ROADMAP's last mentions of #5630 and #5684, approved RFCs that shipped in v26.9.4. Both are now closed as completed, and both remain in CHANGELOG. - **A CTR2 relay In-flight entry:** maintainer's call. Wi-Fi works today. USB's host side shipped and has carried the radio's status stream to the firmware author's development firmware; it waits on a CTR2 USB firmware release. - **Intro wording with zero Dependabot updates:** "AetherClaude and no dependency updates within that total" keeps the house shape and passes the count check. The Contributors block has no Dependabot sentence. - **#6125 is filed under the seam section.** It fixes a WAN/SmartLink receive regression introduced by #5907 inside this range, so released builds never had the regression. The section states what ships: receive intents gate on `RadioModel::isConnected()`. - Ready for review, not draft: maintainer's call. ## Scope | file | change | |---|---| | `CMakeLists.txt` | `project(AetherSDR VERSION 26.10.1)` | | `README.md` | Current version line, plus the prose refresh above | | `AGENTS.md` | Current version line | | `CHANGELOG.md` | new `[v26.10.1] — 2026-10-04` section; `[Unreleased]` stays and stays empty; history below byte-identical | | `packaging/linux/io.github.aethersdr.aethersdr.metainfo.xml` | `<release version="26.10.1" date="2026-10-04"/>` first in `<releases>` | | `ROADMAP.md` | cycle heading → post-v26.10.1, plus the prose refresh above | ## Validation `check_release_prep.py --mapping mapping.json`: 29 checks, **1 FAIL** (explained above), 0 WARN. - All six version spots say 26.10.1. The CHANGELOG heading date equals the metainfo date, 2026-10-04. - `appstreamcli validate` passed. The `<release>` list is date-descending with unique versions (22 entries). - `[Unreleased]` stays empty, `[v26.10.1]` sits directly under it, and the CHANGELOG diff is exactly the inserted section, with history byte-identical to `origin/main`. - All 95 PRs in the range are cited. The only other numbers cited are #3849 (RFC), #5262 (tracking issue), #5468 (RFC), #6091 (RFC, approved and closed) and #6142 (open follow-up issue for #6119). - The intro (95/12), the Contributors counts (13 entries) and "first-time: none" are re-derived from the mapping, and they match. - ROADMAP `(vX)` markers lead with v26.10.1. Every local link in README and ROADMAP resolves. No removed heading is linked elsewhere. Code fences balance, with no duplicated table headers or doubled `---`. - **FAIL:** "only the four current-version lines lose `26.9.5`". The lines are the trimmed Recently-shipped markers; see the judgment calls above. - `git diff --check` is clean. The diff touches no `src/`, `tests/`, `third_party/` or `.github/workflows/` path. - Configure: `cmake -S . -B <scratch>` reports `CMAKE_PROJECT_VERSION=26.10.1`. No build or test was run, because this is metadata only; CI builds it. - `main` CI at the cutoff: `CI` passed on `18c5245e5`. After merge: tag the resulting **main** commit, not this branch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge order: #6008 has merged (
4d5b9893). This PR is replayed ontomain(tip5fae2f86when pushed) and no longer carries any #6008 commits.Summary
Closes #6026. Radio Setup → Peripherals shows a persisted device list and a per-device detail page, with Add, Remove, and a per-device automatic-connection toggle (Connect directly when available on TGXL and PGXL, Connect automatically on Antenna Genius and ShackSwitch). This implements the ruling on #6026 ("approved, with conditions"); the checklist below maps each condition to its commit.
Ruling checklist
itemAtPosition; list wording DIRECT / RADIO / OFFLINE1d96a707feaaefe8feaaefe8,a8300aceDiscoveryDismissedkeyfeaaefe8,da7df5c7PeripheralSettings.hnote correctedfeaaefe806a20738VisibleDevicesseeding, AG reconciliation and refresh follow signals; the 250 ms timer is gone1d96a7071d96a7071d96a707a8300acePeripheralDeviceStatus); nothing searches text forError:or a dash1d96a707Also from the 2026-10-02 and 2026-10-03 reviews: the removal lease now stops every route to a socket (explicit, automatic, alternate, reconnect timer), checked before attempt state changes (
06a20738,38931177); a blocked recovery row shows the endpoint that refused the code; an address saved from the AG applet while a deletion is pending survives the Remove; an AG or ShackSwitch row with no address can be removed beside the other device's stored code; a disabled Remove button reads as disabled (37baf0e1).Connecting automatically
TGXL, PGXL, Antenna Genius and ShackSwitch each have a toggle, on by default, stored as
Peripherals.<id>.AutoConnect. Off blocks startup, discovery/radio-presence, alternate-address and reconnect attempts for that device. An explicit Connect always works and does not change the toggle; a session it starts is not reconnected after it drops.Every path that opens a socket emits
attemptStarted()onTgxlConnection,PgxlConnectionandAntennaGeniusModel, so Setup shows Connecting… for automatic attempts as well. The authorization-blocked recovery row shows the endpoint that refused the code unless the operator has edited the field.Remove outcomes
Scope
Peripherals UI and the persistence, credential-store and connection-lifecycle changes it needs. No authentication protocol, dependency, thread, radio family or TX command is added. TGXL/PGXL/AG/ShackSwitch endpoints keep their flat keys; no migration. The three applets now take their DIRECT / RADIO / OFFLINE strings from
PeripheralConnectionSource.h(strings unchanged). Touchpoint manifest regenerated.Test plan
peripheral_auth_dialog_test,peripheral_auth_handshake_test,peripheral_auth_keychain_test, both automation-redaction tests,tgxl_*,amp_applet_testand theradio_setup_*tests pass.pgxl_panel_testfails on unchangedmaintoo (a layout height assertion) and is unrelated.gen_touchpoint_manifest.py --check,check_engine_boundary.py --strict(0 blocking),check_a11y.pyon the new file (0 findings);grep itemAtPosition src/gui/RadioSetupDialog*.cppis empty.DEMO-0001): Add persists across a restart; the toggles read "Connect directly when available" (TGXL, PGXL) and "Connect automatically" (AG, ShackSwitch); with the AG toggle off a discovered Antenna Genius stays OFFLINE through repeated beacons (with it on, it connects); Remove asks first and Cancel keeps the row.ci.yml -Rfilter.CI: the focused tests above are not in any
ci.yml-Rfilter, so green CI does not mean they ran; that filter set is frozen per PR and is left to the maintainer.🤖 Generated with Claude Code