fix(radio): make disabled Reboot Radio button legible instead of near-invisible (#3334)#3441
Conversation
|
Hi @M7HNF-Ian — thanks for picking this one up, it's a great little fix and the stylesheet token rewrite is exactly the right call. Quick note on the red ❌ you're seeing: the only failing check on What the check actually does
And the workflow's summary comment ends with "Findings are warnings only — the build is not blocked." Why your change is fineYour diff only touches one disabled-button stylesheet in Exit code 0. What's actually breakingThe failing step in the workflow is: python tools/check_a11y.py $FILES | tee /tmp/a11y_output.txt
COUNT=$(grep -c "^::warning" /tmp/a11y_output.txt || echo "0")
echo "findings=$COUNT" >> $GITHUB_OUTPUTGitHub Actions runs shell steps with This is not anything you did — your PR is the right shape and the change is sound. The fix belongs in the workflow file, not in your branch. What to do
Thanks again for sending this in — making the disabled Reboot Radio button legible is a nice quality-of-life fix and the inline comment explaining the reasoning is exactly the right touch. 🎯 — AetherClaude 🤖 aethersdr-agent · cost: $14.1134 · model: claude-opus-4-7 |
|
Heads up: the failing Qt Accessibility Static Analysis check is a pre-existing CI workflow bug, not this PR — the scan itself reports 0 findings ("No accessibility issues found"), then the step errors while capturing the count:
|
…findings (#3442) The accessibility check job fails on any PR where a changed `src/gui` file has **zero** findings — the check itself passes, but the step errors with: ``` ##[error]Unable to process file command 'output' successfully. ##[error]Invalid format '0' ``` ## Cause `grep -c "^::warning"` prints `0` to stdout **and** exits non-zero when there are no matches. So: ```bash COUNT=$(grep -c "^::warning" /tmp/a11y_output.txt || echo "0") ``` captures **both** grep's own `0` and the `|| echo "0"`, producing the two-line value `"0\n0"`. Writing `findings=0\n0` to `$GITHUB_OUTPUT` is malformed → step failure. ## Fix ```diff - COUNT=$(grep -c "^::warning" /tmp/a11y_output.txt || echo "0") + COUNT=$(grep -c "^::warning" /tmp/a11y_output.txt || true) ``` `grep -c` already prints `0` on no match; `|| true` simply swallows its non-zero exit under `set -e`, without appending a duplicate. Verified locally: | Input | Old `COUNT` | New `COUNT` | |---|---|---| | no `::warning` lines | `0\n0` (malformed) | `0` | | 2 `::warning` lines | `2` | `2` | Seen on #3441 (a clean GUI change with 0 a11y findings).
…-invisible (aethersdr#3334) The "Reboot Radio" button in Radio Setup appears to vanish when the dialog is opened while the radio is disconnected/reconnecting (e.g. the ~40s after clicking Reboot). The button is always added, but setEnabled(isConnected()) disables it when disconnected, and the disabled stylesheet used three dim blue-grey tokens (background.1 #1a2a3a / meter.bar.fill #405060 / background.2 #304050) against the dialog background background.0 #0f0f1a — all near-equal dark tones, so the disabled button blended into the dialog and read as an empty row rather than a greyed-out control. It reappears on reconnect via the existing connectionStateChanged -> setEnabled hook. Give the disabled state a legible muted-red appearance consistent with the button's enabled red identity so a disabled control reads as disabled, not missing. Follow-up to aethersdr#3334.
c25a947 to
2853ac6
Compare
|
Rebased your branch onto current main (`2853ac62`) to pick up #3442's a11y workflow fix. Your code didn't trigger any a11y findings — the failure was the same workflow-yaml bug that hit #3439: the pre-#3442 step crashed writing the zero-findings result to `$GITHUB_OUTPUT`. Root cause (workflow bug, not your code)The old workflow had `COUNT=$(grep -c "^::warning" ... || echo "0")` — when `grep -c` finds zero matches it exits 1 and prints `0`, so the `|| echo "0"` appended a second `0` and `echo "findings=$COUNT" >> $GITHUB_OUTPUT` wrote a malformed multi-line value. The parser rejected it with `Invalid format '0'` and the step failed even though the lint had already succeeded. #3442 changed it to `|| true`. Timing: your CI ran 2026-06-06 20:07 UTC, #3442 merged 21:44 UTC — the workflow ran from your PR branch's pre-fix copy. What I didSame maintainer-fix-up pattern as the other PRs landing today — rebased your single commit onto current main, no content changes to the actual fix (the `#2a1818 / #8a6055 / #4a2828` hex values + the `#3334 follow-up` comment are exactly as you wrote them). CI will re-run against `2853ac62` and the a11y check will pass cleanly this time. Real-talk on the fix itself: nice catch. Replacing those theme tokens with concrete contrast-good hex values is the right call — the previous `{{color.background.1}}` / `{{color.meter.bar.fill}}` / `{{color.background.2}}` tokens really did all dim to similar blue-greys, making the disabled button look absent rather than greyed-out. Will give the actual change a closer look once CI greens. Principle XI. |
Two related fixes to TitleBar's new PanLock accessors so all four platforms compile again: 1. signals: vs. public: — MOC parses anything inside a `signals:` block as a signal declaration, so the inline `bool isPanFollowChecked() const` and `void setPanFollowChecked(bool)` were rejected at moc time with "Not a signal declaration" (TitleBar.h:56). Both methods are accessors, not signals — they belong in `public:`. 2. Out-of-line over inline. Even after the section move, the inline bodies referenced `m_panFollowBtn->isChecked()` and `QSignalBlocker`, which need the full QPushButton type. The header only forward-declares QPushButton, so inline bodies couldn't compile in callers either (the moc error masked this until now). Moving the definitions to TitleBar.cpp keeps the header light and matches the style of neighbours like `isSystemMoveAreaAt`. Build verified locally on Arch Linux x86 — 632/632 clean (only the pre-existing unrelated macDaxDriverInstalled warning). Same maintainer fix-up pattern as aethersdr#3279/aethersdr#3286/aethersdr#3289/aethersdr#3381/aethersdr#3398/aethersdr#3417/aethersdr#3439/aethersdr#3441. Principle XI.
…tions Follow-up to #3289 closing five gaps surfaced by reviewing our doc against the WCAG/POUR-grounded coverage at accessibilitychecker.org. Our existing doc is materially deeper than that article on Qt-specific patterns (live value announcements, throttling, custom-painted widgets, lint mechanics), but it was missing five concrete things every Qt-desktop a11y guide should have. All five land as additions; no existing section is changed. 1. "What we're following" — three-line POUR (Perceivable / Operable / Understandable / Robust) grounding right after the Background. Cites WCAG 2.1 quick-ref. Future contributors asking "why does this rule exist" now get "WCAG 2.1 says so" rather than "because the project says so." 2. Form-field instructions — added to the existing "Accessible description" bullet. For QSpinBox / QLineEdit / QComboBox, the accessible name answers "what is this?" and the description answers "what do I type?". Tooltips don't reach the screen reader; the description does — that's the right home for input semantics. 3. Keyboard-only test — added to the existing "Tab focus" bullet. Tab to the widget from a sibling, activate with Space/Return, no mouse. If you can't reach it the focus policy is wrong; if Space/Return doesn't fire it the widget needs a keyPressEvent (or it should have been a QPushButton — links to the QLabel anti-pattern section). 4. Colour contrast — new section between "Live value updates" and "Custom-painted widgets." WCAG 4.5:1 normal text / 3:1 large text and non-text components. Cites #3441 (Reboot Radio button) as the canonical disabled-state-blends-into-background case so future readers see why the section exists. WebAIM Contrast Checker + the OS-level "Increase Contrast" mode as the practical verification workflow. 5. Manual verification — new section between "Suppressing the lint" and "CI enforcement." Names the three platform screen readers (VoiceOver, NVDA/Narrator, Orca) with the actual keystrokes to launch them, and tells contributors to walk the widget the way a screen-reader user would. Lint = static; this = real test. Explicit that the lint catches what it can detect statically and nothing more. Doc grows 166 → 254 lines (+88). Section order flows motivation → grounding → patterns → escape valves → verification → mechanism. Principle XI.
Self-review of #3443 surfaced five issues; all five addressed here. 1. Colour contrast section now matches the theme-tokens-first policy established in #3441 / #3446 (instead of contradicting it). The earlier text read "pick concrete hex values that hit the WCAG ratio against the real surrounding background; reserve the dimmest theme tokens for genuinely decorative state where invisibility is the intent" — which advocated hex over tokens for disabled state and would have read as policy by future contributors. The actual policy from the #3441/#3446 thread: theme tokens are first choice; hex is a valid intermediate state when no semantic token exists, with a follow-up issue to add the proper token (mirroring how #3441 + #3446 played out). Adds the three-step rule of thumb (existing token for this widget × state? token for similar widget? hex + issue?) and cites #3441's merge commit and #3446's open status as the canonical example. 2. POUR Operable softened from overclaim to aspirational. Was: "Operable — keyboard focus and activation paths for every interactive widget; no mouse-only interactions." That's false today — CHAIN drag-drop, frequency drag, drag-to-pan, custom panadapter handlers are all mouse-only. Replaced with "every interactive widget reachable by keyboard, with a non-mouse activation path. (AetherSDR still has surfaces that are mouse-only today — [list] — which we're working toward; new code shouldn't add to the list.)" 3. Tooltip-vs-description claim corrected. Was: "right place to put input semantics, not in a separate tooltip the screen reader can't reach." Qt does expose tooltips to AT via QAccessible::Help — they reach screen readers as help text, fired on explicit request. The argument that descriptions are the better home for input semantics still stands, but now states the actual reason (descriptions are read automatically as part of widget identity; tooltips are help-text-on-request). 4. NVDA "say more" mode reference replaced with neutral language. "Say more" isn't a standard NVDA mode name (it may be confused with VoiceOver's "speak more"). Now says "in verbose announcement modes" without committing to a specific NVDA mode label. 5. WebAIM Contrast Checker workflow fixed. The previous text said "take a screenshot of the widget against its real background and drop it into the WebAIM Contrast Checker" — but that tool takes two hex codes, not an image. Reworded to mention the eyedropper step (Digital Color Meter on macOS, PowerToys Color Picker on Windows, gpick/gcolor3 on Linux) and added a pointer to Colour Contrast Analyser (TPGi) for a one-step screenshot-to-ratio workflow with integrated eyedropper. Doc grows 254 → 286 lines net (+74 modifications inside the existing +89 from the original commit). Section structure unchanged. Principle XI.
Two related fixes to TitleBar's new PanLock accessors so all four platforms compile again: 1. signals: vs. public: — MOC parses anything inside a `signals:` block as a signal declaration, so the inline `bool isPanFollowChecked() const` and `void setPanFollowChecked(bool)` were rejected at moc time with "Not a signal declaration" (TitleBar.h:56). Both methods are accessors, not signals — they belong in `public:`. 2. Out-of-line over inline. Even after the section move, the inline bodies referenced `m_panFollowBtn->isChecked()` and `QSignalBlocker`, which need the full QPushButton type. The header only forward-declares QPushButton, so inline bodies couldn't compile in callers either (the moc error masked this until now). Moving the definitions to TitleBar.cpp keeps the header light and matches the style of neighbours like `isSystemMoveAreaAt`. Build verified locally on Arch Linux x86 — 632/632 clean (only the pre-existing unrelated macDaxDriverInstalled warning). Same maintainer fix-up pattern as aethersdr#3279/aethersdr#3286/aethersdr#3289/aethersdr#3381/aethersdr#3398/aethersdr#3417/aethersdr#3439/aethersdr#3441. Principle XI.
…tions (#3443) ## Summary Follow-up to #3289 closing five gaps surfaced by reviewing `docs/a11y.md` against the WCAG/POUR-grounded coverage at [accessibilitychecker.org/blog/a11y/](https://www.accessibilitychecker.org/blog/a11y/). Our existing doc is materially deeper than that article on Qt-specific patterns (live value announcements, throttling, custom-painted widgets, lint mechanics) — but it was missing five concrete things every Qt-desktop a11y guide should have. All five land as pure additions; no existing section is modified. ## What's added 1. **"What we're following" section** — three-line POUR (Perceivable / Operable / Understandable / Robust) grounding right after the Background. Cites WCAG 2.1 quick-ref. Future contributors asking *"why does this rule exist"* now get *"WCAG 2.1 says so"* rather than *"because the project says so."* 2. **Form-field instructions guidance** — added to the existing "Accessible description" bullet. For `QSpinBox` / `QLineEdit` / `QComboBox`, the accessible name answers *"what is this?"* and the description answers *"what do I type?"*. Tooltips don't reach the screen reader; the description does. 3. **Keyboard-only test** — added to the existing "Tab focus" bullet. Tab to the widget from a sibling, activate with `Space`/`Return`, no mouse. Links to the existing QLabel anti-pattern section as the structural escape. 4. **Colour contrast section** — between "Live value updates" and "Custom-painted widgets." WCAG **4.5:1 normal text / 3:1 large text** and non-text components. Cites #3441 (Reboot Radio button) as the canonical disabled-state-blends-into-background case so future readers see *why* the section exists. WebAIM Contrast Checker + the OS-level "Increase Contrast" mode as the practical verification workflow. 5. **Manual verification section** — between "Suppressing the lint" and "CI enforcement." Names the three platform screen readers ([VoiceOver](https://www.apple.com/accessibility/), [NVDA](https://www.nvaccess.org/) / Narrator, [Orca](https://help.gnome.org/users/orca/stable/)) with the actual keystrokes to launch them. Lint = static; this = real test. Explicit that *"the lint catches what it can detect statically — this is the real test."* ## Doc shape | | Before | After | |---|---|---| | Lines | 166 | 254 | | Sections | 7 | 9 | Section order now flows: motivation → grounding → patterns → escape valves → verification → mechanism. ## What I *didn't* import from the article - **Legal framing** ("legally mandated… legal action or fines"). Out of voice. AetherSDR's a11y motivation is "a blind ham operator filed #3288 and we want AetherSDR usable for him" — that's a stronger frame than fear of lawsuits. - **POUR as the organizing structure of the whole doc**. The article is structured around POUR; we'd lose clarity if we reshuffled our concrete Qt sections under POUR headings. Mention WCAG/POUR once near the top, keep our pragmatic structure. - **Alt text on images / captions on video**. We have ~zero of either in AetherSDR — no help text videos, the only "images" are panadapter renders (covered by our custom-painted-widgets section). ## Test plan - [x] `docs/a11y.md` parses as valid markdown (9 `##` sections, no orphaned formatting). - [x] Links checked: WCAG 2.1 quickref, WebAIM contrast checker + articles/contrast, NVDA, Apple Accessibility, Orca docs — all resolve. - [x] No code changes — `tools/check_a11y.py` and the workflow are untouched, so no CI behaviour change. Refs #3288 #3289
… ThemeManager tokens. Principle IX. (#3645) ## Summary Follow-up to #3439 and #3441. Fills three hardcoded-colour gaps in the theme token system — the last surfaces in the FreeDV Reporter dialog and Radio Setup dialog that hadn't been migrated to ThemeManager. **FreeDvReporterModel — row highlight colours** - Removes four hardcoded `QColor` constants (`kTxBg` / `kRxBg` / `kMsgBg` / `kHlFg`) and replaces their three use sites in `data()` with `ThemeManager::instance().color(token)` calls. - Adds a `themeChanged` → `dataChanged{BackgroundRole, ForegroundRole}` connection in the constructor so highlighted rows repaint immediately on a theme switch instead of waiting for the next 250 ms highlight tick. - Note: `color.background.tx` (`#3a2a0e`) is a chrome-panel tint used in 35+ surfaces and could not be reused here. The TX-row highlight (`#fc4500`) is a distinct high-visibility colour matching the freedv-gui reference palette, so new tokens were required for all four constants. **RadioSetupDialog — button disabled states** - Migrates three `QPushButton:disabled` CSS blocks to tokens. - Reboot Radio button (`buildRadioTab` ~line 601): uses new `color.button.danger.*disabled` tokens — preserves the intentional danger-red colouring in the disabled state. - Upload Firmware button (`buildRadioTab` ~line 838): was using `{{color.meter.bar.fill}}` and `{{color.background.1}}` as disabled-state stand-ins (semantically wrong tokens from a prior partial migration). Now uses correct `color.button.*disabled` tokens. - Serial tab Open/Close port buttons (`buildSerialTab` ~line 4054): hardcoded hex replaced with `color.button.*disabled` tokens; `setStyleSheet` calls replaced with `applyStyleSheet` so the template engine resolves tokens and the widgets participate in live theme switching. **New tokens — 10 total across both theme files** | Token | Dark | Light | |---|---|---| | `color.highlight.tx` | `#fc4500` | `#fc4500` | | `color.highlight.rx` | `#379baf` | `#379baf` | | `color.highlight.message` | `#e58be5` | `#e58be5` | | `color.highlight.fg` | `#000000` | `#000000` | | `color.button.background.disabled` | `#203040` | `{color.gray.700}` | | `color.button.foreground.disabled` | `{color.gray.500}` | `{color.gray.500}` | | `color.button.border.disabled` | `{color.gray.700}` | `{color.gray.600}` | | `color.button.danger.background.disabled` | `#2a1818` | `#f0d8d8` | | `color.button.danger.foreground.disabled` | `#8a6055` | `#b08080` | | `color.button.danger.border.disabled` | `#4a2828` | `#d0a8a8` | Highlight colours are kept identical between dark and light themes — they are high-saturation reference palette colours from freedv-gui that provide adequate contrast with the black foreground on both backgrounds. A future theme author can override them via the token. Fixes #3446. ## Test plan - [ ] Build clean on Windows (MinGW), no new warnings - [ ] FreeDV Reporter: open dialog, observe another station transmitting — TX row shows orange background with black text - [ ] FreeDV Reporter: RX highlight (teal) and message highlight (purple) appear and expire after ~5 s - [ ] FreeDV Reporter: switch theme while a highlight is active — row repaints immediately (does not wait for the 250 ms tick) - [ ] Radio Setup → General tab: disconnect radio, confirm Reboot Radio button disabled state is dark red-tinted (not generic grey) - [ ] Radio Setup → General tab: Upload Firmware button renders correct disabled state when no firmware check has run - [ ] Radio Setup → Serial tab: open a port config, confirm Open/Close buttons show correct disabled styling when not applicable - [ ] Light theme: open FreeDV Reporter and Radio Setup, confirm all three token groups (highlight, button, danger) render legibly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…findings (aethersdr#3442) The accessibility check job fails on any PR where a changed `src/gui` file has **zero** findings — the check itself passes, but the step errors with: ``` ##[error]Unable to process file command 'output' successfully. ##[error]Invalid format '0' ``` ## Cause `grep -c "^::warning"` prints `0` to stdout **and** exits non-zero when there are no matches. So: ```bash COUNT=$(grep -c "^::warning" /tmp/a11y_output.txt || echo "0") ``` captures **both** grep's own `0` and the `|| echo "0"`, producing the two-line value `"0\n0"`. Writing `findings=0\n0` to `$GITHUB_OUTPUT` is malformed → step failure. ## Fix ```diff - COUNT=$(grep -c "^::warning" /tmp/a11y_output.txt || echo "0") + COUNT=$(grep -c "^::warning" /tmp/a11y_output.txt || true) ``` `grep -c` already prints `0` on no match; `|| true` simply swallows its non-zero exit under `set -e`, without appending a duplicate. Verified locally: | Input | Old `COUNT` | New `COUNT` | |---|---|---| | no `::warning` lines | `0\n0` (malformed) | `0` | | 2 `::warning` lines | `2` | `2` | Seen on aethersdr#3441 (a clean GUI change with 0 a11y findings).
…-invisible (aethersdr#3334) (aethersdr#3441) ## Problem The "Reboot Radio" button in Radio Setup (added in aethersdr#3334) appears to vanish if you open Radio Setup while the radio is disconnected/reconnecting — e.g. during the ~40s after clicking Reboot, or any reconnect window. ## Root cause The button is always added, but `setEnabled(m_model->isConnected())` disables it when disconnected. The disabled stylesheet used three dim blue-grey tokens — `background.1 (#1a2a3a)`, `meter.bar.fill (#405060)`, `background.2 (#304050)` — against the dialog background `background.0 (#0f0f1a)`. All near-equal dark tones, so the disabled button blends into the dialog and reads as an empty row rather than a greyed-out control. It reappears once the radio reconnects via the existing `connectionStateChanged → setEnabled` hook. ## Fix Give the disabled state a legible muted-red appearance consistent with the button's red identity: ``` QPushButton:disabled { background: #2a1818; color: #8a6055; border-color: #4a2828; } ``` ## How found Instrumented logging confirmed the button is added with `enabled=true` when connected and `enabled=false` when the dialog is built mid-reconnect — i.e. a contrast/styling issue, not a missing widget. ## Testing - Clean build on macOS. - Enabled state unchanged (bright orange, verified). - Disabled-state contrast raised from near-invisible dim blue-grey to legible muted red. Follow-up to aethersdr#3334.
…tions (aethersdr#3443) ## Summary Follow-up to aethersdr#3289 closing five gaps surfaced by reviewing `docs/a11y.md` against the WCAG/POUR-grounded coverage at [accessibilitychecker.org/blog/a11y/](https://www.accessibilitychecker.org/blog/a11y/). Our existing doc is materially deeper than that article on Qt-specific patterns (live value announcements, throttling, custom-painted widgets, lint mechanics) — but it was missing five concrete things every Qt-desktop a11y guide should have. All five land as pure additions; no existing section is modified. ## What's added 1. **"What we're following" section** — three-line POUR (Perceivable / Operable / Understandable / Robust) grounding right after the Background. Cites WCAG 2.1 quick-ref. Future contributors asking *"why does this rule exist"* now get *"WCAG 2.1 says so"* rather than *"because the project says so."* 2. **Form-field instructions guidance** — added to the existing "Accessible description" bullet. For `QSpinBox` / `QLineEdit` / `QComboBox`, the accessible name answers *"what is this?"* and the description answers *"what do I type?"*. Tooltips don't reach the screen reader; the description does. 3. **Keyboard-only test** — added to the existing "Tab focus" bullet. Tab to the widget from a sibling, activate with `Space`/`Return`, no mouse. Links to the existing QLabel anti-pattern section as the structural escape. 4. **Colour contrast section** — between "Live value updates" and "Custom-painted widgets." WCAG **4.5:1 normal text / 3:1 large text** and non-text components. Cites aethersdr#3441 (Reboot Radio button) as the canonical disabled-state-blends-into-background case so future readers see *why* the section exists. WebAIM Contrast Checker + the OS-level "Increase Contrast" mode as the practical verification workflow. 5. **Manual verification section** — between "Suppressing the lint" and "CI enforcement." Names the three platform screen readers ([VoiceOver](https://www.apple.com/accessibility/), [NVDA](https://www.nvaccess.org/) / Narrator, [Orca](https://help.gnome.org/users/orca/stable/)) with the actual keystrokes to launch them. Lint = static; this = real test. Explicit that *"the lint catches what it can detect statically — this is the real test."* ## Doc shape | | Before | After | |---|---|---| | Lines | 166 | 254 | | Sections | 7 | 9 | Section order now flows: motivation → grounding → patterns → escape valves → verification → mechanism. ## What I *didn't* import from the article - **Legal framing** ("legally mandated… legal action or fines"). Out of voice. AetherSDR's a11y motivation is "a blind ham operator filed aethersdr#3288 and we want AetherSDR usable for him" — that's a stronger frame than fear of lawsuits. - **POUR as the organizing structure of the whole doc**. The article is structured around POUR; we'd lose clarity if we reshuffled our concrete Qt sections under POUR headings. Mention WCAG/POUR once near the top, keep our pragmatic structure. - **Alt text on images / captions on video**. We have ~zero of either in AetherSDR — no help text videos, the only "images" are panadapter renders (covered by our custom-painted-widgets section). ## Test plan - [x] `docs/a11y.md` parses as valid markdown (9 `##` sections, no orphaned formatting). - [x] Links checked: WCAG 2.1 quickref, WebAIM contrast checker + articles/contrast, NVDA, Apple Accessibility, Orca docs — all resolve. - [x] No code changes — `tools/check_a11y.py` and the workflow are untouched, so no CI behaviour change. Refs aethersdr#3288 aethersdr#3289
Problem
The "Reboot Radio" button in Radio Setup (added in #3334) appears to vanish if you open Radio Setup while the radio is disconnected/reconnecting — e.g. during the ~40s after clicking Reboot, or any reconnect window.
Root cause
The button is always added, but
setEnabled(m_model->isConnected())disables it when disconnected. The disabled stylesheet used three dim blue-grey tokens —background.1 (#1a2a3a),meter.bar.fill (#405060),background.2 (#304050)— against the dialog backgroundbackground.0 (#0f0f1a). All near-equal dark tones, so the disabled button blends into the dialog and reads as an empty row rather than a greyed-out control. It reappears once the radio reconnects via the existingconnectionStateChanged → setEnabledhook.Fix
Give the disabled state a legible muted-red appearance consistent with the button's red identity:
How found
Instrumented logging confirmed the button is added with
enabled=truewhen connected andenabled=falsewhen the dialog is built mid-reconnect — i.e. a contrast/styling issue, not a missing widget.Testing
Follow-up to #3334.