// Auto-generated from CHANGELOG.md by scripts/gen_whatsnew.py
// Do not edit — changes will be overwritten on next build.
#include "generated/WhatsNewData.h"

namespace AetherSDR {

const std::vector<ReleaseEntry>& whatsNewEntries() {
    static const std::vector<ReleaseEntry> entries = {
        {QStringLiteral("26.5.2.1"), QStringLiteral("2026-05-17"), QStringLiteral("New region data"), {
            {ChangeCategory::BugFix, QStringLiteral("TCI TX audio level regression vs v26.5.1"), QStringLiteral("v26.5.2's TCI server identity (`device:SunSDR2DX` + `protocol:ExpertSDR2,1.9`, added in #2597 for RF2K-S amplifier whitelist compatibility) hit a specific code path in WSJT-X's `TCITransceiver` (`T...")},
            {ChangeCategory::BugFix, QStringLiteral("Windows process lingering in Task Manager after close (#2802, chibondking)"), QStringLiteral("Three independent root causes ganged up to keep `AetherSDR.exe` visible in Task Manager indefinitely after the main window closed. Each ships a minimal one-line fix matching an existing precedent: ...")},
            {ChangeCategory::BugFix, QStringLiteral("Linux AppImage build failure on gcc 11 strict mode"), QStringLiteral("`third_party/libmodem_core/bitstream.h:192` used unqualified `container` inside the `noexcept(noexcept(...))` clause of a class template member. gcc 11.4 (Ubuntu 22.04 native runner used by the App...")},
        }},
        {QStringLiteral("26.5.2"), QStringLiteral("2026-05-17"), QStringLiteral("Six days of community momentum — AetherModem, FreeDV ergonomics, FLEX-6700 4m/2m, dialog architecture sweep"), {
            {ChangeCategory::Feature, QStringLiteral("AetherModem — AX.25 packet decoder (Phase 0, #2753, jensenpat)"), QStringLiteral("Native HDLC/AX.25 packet decoder integrated into the slice signal path. 21-lane phase-bank receiver with FCS validation. New `Packet Decoder` dialog accessible from the slice context menu; tagged a...")},
            {ChangeCategory::Feature, QStringLiteral("AetherModem Phase-0 indicator on Packet Decoder dialog"), QStringLiteral("Surfaces the Experimental status with a dedicated banner so operators understand TX coverage (HF 300 baud only) vs RX coverage (HF 300 + VHF 1200).")},
            {ChangeCategory::Feature, QStringLiteral("FreeDV/RADE sync + SNR indicator in VFO widget (#2776, NF0T)"), QStringLiteral("New VFO widget chrome on FDVU/FDVL slices: shows sync state and decoded SNR alongside the standard frequency / mode / filter display.")},
            {ChangeCategory::Feature, QStringLiteral("RADE info row — far-end callsign, SNR, freq offset"), QStringLiteral("Per-slice RADE info row exposing the far-end's callsign, decoded SNR, and frequency offset for live awareness during a QSO.")},
            {ChangeCategory::Feature, QStringLiteral("RADE EOO callsign decode + FreeDV Reporter posting"), QStringLiteral("Decodes the end-of-over callsign from the far end and posts it to FreeDV Reporter for spotting / heard-list integration.")},
            {ChangeCategory::Feature, QStringLiteral("FlexWaveformModel for firmware v4.2.18 waveform status (#2759, NF0T)"), QStringLiteral("New `FlexWaveformModel` parses `waveform` status objects from firmware v4.2.18 and surfaces them in a dedicated Waveforms dialog (#2779) under File menu for WFP status and per-waveform management.")},
            {ChangeCategory::Feature, QStringLiteral("4m / 2m bands on FLEX-6500 and FLEX-6700"), QStringLiteral("Surfaces 4 m and 2 m band buttons on FLEX-6500 / FLEX-6700 via `ModelCapabilities`. Reads the per-model XVTR allocation from FlexLib `ModelInfo.cs` (authoritative source) and routes click-to-band t...")},
            {ChangeCategory::Feature, QStringLiteral("Smart Spot Filtering — dim unmatched DX voice spots"), QStringLiteral("New SpotHub filter that dims/hides DX voice spots that don't match a user-defined pattern (country/zone/mode). Companion to the existing callsign-pattern filter; user-configurable match window in #...")},
            {ChangeCategory::Feature, QStringLiteral("Auto-squelch with 3-way SQL cycle"), QStringLiteral("New tri-state squelch toggle: Off → SQL (manual) → Auto (tracks the noise floor). Cycles via the SQL button or the keyboard shortcut.")},
            {ChangeCategory::Feature, QStringLiteral("Local antenna display names"), QStringLiteral("Per-radio antenna labels (e.g. \"ANT1 → 80m Dipole\") stored client-side and applied across slice context menus, RX applet, and TX band dialog. Radio still uses ANT1/ANT2/RX-A for the wire protocol.")},
            {ChangeCategory::Feature, QStringLiteral("ATU band pre-tune sweep + clear memories"), QStringLiteral("New ATU action: sweep the active band in 20 kHz steps and pre-tune every segment, building a band-wide ATU memory table in one pass. Plus a per-band Clear Memories action.")},
            {ChangeCategory::Feature, QStringLiteral("License confirmation modal for Antenna SWR Sweep"), QStringLiteral("Adds a one-time license-acknowledgement modal before allowing Antenna SWR Sweep — clarifies that the sweep transmits.")},
            {ChangeCategory::Feature, QStringLiteral("Numeric entry for Aetherial Audio effect parameters"), QStringLiteral("All Aetherial Audio Channel Strip knobs now support direct numeric entry alongside drag-to-set.")},
            {ChangeCategory::Feature, QStringLiteral("FlexRadio profile import/export via .ssdr_cfg"), QStringLiteral("Import and export TX / Mic / Global profiles in FlexRadio's `.ssdr_cfg` wire format. Round-trip compatible with SmartSDR; lets operators migrate profile libraries from Windows to Linux/macOS.")},
            {ChangeCategory::Feature, QStringLiteral("FlexControl CWX F1-F12 macro actions"), QStringLiteral("The FlexControl USB knob can now trigger CWX (and DVK) F-key macros via button mappings.")},
            {ChangeCategory::Feature, QStringLiteral("Independent RX/TX CW decode toggles"), QStringLiteral("RX-side and TX-side CW decode can be enabled independently; previously toggled together.")},
            {ChangeCategory::Feature, QStringLiteral("Auto-Save inline when TX/Mic profile name collides"), QStringLiteral("The profile-save warning dialog now offers Auto-Save as an inline option rather than requiring a separate menu trip.")},
            {ChangeCategory::Feature, QStringLiteral("Persist TX Tune Mode + EQ applet RX/TX view"), QStringLiteral("Tune Mode (single-tone vs two-tone) and the EQ applet's RX/TX selection persist across launches. Note: TX Tune Mode persistence was subsequently reverted to a right-click menu in #2787 to avoid the...")},
            {ChangeCategory::Feature, QStringLiteral("Two-tone TUNE selector via right-click"), QStringLiteral("TUNE button right-click exposes Mono Tone / Two Tone selection; cleaner than the persisted-setting form.")},
            {ChangeCategory::Feature, QStringLiteral("TNF status tooltip"), QStringLiteral("Hovering a TNF marker now shows depth/permanent state inline.")},
            {ChangeCategory::Feature, QStringLiteral("Audio device hotplug + notification dialog"), QStringLiteral("Detects audio device add/remove events from the OS and routes the current PC audio path through the change with a notification dialog so the user can choose to follow the new default or stay.")},
            {ChangeCategory::Feature, QStringLiteral("PEP peak-hold tick on TX FWDPWR gauge"), QStringLiteral("Adds the canonical PEP peak-hold tick (with configurable hold time) to the FWDPWR meter, matching SmartSDR's PEP display semantics.")},
            {ChangeCategory::Feature, QStringLiteral("Profile Manager non-modal + persist geometry"), QStringLiteral("Profile Manager dialog migrated to the PersistentDialog base class — non-modal, geometry persists, frameless chrome support.")},
            {ChangeCategory::Feature, QStringLiteral("Ctrl-drag waterfall rate control"), QStringLiteral("Holding Ctrl while dragging the waterfall vertically adjusts the scroll rate (50 ms – 500 ms per line).")},
            {ChangeCategory::Feature, QStringLiteral("Ctrl-drag dBm scale zoom"), QStringLiteral("Holding Ctrl while dragging the dBm scale zooms vertical range symmetrically; tooltip and help docs (#2726) document the gesture.")},
            {ChangeCategory::Feature, QStringLiteral("Pan-follow-VFO triggers off flag outer edge"), QStringLiteral("When a VFO flag approaches the panadapter edge, pan-follow now triggers off the outer edge of the flag rather than the center — no more cropped flags when the user pans to track a signal.")},
            {ChangeCategory::Feature, QStringLiteral("Spectrum overlay Display panel reorder + SQL Margin → RX Applet"), QStringLiteral("Display panel controls reordered for clarity; SQL Margin moved from the spectrum overlay to the RX applet where it belongs.")},
            {ChangeCategory::Feature, QStringLiteral("Audio Device Detected dialog gets frameless chrome"), QStringLiteral("Standard 18 px gradient title bar + chrome treatment, matching the rest of the app.")},
            {ChangeCategory::Feature, QStringLiteral("Floor: auto-adjust — asymmetric smoothing + transient rejection"), QStringLiteral("The Floor: auto-adjust algorithm now uses asymmetric smoothing (fast attack on rising floor, slow release on falling) and rejects transient peaks. Replaces the old zoom-based response with a pan-on...")},
            {ChangeCategory::Feature, QStringLiteral("Radio-letter slice display mode for Multi-Flex"), QStringLiteral("New display mode shows radio-side slice identifier (A/B/C/D) instead of client-local letter for Multi-Flex sessions where multiple clients may have different letter assignments.")},
            {ChangeCategory::Feature, QStringLiteral("Network audio packet-loss concealment"), QStringLiteral("PLC algorithm fills brief network audio dropouts (1–3 packets) with spectrally-matched filler instead of silence; eliminates audible pops on lossy Wi-Fi.")},
            {ChangeCategory::Feature, QStringLiteral("Lock split-pair VFO panels to opposite sides"), QStringLiteral("Split-pair VFOs (A/B in split mode) now lock to opposite sides of the dual-VFO layout to prevent operator confusion.")},
            {ChangeCategory::Feature, QStringLiteral("Right-click context menu on CWX history bubbles"), QStringLiteral("CWX history bubbles get a right-click context menu for delete / resend / edit actions.")},
            {ChangeCategory::Feature, QStringLiteral("Cluster overflow popup emits spotTriggered"), QStringLiteral("Clicking a spot inside the cluster overflow popup now triggers the full `spotTriggered` flow (slice retune + spot record), not just the bare frequency move.")},
            {ChangeCategory::Feature, QStringLiteral("RF Gain and AGC-T keyboard shortcuts (#2710, jensenpat)"), QStringLiteral("Adds `rf_gain_up/down` and `agc_t_up/down` shortcut actions for keyboard and FlexControl mapping.")},
            {ChangeCategory::Feature, QStringLiteral("Interlock notification system"), QStringLiteral("Per-band interlock state changes (band-cancel, amp-warmup, ATU-busy) now route through a unified notification system rather than ad-hoc modal popups.")},
            {ChangeCategory::Feature, QStringLiteral("Smart Spot Filter match window"), QStringLiteral("User-configurable time window for the Smart Spot Filter — controls how long a matched spot's marker remains highlighted.")},
            {ChangeCategory::BugFix, QStringLiteral("Spectrum dBm range echo smoothing (#2793, rfoust)"), QStringLiteral("After releasing a dBm scale drag, in-flight FFT frames encoded for the old range no longer snap the trace back. Rebases up to 10 frames with a 0.75 dB median-improvement guard. Also splits auto-flo...")},
            {ChangeCategory::BugFix, QStringLiteral("Panadapter layout / floating persistence + macOS GPU lifecycle (#2786, #2780, rfoust)"), QStringLiteral("Panadapter layout state restores cleanly on radio reconnect across multi-pan and floating-pan window configurations. Bundles the macOS QRhi GPU lifecycle fix (reparenting on dock/float and clean sh...")},
            {ChangeCategory::BugFix, QStringLiteral("Extend panadapter startup layout restore window (#2792, rfoust)"), QStringLiteral("Restore window widened from 5 s to 30 s for slow reconnects, with a user-intent suppress flag so layout-button presses during the wider window don't get clobbered.")},
            {ChangeCategory::BugFix, QStringLiteral("Spectrum FFT line width slider produces visible pixel-width line"), QStringLiteral("Slider value previously produced sub-pixel-width strokes on some scale factors. Now floor-rounded to an integer pixel width before draw.")},
            {ChangeCategory::BugFix, QStringLiteral("M-message severity respected so Info notices don't pop modals"), QStringLiteral("The radio's M-message severity bits (Info / Notice / Warning / Error) were previously ignored; Info-level messages popped error modals. Now routed to the status bar for Info/Notice and the modal on...")},
            {ChangeCategory::BugFix, QStringLiteral("Parse hex meter num field + handle M-prefix radio messages (#2771, NF0T)"), QStringLiteral("Meter-number field was decimal-only, breaking on radios that send hex. Plus the `M`-prefix radio message form is now parsed correctly.")},
            {ChangeCategory::BugFix, QStringLiteral("Panadapter vanished panstack on left-dock startup"), QStringLiteral("Empty panstack on first launch when the applet panel was left-docked. Two related size-setter bugs (#2746 follow-up) addressed.")},
            {ChangeCategory::BugFix, QStringLiteral("XVTR waterfall offset matching (#2709, jensenpat)"), QStringLiteral("Waterfall display offset for XVTR slices now matches the spectrum offset on unvalidated profile entries.")},
            {ChangeCategory::BugFix, QStringLiteral("Spot auto-mode slice routing (#2713, jensenpat)"), QStringLiteral("Auto-mode spots now route to the correct slice based on mode-compatibility rather than always-slice-0.")},
            {ChangeCategory::BugFix, QStringLiteral("Spot tooltips disappearing near waterfall edge (#2740, M7HNF-Ian)"), QStringLiteral("Tooltips clipped past the waterfall edge are now anchored above the cursor.")},
            {ChangeCategory::BugFix, QStringLiteral("FFT floor lock edge cases (#2715, rfoust)"), QStringLiteral("Edge-case crashes in the FFT-floor-lock state machine resolved.")},
            {ChangeCategory::BugFix, QStringLiteral("XVTR direct-entry tuning above 450 MHz"), QStringLiteral("Direct frequency entry on XVTR slices accepted values above 450 MHz in display but the slice tune command rejected. Now passes through.")},
            {ChangeCategory::BugFix, QStringLiteral("Spot tooltip cursor offset + re-anchor suppression"), QStringLiteral("DX / band-plan tooltips offset from the cursor to avoid covering the spot label; suppresses re-anchor flicker during slow mouse moves.")},
            {ChangeCategory::BugFix, QStringLiteral("Spot label gap in spectrum (#2702, algojogacor)"), QStringLiteral("Inter-label spacing now enforced so adjacent spot labels don't overlap.")},
            {ChangeCategory::BugFix, QStringLiteral("TX/Mic Save button silently no-ops on existing names"), QStringLiteral("Profile Save with an existing name now shows a confirm dialog instead of failing silently.")},
            {ChangeCategory::BugFix, QStringLiteral("Layout: second size-setter also clobbered left-dock startup"), QStringLiteral("Follow-up to #2733; a second size-setter path was also clobbering the left-dock layout.")},
            {ChangeCategory::BugFix, QStringLiteral("Restore WAVE TX scope for digital audio"), QStringLiteral("Digital-mode TX (DIGU/DIGL/FT8/RTTY) now restores the WAVE-TX scope selection on reconnect.")},
            {ChangeCategory::BugFix, QStringLiteral("RADE auto-deactivate when slice mode changes externally (#2747, NF0T)"), QStringLiteral("When the radio (or another client) changes a RADE slice to a non- RADE mode, RADE now auto-deactivates client-side.")},
            {ChangeCategory::BugFix, QStringLiteral("Restore unconditional radeActivated(false) emit on RX applet (#2745, NF0T)"), QStringLiteral("RX applet's RADE state emit was elided in some teardown paths, leaving downstream consumers stale.")},
            {ChangeCategory::BugFix, QStringLiteral("CharGap between live-mode Pending entries in CwxLocalKeyer (#2754, NF0T)"), QStringLiteral("Successive Pending entries in the CWX live-mode buffer were concatenated without the inter-character gap.")},
            {ChangeCategory::BugFix, QStringLiteral("Suppress amp/tuner popup during normal TX"), QStringLiteral("Interlock notification popup was firing during normal TX engagement. Now gated on `tx_allowed` to fire only when the interlock actually blocks TX.")},
            {ChangeCategory::BugFix, QStringLiteral("ATU walk segment list per discrete-channel band"), QStringLiteral("ATU sweep was sampling continuous-band centers on discrete-channel bands (60 m). Now walks the segment list instead.")},
            {ChangeCategory::BugFix, QStringLiteral("wasVisible-guard on remaining setGeometry sites"), QStringLiteral("Four (plus three) more frameless-toggle setGeometry sites adopted the wasVisible-guard pattern to avoid first-show off-screen positions.")},
            {ChangeCategory::BugFix, QStringLiteral("SQL threshold line auto-hide restored"), QStringLiteral("Manual SQL threshold line auto-hides after 3 s of inactivity again.")},
            {ChangeCategory::BugFix, QStringLiteral("Waterfall pace TX/FFT-fallback rows to radio line_duration"), QStringLiteral("During TX or FFT-fallback, the waterfall now paces row insertion to the radio's reported `line_duration` instead of free-running.")},
            {ChangeCategory::BugFix, QStringLiteral("Spot dedup cluster echo of manually-posted spot"), QStringLiteral("A manually-posted spot is no longer re-displayed when the cluster echoes it back.")},
            {ChangeCategory::BugFix, QStringLiteral("Profile Manager 'next tab' wording"), QStringLiteral("Wording clarified from ambiguous \"next\" to specific tab name.")},
            {ChangeCategory::BugFix, QStringLiteral("TX timeout display in seconds, not milliseconds"), QStringLiteral("TX timeout indicator was showing milliseconds with a \"s\" unit.")},
            {ChangeCategory::BugFix, QStringLiteral("Tear down float window when dock-side icon clicked"), QStringLiteral("Re-docking a float window via the dock-side icon now tears down the float window cleanly.")},
            {ChangeCategory::BugFix, QStringLiteral("CW Int16 fallback for sidetone sink on Int16-only WASAPI devices"), QStringLiteral("CW sidetone now falls back to Int16 on WASAPI devices that don't support float32.")},
            {ChangeCategory::BugFix, QStringLiteral("Don't toggle radio dax flag on Windows from slice-mode changes"), QStringLiteral("Windows slice-mode-change side-effect that toggled the radio's DAX flag eliminated.")},
            {ChangeCategory::BugFix, QStringLiteral("Guard m_daxBridge ref in deactivateRADE()"), QStringLiteral("Null-guards added to the RADE deactivation path for Windows / no- PipeWire builds.")},
            {ChangeCategory::BugFix, QStringLiteral("RADE DAX stream lifecycle on macOS"), QStringLiteral("RADE DAX stream lifecycle on macOS now matches Linux/Windows behavior.")},
            {ChangeCategory::BugFix, QStringLiteral("macOS Bluetooth headset mic capture rate"), QStringLiteral("macOS Bluetooth headset mic capture now correctly negotiates the supported rate (was hardcoded to 48 kHz, breaking 16 kHz BT codecs).")},
            {ChangeCategory::BugFix, QStringLiteral("S-History voice signal carrier detection"), QStringLiteral("Voice signal carrier frequency detection improved — handles slightly off-tune voice signals.")},
            {ChangeCategory::BugFix, QStringLiteral("Dx-cluster strip non-printable control chars"), QStringLiteral("Cluster console lines now have control characters stripped before display.")},
            {ChangeCategory::BugFix, QStringLiteral("TCI spec/impl compliance + RF2K-S amplifier interop"), QStringLiteral("Several TCI v2.0 command spec divergences corrected; RF2K-S amplifier now interoperates correctly via TCI.")},
            {ChangeCategory::BugFix, QStringLiteral("Prevent remote_audio_rx stream on TCI autostart if PC audio disabled"), QStringLiteral("TCI autostart no longer creates a remote_audio_rx stream when PC audio is disabled in settings.")},
            {ChangeCategory::BugFix, QStringLiteral("Make set_split_vfo / tx_enable idempotent to stop radio TX watchdog"), QStringLiteral("These two CAT commands now idempotent — repeated state-equal calls no longer trip the radio's TX watchdog.")},
            {ChangeCategory::BugFix, QStringLiteral("Drive CWX/DVK F1-F12 shortcuts by active slice mode"), QStringLiteral("F-key shortcut routing now respects the active slice's mode.")},
            {ChangeCategory::BugFix, QStringLiteral("Frameless title bar drag on macOS"), QStringLiteral("macOS frameless title bar drag now uses the native window-drag API instead of synthetic mouse events.")},
            {ChangeCategory::BugFix, QStringLiteral("FPS meter overlay redraw stutter"), QStringLiteral("FPS meter overlay was redrawing every frame; now diff-gated.")},
            {ChangeCategory::BugFix, QStringLiteral("HID automoc source registration"), QStringLiteral("HID source files now correctly registered with automoc.")},
            {ChangeCategory::BugFix, QStringLiteral("macOS iconset 512@2x size"), QStringLiteral("macOS iconset 512@2x size corrected.")},
            {ChangeCategory::BugFix, QStringLiteral("TX mic stereo channel canonicalization"), QStringLiteral("Stereo mic channel order canonicalized to L=mic / R=mic across all platforms.")},
            {ChangeCategory::BugFix, QStringLiteral("Streamdeck tune-up/down step"), QStringLiteral("Streamdeck tune step bumped from 100 Hz to 1 kHz (matches main app step semantics).")},
            {ChangeCategory::BugFix, QStringLiteral("Consolidate duplicate aether.ax25 Q_LOGGING_CATEGORY"), QStringLiteral("The `aether.ax25` logging category was declared in two TUs; consolidated to a single declaration in `LogManager.cpp` with shared header access.")},
            {ChangeCategory::BugFix, QStringLiteral("dBm-scale Ctrl-drag zoom tooltip + help docs"), QStringLiteral("Companion to #2717; documents the gesture in tooltip + help.")},
            {ChangeCategory::BugFix, QStringLiteral("Log diagnostic when noise-floor capture bails"), QStringLiteral("New diagnostic log line when the noise-floor capture path returns early without producing a sample.")},
            {ChangeCategory::BugFix, QStringLiteral("ANT-tab slider default Qt border"), QStringLiteral("Restores the default Qt slider border on the ANT-tab sliders.")},
            {ChangeCategory::Infrastructure, QStringLiteral("PersistentDialog base class + showOrRaisePersistent migration sweep"), QStringLiteral("New `PersistentDialog` base class providing frameless-chrome support, geometry persistence, and lazy-construct/raise lifecycle via `showOrRaisePersistent<T>()`. Migrated dialogs include Profile Man...")},
            {ChangeCategory::Infrastructure, QStringLiteral("Migrate three secondary RadioSetupDialog entry points"), QStringLiteral("FlexControl, USB Cables, and XVTR overlay entry points now use `showOrRaisePersistent` and converge on the single tracked instance. Closes the duplicate-RadioSetup-window class of bug.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Migrate seven dialogs to PersistentDialog"), QStringLiteral("Bulk migration of Memory, NetworkDiagnostics, AetherDsp, MIDI, MQTT, SpotHub, and AX.25 Packet Decoder dialogs to the new base class.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Migrate PropDashboardDialog to PersistentDialog"), QStringLiteral("Companion to #2676.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Extract TxBandDialog to its own class + migrate to PersistentDialog"), QStringLiteral("TxBandDialog extracted from MainWindow.cpp inline construction into its own class file + base-class migration.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Log rotation + startup retention cap"), QStringLiteral("AsyncLogWriter now rotates the log file on size threshold and caps retained log files on startup.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Unit tests for AsyncLogWriter"), QStringLiteral("New test harness for AsyncLogWriter rotation, buffering, and shutdown behavior.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Unit tests for PerfTelemetry"), QStringLiteral("New test harness covering frame-time histogram, heartbeat capture, and aggregation.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Lock IARU R1 reference table for computeCenters()"), QStringLiteral("New test guards the IARU R1 reference table used for ATU center computation.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Make VirtualAudioBridge thread-safe for DAX RX DirectConnection"), QStringLiteral("Thread-safe accessors added to VirtualAudioBridge for the DAX RX DirectConnection path.")},
            {ChangeCategory::Infrastructure, QStringLiteral("CI post-merge guard + drop stale-branch pre-merge enforcement"), QStringLiteral("Adds a post-merge job that catches stale-snapshot reverts; removes the brittle pre-merge stale-branch check that was producing too many false positives.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Refresh in-repo URLs to aethersdr/AetherSDR org"), QStringLiteral("Sweep across docs, CI workflows, and source comments to update the GitHub org reference following the org move from `ten9876` → `aethersdr` (2026-05-16).")},
            {ChangeCategory::Infrastructure, QStringLiteral("Bundle zlib 1.3.1 under third_party/"), QStringLiteral("zlib now vendored to remove the system-dep variation across distros.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Add spec-kit constitution at .specify/memory/constitution.md"), QStringLiteral("Establishes the project's eight inviolable principles (FlexLib authority, MeterSmoother canonicality, UI-label-driven naming, region-aware band data, nested-JSON config, CHAIN-widget TX DSP entry, ...")},
            {ChangeCategory::Infrastructure, QStringLiteral("Frameless guidance for popout dialogs in CLAUDE.md"), QStringLiteral("Documents the canonical frameless pattern for popout dialogs.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Add frameless support for popout dialogs"), QStringLiteral("Foundational frameless chrome support for popout / floating dialogs (sets up the PersistentDialog work that followed).")},
            {ChangeCategory::Infrastructure, QStringLiteral("Document _tci._tcp.local mDNS discovery schema"), QStringLiteral("TCI mDNS discovery schema documented.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Document AetherSDR audio pipeline"), QStringLiteral("New docs page covering the full audio pipeline (TX mic path, RX audio path, DAX, RADE, TCI, AetherModem).")},
            {ChangeCategory::Infrastructure, QStringLiteral("Drop dead SHistorySoftEdgeDb settings key"), QStringLiteral("Removes an unused settings key + saves a migration step.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Add PC Audio device tooltip"), QStringLiteral("The PC Audio panel now shows a tooltip with the underlying device identifier on hover.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Remove dead startSystemMove branch in ContainerWidget"), QStringLiteral("Dead-code cleanup.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Remove dead drawFpsMeters() painter path"), QStringLiteral("Replaced by the overlay-based FPS meter from #2480.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Add aethersdr-wallpaper.png to docs"), QStringLiteral("Project wallpaper for marketing / community.")},
        }},
        {QStringLiteral("26.5.1"), QStringLiteral("2026-05-10"), QStringLiteral("1.0 — first stable release; CalVer cutover"), {
            {ChangeCategory::Feature, QStringLiteral("S-History v2 — voice signal detection + QRM classification"), QStringLiteral("New CNN classifier on the panadapter that distinguishes confirmed voice signals from QRM channels in the Signal History overlay. Voice markers age out independently of QRM markers (30 s after the l...")},
            {ChangeCategory::Feature, QStringLiteral("Connected Stations dialog when multiFLEX disabled"), QStringLiteral("Operators running with multiFLEX off can now see who else is on the radio via Help → Connected Stations. Lists every connected client with handle, program, station, and ownership state.")},
            {ChangeCategory::Feature, QStringLiteral("FPS meters + performance telemetry"), QStringLiteral("Footer FPS readouts for spectrum / waterfall plus a `PerfTelemetry::recordUiHeartbeat()` hook that captures the Qt event-loop cadence at 50 ms intervals. Feeds the future System Info diagnostics di...")},
            {ChangeCategory::Feature, QStringLiteral("8-axis edge resize for frameless main window"), QStringLiteral("The main window's frameless mode now supports the full eight-axis resize grip (N/S/E/W/NE/NW/SE/SW) matching the Aetherial Audio Channel Strip and other frameless dialogs.")},
            {ChangeCategory::Feature, QStringLiteral("SpotHub Display tab + tunable Signal History"), QStringLiteral("Consolidates the SpotHub display controls into a single tab with Signal History tunables (qualify duration, hide-after, suspect-QRM threshold). Sub-tabs replaced with grouped sections.")},
            {ChangeCategory::Feature, QStringLiteral("Frameless chrome on Memory Channels dialog"), QStringLiteral("The Memory Channels dialog gets the standard 18 px gradient title bar, grip glyph, drag-to-move, double-click-to-maximize, 8-axis resize — matching NetworkDiagnosticsDialog and the Channel Strip.")},
            {ChangeCategory::Feature, QStringLiteral("Add Memory action moved into MemoryBrowsePanel"), QStringLiteral("The \"Add memory\" button now lives in the MemoryBrowsePanel itself, closer to where users are looking when they want to capture the current slice. The slice-letter badge variant was dropped — adding...")},
            {ChangeCategory::Feature, QStringLiteral("Float popped-out applet panel as frameless project window"), QStringLiteral("Popping out the applet panel via the new title-bar icon (or Ctrl+Shift+S) now floats it as a frameless project window with the full chrome treatment: gradient title bar, drag handle, 8-axis resize....")},
            {ChangeCategory::Feature, QStringLiteral("Always-on-Top pin on popped-out applets"), QStringLiteral("Per-container pin on the popped-out applet header keeps the float above the panadapter window for instrument-on-instrument workflows.")},
            {ChangeCategory::Feature, QStringLiteral("FlexControl WheelRit / WheelXit modes"), QStringLiteral("Two new FlexControl USB knob assignment modes drive RIT and XIT offsets directly without touching the slice frequency.")},
            {ChangeCategory::Feature, QStringLiteral("Easter egg: Ctrl+Shift+A toggles starstruck pan-drag sound"), QStringLiteral("Hidden audio cue tied to pan dragging — off by default, no shortcut reservation, harmless when not enabled.")},
            {ChangeCategory::Feature, QStringLiteral("Status-bar +PAN icon redrawn as a jagged FFT trace"), QStringLiteral("Replaces the previous generic icon with a 30-point jagged FFT spectrum trace at 5 distinct peaks — matches the actual spectrum visual idiom AetherSDR is built around.")},
            {ChangeCategory::BugFix, QStringLiteral("Linux waterfall smear after TX (#2527, fixes #2527-class reports)"), QStringLiteral("10–18 second post-TX vertical-stripe smear on Linux only. Root cause: `WaterfallBlanker` retained a TX-era \"last good row\" across the TX→RX transition; subsequent impulse-detection substitutions pa...")},
            {ChangeCategory::BugFix, QStringLiteral("Slice reduction crash + float/dock GPU corruption + exit crash"), QStringLiteral("Three Multi-Flex / multi-pan crashes consolidated. Slice reduction on disconnect cascade tripped a use-after-free in `wirePanadapter()`. Float→dock transitions on QRhiWidget panes corrupted the spe...")},
            {ChangeCategory::BugFix, QStringLiteral("Spot `background_color` honored when override is off"), QStringLiteral("`SpotData::backgroundColor` was parsed from the FlexLib protocol and stored in the model, but dropped at the model→view boundary: `SpotMarker` didn't carry the field, the converter silently dropped...")},
            {ChangeCategory::BugFix, QStringLiteral("ALC gauge rewired to SW ALC meter; mirror across Phone + CW"), QStringLiteral("HWALC (RCA voltage telemetry) was driving the SmartSDR-equivalent ALC gauge, producing meaningless readings. Split into hwAlc / swAlc; the gauge now consumes the SW ALC meter (post-software-ALC SSB...")},
            {ChangeCategory::BugFix, QStringLiteral("DSP curve widgets: HarfBuzz reshape on every paint"), QStringLiteral("The DSP curve widgets (`ClientCompCurveWidget`, `ClientGateCurveWidget`, `ClientDeEssCurveWidget`) were calling `QPainter::drawText` for axis labels on every paint, forcing a full HarfBuzz reshape ...")},
            {ChangeCategory::BugFix, QStringLiteral("macOS app icon missing on local builds (#2558, jensenpat)"), QStringLiteral("`docs/AetherSDR.icns` was never committed (only generated inside the macos-dmg CI runner), so every local macOS dev build silently produced a generic-icon `.app`. Fixed by moving icns generation in...")},
            {ChangeCategory::BugFix, QStringLiteral("Stream Deck mute toggle TCI command"), QStringLiteral("The mute action on Stream Deck mappings sent the wrong TCI command. Corrected to the canonical mute/unmute primitive.")},
            {ChangeCategory::BugFix, QStringLiteral("DAX IQ silently broken on Windows / non-PipeWire Linux"), QStringLiteral("DAX IQ streams produced no output on Windows and on Linux systems without PipeWire (PulseAudio-only). Root cause: the IQ stream setup path assumed PipeWire native and silently returned without init...")},
            {ChangeCategory::BugFix, QStringLiteral("Windows ClangCL build failures"), QStringLiteral("ClangCL toolchain failures for NR4/specbleach and other modules resolved with target-specific include path adjustments.")},
            {ChangeCategory::BugFix, QStringLiteral("Memory recall radio state sync"), QStringLiteral("Recalling a memory entry now correctly syncs all radio state (mode, filter, slice settings) instead of just frequency.")},
            {ChangeCategory::BugFix, QStringLiteral("RADE mode not disabled on PTT release"), QStringLiteral("Mic metering during RADE TX could be interrupted if RADE auto- disabled on key-up. Now stays in RADE mode across PTT cycles.")},
            {ChangeCategory::BugFix, QStringLiteral("RX output panel rebuild with canonical meter ballistics"), QStringLiteral("Aetherial Audio Channel Strip's RX output panel rebuilt to use the canonical `MeterSmoother` ballistics (30 ms attack, 180 ms release at 120 Hz) — matches every other meter in the app.")},
            {ChangeCategory::BugFix, QStringLiteral("XVTR Flex band-stack key uses status index, not order"), QStringLiteral("Band-stack persistence keyed off the position-in-list of XVTR entries; if the radio reordered them, recall pulled the wrong entry. Now keys on the radio-supplied status index.")},
            {ChangeCategory::BugFix, QStringLiteral("Frameless float popout follows frameless setting"), QStringLiteral("Popped-out applets now respect the global \"use frameless windows\" preference — previously they were always system-decorated.")},
            {ChangeCategory::BugFix, QStringLiteral("MIDI toggle params + modeUp/Down + category filter"), QStringLiteral("Toggle MIDI params no longer auto-revert on note release. Adds `modeUp` / `modeDown` actions. Category filter in MIDI Learn expanded to cover every action group.")},
            {ChangeCategory::BugFix, QStringLiteral("RTTY squelch auto-disable"), QStringLiteral("Switching to RTTY mode now auto-disables squelch (squelch on RTTY notches characters out and breaks decoding).")},
            {ChangeCategory::BugFix, QStringLiteral("NR4 button disabled when libspecbleach unavailable"), QStringLiteral("Builds without `libspecbleach` left the NR4 button live but no-op — confusing operators about what NR4 actually does. Button is now disabled with a tooltip when the dependency is missing.")},
            {ChangeCategory::BugFix, QStringLiteral("CWX release TX when queue drains"), QStringLiteral("CWX macros could leave the radio stuck in TX if the queue drained without an explicit end-of-message marker. Now releases TX when the queue is empty.")},
            {ChangeCategory::BugFix, QStringLiteral("Audio mute restore on reconnect"), QStringLiteral("Reconnecting to the radio no longer restores per-slice audio mute state — that's a radio-authoritative setting per the Radio-Authoritative Settings Policy.")},
            {ChangeCategory::BugFix, QStringLiteral("QsoRecorder QTimer playback → QAudioSink pull mode"), QStringLiteral("QSO recorder playback used a `QTimer` to push audio buffers, producing audible glitches under high system load. Rewritten to `QAudioSink` pull mode for glitch-free playback.")},
            {ChangeCategory::BugFix, QStringLiteral("Slice capacity clamp to model limit"), QStringLiteral("Some radio models report a slice count higher than they actually support. Client now clamps to the model-specific limit.")},
            {ChangeCategory::BugFix, QStringLiteral("DVK F1-F12 shortcut conflict with CwxPanel"), QStringLiteral("F-key shortcuts triggered both DVK and CWX panels when both were visible. Resolved via panel-scoped shortcut routing.")},
            {ChangeCategory::BugFix, QStringLiteral("ShackSwitch button stale visibility"), QStringLiteral("Top-right tray's ShackSwitch button stayed visible after the ShackSwitch feature was disabled. Now toggles in lockstep.")},
            {ChangeCategory::BugFix, QStringLiteral("RX applet radeActivated guard"), QStringLiteral("`RxApplet` was missing the RADE-active guard that `VfoWidget` uses — could fire stale mode-change events while RADE was busy.")},
            {ChangeCategory::BugFix, QStringLiteral("Avoid unchanged spot overlay repaints"), QStringLiteral("Spot marker list was being rebuilt and repainted on every spot source update even when no visual change occurred. New `spotMarkersVisuallyEqual()` predicate gates the repaint.")},
            {ChangeCategory::BugFix, QStringLiteral("Reassert desired panadapter and waterfall display rates"), QStringLiteral("Radio reconnect could silently leave the panadapter and waterfall at the radio's default display rate (10 Hz) instead of the client's configured rate. Now reasserted on every reconnect.")},
            {ChangeCategory::BugFix, QStringLiteral("Windows 48 kHz L-R audio pan"), QStringLiteral("L-R audio panning was incorrect on the Windows 48 kHz resampler path. Restored to match the macOS / native-rate paths.")},
            {ChangeCategory::BugFix, QStringLiteral("Protocol: send client program/low_bw_connect before client gui"), QStringLiteral("Subscription order matters on the FlexLib side; sending `client program` and `client low_bw_connect` after `client gui` caused intermittent rejection of metadata. Reordered to match what SmartSDR-f...")},
            {ChangeCategory::BugFix, QStringLiteral("TCI volume/drive/rx_volume command dispatch"), QStringLiteral("Three TCI v2.0 commands had no dispatch wiring on AetherSDR's side. Added the three handlers + bidirectional state sync.")},
            {ChangeCategory::BugFix, QStringLiteral("Spectrum cursor update guards"), QStringLiteral("Spectrum cursor position updates fired without checking widget lifetime, producing rare null-deref crashes during layout changes.")},
            {ChangeCategory::BugFix, QStringLiteral("AmpApplet telemetry forwarding"), QStringLiteral("Amplifier telemetry from the radio (id, vac, meffa, temp) was parsed but not forwarded to `AmpApplet`. Wired through.")},
            {ChangeCategory::BugFix, QStringLiteral("Windows UI scaling settings path"), QStringLiteral("Pre-Qt settings read on Windows used the Linux path convention. Corrected to `%APPDATA%/AetherSDR/`.")},
            {ChangeCategory::BugFix, QStringLiteral("FreeDV Station Msg live-update without restart"), QStringLiteral("The Station Msg field in FreeDV settings required an app restart to take effect. Now live-updates the reporter.")},
            {ChangeCategory::BugFix, QStringLiteral("Container pop-out glyph + size"), QStringLiteral("Switches the pop-out glyph to `⧉` at a larger size for discoverability.")},
            {ChangeCategory::BugFix, QStringLiteral("Minimal mode hides dock-side icons"), QStringLiteral("The three dock-side selectors (left dock, right dock, pop-out) and their separator are now hidden in minimal mode — they have no meaning when the spectrum is hidden.")},
            {ChangeCategory::BugFix, QStringLiteral("View → Applet Panel / Pop Out menu entries removed"), QStringLiteral("Both menu entries are redundant with the new title-bar dock-side icons (#2536) and the Ctrl+Shift+S window-scoped shortcut.")},
            {ChangeCategory::BugFix, QStringLiteral("Reconnect dialog chrome polish"), QStringLiteral("The reconnect dialog gets the frameless gradient title bar and proper chrome to match the rest of the app.")},
            {ChangeCategory::BugFix, QStringLiteral("`_platform_memmove` cost in macOS backing-store flush"), QStringLiteral("Two related claims about Qt's widget-RHI fallback cost on macOS (#2547) and 30 Hz hidden-widget timer storms (#2548). Implemented the #2548 fix in full (~150 LOC, 10 widgets timer-gated on visibili...")},
            {ChangeCategory::Infrastructure, QStringLiteral("Buffered async file logging"), QStringLiteral("AetherSDR's log file path now flushes via a buffered async writer to avoid blocking the GUI thread on slow disks (especially network shares on macOS).")},
            {ChangeCategory::Infrastructure, QStringLiteral("FPS meters + perf telemetry framework"), QStringLiteral("Foundation for the System Info diagnostics dialog (#2554) — see Features above.")},
            {ChangeCategory::Infrastructure, QStringLiteral("actions/cache bump 4 → 5"), QStringLiteral("Dependabot bump for the GitHub Actions cache action.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Qt 6.7+ GPU rendering requirement documented"), QStringLiteral("README now documents the minimum Qt version for GPU spectrum rendering. Older Qt falls back to CPU rasterization.")},
            {ChangeCategory::Infrastructure, QStringLiteral("README supported hardware refresh"), QStringLiteral("Aurora 510M, RT-series, ML-series, CL-series additions to the Supported Hardware list. AU-520 confirmed working.")},
            {ChangeCategory::Infrastructure, QStringLiteral("libopengl0 build dep documented"), QStringLiteral("Ubuntu/Debian build instructions now list `libopengl0` explicitly.")},
            {ChangeCategory::Infrastructure, QStringLiteral("1.0 release prep — stale reference sweep"), QStringLiteral("Sweep across CLAUDE.md, README.md, CHANGELOG.md, and `docs/architecture-pipelines.md` removing pre-1.0 framing and TODO breadcrumbs.")},
        }},
        {QStringLiteral("0.9.8"), QStringLiteral("2026-05-06"), QStringLiteral("Aetherial Audio Channel Strip RX + community-driven reliability sweep"), {
            {ChangeCategory::Feature, QStringLiteral("Aetherial Audio Channel Strip — RX side"), QStringLiteral("The Channel Strip now has a fully-built RX path alongside the TX path. RX side mirrors the TX layout: AGC-G (formerly AGC-T), EQ, De-Esser, Compressor, Tube/Saturator, PUDU, Final Output, and Wavef...")},
            {ChangeCategory::Feature, QStringLiteral("Editable CW Delay / Speed / Sidetone / Pitch fields (#2429, chibondking)"), QStringLiteral("The four CW value labels in the PhoneCwApplet are now `QLineEdit` widgets. Click any value and type a number directly — SmartSDR parity. `QIntValidator` clamps to the valid range, `editingFinished`...")},
            {ChangeCategory::Feature, QStringLiteral("Hamlib NET-rigctl / Not1MM interoperability (#2438, jensenpat, closes #2048 #2108)"), QStringLiteral("Pipe separator (`|`) — rigctld's wire convention. AetherSDR only recognised `;`, so every pipe-prefixed command returned `RPRT -4`. Cherry-picks the orphaned PR #2051 fix from @ct1drb with full att...")},
            {ChangeCategory::Feature, QStringLiteral("AetherDSP Settings dialog refit + VFO DSP launchers"), QStringLiteral("AetherDSP Settings dialog gets the standard frameless 18 px gradient title bar (matching NetworkDiagnosticsDialog and AetherialAudioStrip) with grip glyph, min/max/close trio, drag-to-move, double-...")},
            {ChangeCategory::BugFix, QStringLiteral("Spot client thread affinity Windows crash (#2420, jensenpat, fixes #1929 #2418)"), QStringLiteral("`QTcpSocket` / `QUdpSocket` / `QWebSocket` create an internal `QSocketNotifier` whose Win32 message-loop affinity binds to the *construction* thread, not the QObject's current thread. Constructing ...")},
            {ChangeCategory::BugFix, QStringLiteral("Audio callbacks during shutdown (#2413, rfoust)"), QStringLiteral("Quit-time crash on macOS where `WanConnection::~WanConnection` triggered a TLS disconnect cascade that emitted `radioTransmittingChanged(false)` after `m_audio` was already null but before `QObject...")},
            {ChangeCategory::BugFix, QStringLiteral("Hardware PTT into radio with mic_selection=PC (#2431, jensenpat, fixes #2373 #2200)"), QStringLiteral("The interlock gate was forcing `setTransmitting(false)` whenever `m_txRequested` was false — even when the radio reported our handle as TX owner. Hardware-keyed PTT (mic-line, ACC footswitch, RCA T...")},
            {ChangeCategory::BugFix, QStringLiteral("UI Scale not applied on Windows; reset to 100% silently ignored (#2432, chibondking)"), QStringLiteral("Two unrelated bugs in `main.cpp`'s pre-Qt settings read. (1) The pre-Qt path was hardcoded to `~/.config/AetherSDR/` (Linux convention); on Windows AppSettings actually writes to `%APPDATA%/AetherS...")},
            {ChangeCategory::BugFix, QStringLiteral("macOS relaunch via `open -n` on UI Scale restart (#2434, chibondking)"), QStringLiteral("`applyUiScale()` was calling `QProcess::startDetached` on `applicationFilePath()`, which on a `.app` bundle launches `Foo.app/Contents/MacOS/Foo` directly — bypassing Launch Services. The relaunche...")},
            {ChangeCategory::BugFix, QStringLiteral("MIDI jog-wheel VFO tuning stabilization (#2422, s53zo, fixes #2421)"), QStringLiteral("Three independent failure modes on touch-sensitive jog wheels (e.g. Hercules DJControl Starlight). MIDI Learn could bind the touch sensor instead of the movement CC. Saved bindings without the rela...")},
            {ChangeCategory::BugFix, QStringLiteral("VFO filter width indicator showing 0.1 kHz too low (#2435, jensenpat, fixes #2197)"), QStringLiteral("`updateFilterLabel()` computed raw `filterHigh - filterLow`, which for SSB/digital modes is ~100 Hz less than the named preset width due to the intentional low-cut. Bug had been reported three time...")},
            {ChangeCategory::BugFix, QStringLiteral("Mode-correct filter widen/narrow shortcuts (#2436, jensenpat, fixes #2208)"), QStringLiteral("The `filter_widen` / `filter_narrow` shortcuts (also bound to MIDI `global.filterWiden` / `global.filterNarrow`) only adjusted the upper edge by ±100 Hz. On LSB / CWL / DIGL / RTTY the upper edge i...")},
            {ChangeCategory::BugFix, QStringLiteral("PGXL OPERATE/STANDBY button stuck on OPERATE (#2437, chibondking)"), QStringLiteral("AmpApplet button was only updated via the direct PGXL TCP path; if that lagged, the button stayed on the old label. The click handler reads `m_operateBtn->text()` to decide toggle direction, so a s...")},
            {ChangeCategory::BugFix, QStringLiteral("Status bar separator dots for unsupported optional sections (#2412, rfoust)"), QStringLiteral("TGXL and PGXL separator dots stayed visible even when the indicators were hidden, leaving stray `· ·` clusters in the bottom status bar on radios without those accessories. Optional separators now ...")},
            {ChangeCategory::BugFix, QStringLiteral("cwDelay optimistic-update prevents Delay slider reset (#2428, chibondking)"), QStringLiteral("`setCwDelay()` was the lone outlier in the CW setter family — didn't cache `ms` into `m_cwDelay` before emitting the command. Subsequent `phoneStateChanged` emissions read stale `m_cwDelay` and sna...")},
            {ChangeCategory::BugFix, QStringLiteral("DSP-level slider missing on launch"), QStringLiteral("The leveled DSP toggles (NR / NB / ANF / NRL / NRS / NRF / ANFL) push onto a target stack so the shared 4th-row slider re-targets the most recently activated DSP. State changes arriving from radio ...")},
            {ChangeCategory::BugFix, QStringLiteral("Channel Strip initial height on sub-4K displays"), QStringLiteral("The Aetherial Audio Channel Strip opened at its natural 1620 px height on every display. On 1080p the bottom edge landed offscreen with the resize grip unreachable. Queries `QGuiApplication::primar...")},
            {ChangeCategory::Infrastructure, QStringLiteral("CI runtime + reliability sweep"), QStringLiteral("Skip CodeQL on doc-only PRs (#2397). Cache Windows third-party deps — Opus, FFTW, hidapi, DeepFilterNet (#2398) — drops cold Windows builds by ~3 min. Mirror Opus download to GitHub Releases with r...")},
        }},
        {QStringLiteral("0.9.7"), QStringLiteral("2026-05-05"), QStringLiteral("CW keying overhaul + reliability sweep"), {
            {ChangeCategory::Feature, QStringLiteral("CW keyboard + MIDI controls (#2361, #2391, jensenpat)"), QStringLiteral("Three new shared shortcut + MIDI actions: `Trigger straight key`, `Trigger CW Left Paddle`, `Trigger CW Right Paddle`. Same action IDs and display names for keyboard shortcuts and MIDI mappings (`c...")},
            {ChangeCategory::Feature, QStringLiteral("Quindar tones — Apollo-era K/BK on PTT (#2334, fixes #2262)"), QStringLiteral("Optional 2525 Hz \"K\" / 2475 Hz \"BK\" tones on PTT engage / disengage, modeled after Apollo CapCom audio. Lock-free DSP module wired between PC mic gain and the final TX limiter so tones are mixed in...")},
            {ChangeCategory::Feature, QStringLiteral("netCW keying fix + trace logging (#2336, fixes a long-tail of issues)"), QStringLiteral("Fixes netCW timing for both straight-key and iambic CW. Adds detailed `aether.cw` trace category logging across MIDI / keyboard input, netCW UDP scheduling, VITA stream/index details, and TCP fallb...")},
            {ChangeCategory::Feature, QStringLiteral("Spot Lines toggle in SpotHub (#2349, M7HNF-Ian)"), QStringLiteral("New \"Spot Lines\" toggle in the SpotHub Display settings draws vertical lines from the spectrum up to each spot label. Off during contests to reduce visual clutter; on for casual ops to keep label-t...")},
            {ChangeCategory::Feature, QStringLiteral("Network Diagnostics live logs tab"), QStringLiteral("New tab in the Network Diagnostics dialog with live tail of the same logs the diagnostics report references, scoped to the diagnostic categories (`aether.connection`, `aether.dxcluster`, `aether.cw...")},
            {ChangeCategory::Feature, QStringLiteral("Connect-to-last-radio opt-out checkbox (#2390, filemakers)"), QStringLiteral("New \"Connect to last radio on start up\" checkbox on the connection dialog; defaults to ON so existing users keep current behavior. When unchecked, AetherSDR no longer auto-connects on startup, on b...")},
            {ChangeCategory::BugFix, QStringLiteral("SpotHub auto-reconnect after failed connection attempt (#2394, M7HNF-Ian, fixes #2380)"), QStringLiteral("SpotHub (DX cluster / RBN) didn't automatically reconnect after a Wi-Fi drop or any failed connection attempt — Qt's `QAbstractSocket` only emits `disconnected()` on Connected → Unconnected transit...")},
            {ChangeCategory::BugFix, QStringLiteral("TCI crash on quit when TciServer outlives RadioModel (#2386, M7HNF-Ian, fixes #2385)"), QStringLiteral("`TciServer` was constructed as a `QObject` child of `MainWindow`, so Qt deleted it during `~QWidget::deleteChildren()` — which runs *after* `MainWindow`'s value members (including `m_radioModel`) h...")},
            {ChangeCategory::BugFix, QStringLiteral("Waterfall unfreezes on radio interlock state, not MOX edge (#2368, fixes #1927)"), QStringLiteral("Waterfall freeze/unfreeze previously gated on the local MOX edge, which fired the instant the user released PTT — the radio kept streaming TX-contaminated tiles for the `UNKEY_REQUESTED` window, an...")},
            {ChangeCategory::BugFix, QStringLiteral("Honor cw break_in for keyboard CW keying"), QStringLiteral("`RadioModel::sendCwKey` and the iambic keyer's `onPaddleEvent` both unconditionally wrapped each key/squeeze in a `cw ptt 1`...`cw ptt 0` envelope. Made break-in OFF a no-op (auto-PTT forced TX any...")},
            {ChangeCategory::BugFix, QStringLiteral("Spot list double-click switches mode (#2372, fixes #2298)"), QStringLiteral("Double-clicking a spot in the Spot List now switches the radio's mode along with the frequency. Previously only the frequency moved, leaving the user in the wrong mode for the spotted signal.")},
            {ChangeCategory::BugFix, QStringLiteral("Spot trigger includes pan= for external CAT clients (#2369, fixes #2366)"), QStringLiteral("The spot click protocol now includes `pan=<panId>` so external CAT clients (N1MM Logger, etc.) consuming AetherSDR's spot triggers can route the click to the correct panadapter in multi-pan setups.")},
            {ChangeCategory::BugFix, QStringLiteral("Compression meter gates on radio TX state (#2363, jensenpat)"), QStringLiteral("The TX compression meter now displays only while the radio reports `state=TRANSMITTING`. Previously it could read live during RX from stale interlock state, confusing operators about what compressi...")},
            {ChangeCategory::BugFix, QStringLiteral("Voltage gauge label shows live radio voltage (#2362, chibondking)"), QStringLiteral("The voltage gauge label now reflects the live voltage value reported by the radio instead of the static \"VOLTS\" placeholder.")},
            {ChangeCategory::BugFix, QStringLiteral("Minimal mode revert on macOS when entered while maximized (#2367, fixes #2365)"), QStringLiteral("Entering minimal mode while the window was already maximized on macOS would revert to a non-minimal layout immediately on toggle. Layout / state ordering corrected so the entry sticks regardless of...")},
            {ChangeCategory::BugFix, QStringLiteral("MidiControlManager: drop dead paramAction signal"), QStringLiteral("Cleanup follow-up after #2336 introduced `paramActionTrace` — `paramAction` was still emitted but had zero connectors. Removed the orphan signal + matching declaration.")},
            {ChangeCategory::Infrastructure, QStringLiteral("DAX RX native pw_stream source on Linux (#2312, fixes #1008)"), QStringLiteral("DAX RX latency on Linux drops from ~400 ms → ~200 ms via a native PipeWire `pw_stream` source path, replacing the previous PulseAudio client. Brings DAX RX latency in line with macOS / Windows.")},
            {ChangeCategory::Infrastructure, QStringLiteral("RADE TX policy naming + tests (#2353, NF0T, fixes #2343)"), QStringLiteral("Renames the RADE TX policy enum to align with the issue tracking language (`HostedDaxBridge` etc.) and adds unit tests covering each `(reason, platform, mode) → (allowed, note)` decision row.")},
            {ChangeCategory::Infrastructure, QStringLiteral("HiDPI gauge clipping (#2346, M7HNF-Ian)"), QStringLiteral("HGauge tick labels and TxApplet value labels were clipping on HiDPI displays. Layout / paint regions updated to honor device-pixel ratio.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Windows portable ZIP includes MSVC runtime (#2364, jensenpat)"), QStringLiteral("Windows portable ZIP now bundles the MSVC runtime DLLs so the portable build runs on systems without Visual C++ Redistributable installed — matches the experience of the installer build.")},
            {ChangeCategory::Infrastructure, QStringLiteral("Connection-panel slider focus lease split"), QStringLiteral("`shortcutInputCaptured()` was conflating \"slider has focus and arrow-key lease is active\" with \"user is typing in a text field\", blocking momentary CW shortcuts and Space PTT during slider drags. N...")},
        }},
        {QStringLiteral("0.9.6"), QStringLiteral("2026-05-04"), QStringLiteral("Aetherial Audio Channel Strip + AetherSweep Phase 2"), {
            {ChangeCategory::Feature, QStringLiteral("Aetherial Audio Channel Strip"), QStringLiteral("New unified TX DSP window covering every stage in the chain: gate, EQ, compressor, de-esser, tube, AetherVoice (the exciter formerly known as PooDoo), Freeverb, plus a brand-new Final Output Stage ...")},
            {ChangeCategory::Feature, QStringLiteral("AetherSweep Phase 2 (#2320, jensenpat)"), QStringLiteral("Logarithmic SWR scale for cleaner low-ratio detail; threshold-band shading for SWR 1.0–1.5 (green), 1.5–2.0 (amber), and >2.0 (red). Resonance caret + dot at the best measured SWR point; visual sta...")},
            {ChangeCategory::Feature, QStringLiteral("Network Diagnostics trend graphs (#2309, rfoust; #2316)"), QStringLiteral("Per-metric trend plots with Timeframe selector (1 min / 5 min / 15 min / 1 h / 1 d / 1 w). Logarithmic Y-axis on the Rates tab — 0 / 1 / 10 / 100 / 1k kbps decades all visible at once instead of be...")},
            {ChangeCategory::Feature, QStringLiteral("RADE in the official Windows installer (#2324, NF0T)"), QStringLiteral("`ENABLE_RADE=ON` for both the Windows CI job and the official installer workflow.  Vendored RADE-prepared Opus + neural-net weights are statically linked, so no new DLLs ship — `AetherSDR.exe` grow...")},
            {ChangeCategory::Feature, QStringLiteral("Connect by IP recents dropdown (#2296, jensenpat)"), QStringLiteral("Connect-by-IP field is now a combo box pre-populated with the last five addresses (stored as `RecentDirectIpAddresses`).  Successful connect promotes the address to the top of the list.")},
            {ChangeCategory::Feature, QStringLiteral("Black slider auto-offset"), QStringLiteral("Spectrum Overlay menu's Black slider now drives a noise-floor target offset while AUTO is engaged: 50 = at the noise floor (today's behaviour); lower = darker (push threshold above the floor); high...")},
            {ChangeCategory::Feature, QStringLiteral("Minimal mode polish (#2290, jensenpat; #2299)"), QStringLiteral("Title bar drag now reaches the gutter and other previously-dead zones via sub-pixel hit testing.  Exit paths from minimal mode consolidated; layout stops fighting on toggle.")},
            {ChangeCategory::Feature, QStringLiteral("Aetherial Noise Reduction docked applet"), QStringLiteral("Client-side NR controls live in their own docked applet under PooDoo Audio (RX); the redundant DSP sub-panel was removed from the Spectrum Overlay menu for a cleaner DSP surface.")},
            {ChangeCategory::BugFix, QStringLiteral("Oscillator reference status no longer goes stale (#2329, jensenpat)"), QStringLiteral("New `RadioModel::oscillatorChanged()` signal drives the status-bar reference label and Radio Setup combo immediately from oscillator state, instead of relying on event ordering with GPS status. Eli...")},
            {ChangeCategory::BugFix, QStringLiteral("RADE mic level meter + gain slider (#2292, NF0T)"), QStringLiteral("The RADE TX mic level meter now updates while the modem is active, and the gain slider is enabled instead of stuck at zero.  Fixes \"no apparent way to set RADE mic level\" reports.")},
            {ChangeCategory::BugFix, QStringLiteral("Stderr non-draining pipe deadlock (#2300, AetherClaude)"), QStringLiteral("When stdout/stderr was redirected to a pipe with no reader, the audio worker thread could deadlock writing log lines.  Replaced blocking writes with a non-blocking path that drops on the floor rath...")},
            {ChangeCategory::BugFix, QStringLiteral("Windows installer runtime packaging (#2303, jensenpat)"), QStringLiteral("Tightened Windows runtime-DLL bundling for a smaller installer and fewer \"missing DLL\" reports on first launch.")},
            {ChangeCategory::Infrastructure, QStringLiteral("v0.9.5.1 release-notes expansion"), QStringLiteral("The CHANGELOG entry for v0.9.5.1 was expanded post-release to cover all post-v0.9.5 fixes (#2113 reachability sweep, the four rfoust polish PRs, NR2 audio-thread + Qt-log hotfixes) with full contri...")},
        }},
        {QStringLiteral("0.9.5.1"), QStringLiteral("2026-05-02"), QStringLiteral(""), {
            {ChangeCategory::BugFix, QStringLiteral("TCI TX silent on Windows / Linux non-PipeWire"), QStringLiteral("`evaluateDaxTxPolicy()` now always allows `DaxTxRequestReason::TciTxAudio` regardless of platform / hosted-DAX availability.  TCI receives audio over WebSocket and feeds it into a dedicated `dax_tx...")},
            {ChangeCategory::BugFix, QStringLiteral("SmartLink reconnect after WAN drop (#2282, rfoust)"), QStringLiteral("`MainWindow` now owns a WAN reconnect timer that re-requests a SmartLink radio connection using the last selected WAN radio when `RadioModel` reports an unexpected WAN disconnect, instead of leavin...")},
            {ChangeCategory::BugFix, QStringLiteral("SmartLink disconnect teardown (#2278, rfoust)"), QStringLiteral("WAN disconnect previously closed the WAN socket *after* disconnecting RadioModel's signals and nulled `m_wanConn` directly, skipping `RadioModel::onDisconnected()` cleanup entirely.  Panadapters, s...")},
            {ChangeCategory::BugFix, QStringLiteral("Reset RX slice tabs on disconnect between radios with different slice counts (#2254, rfoust)"), QStringLiteral("`RxApplet::clearSliceButtons()` tears down generated slice tab buttons and restores the static slice badge on disconnect.  Stale A–H buttons no longer linger after switching from a high-slice radio...")},
            {ChangeCategory::BugFix, QStringLiteral("Qt log handler serialization fixes macOS tune-time crash (#2284, jensenpat)"), QStringLiteral("The global `qInstallMessageHandler` callback wrote through a single `QFile*` without synchronization, and concurrent `qCInfo`/`qDebug` output from main + worker threads corrupted Qt's internal file...")},
            {ChangeCategory::BugFix, QStringLiteral("macOS panadapter pop-out refresh + multi-pan dock layout (#2280, rfoust)"), QStringLiteral("Detached panadapter windows on macOS no longer show a static/stale spectrum image.  Cross-window reparenting now resets the native QRhi/Metal surface and re-requests pan dimensions from the radio a...")},
            {ChangeCategory::BugFix, QStringLiteral("NR2 wisdom generation no longer freezes the audio thread (#2275, jensenpat)"), QStringLiteral("`AudioEngine::needsWisdomGeneration()` previously only checked whether `aethersdr_fftw_wisdom` existed.  If the file was stale or incompatible (e.g. `fftw-3.3.10` header on a build that uses FFTW 3...")},
        }},
        {QStringLiteral("0.9.5"), QStringLiteral("2026-05-02"), QStringLiteral("Reliability sweep + DAX2 coexistence overhaul"), {
            {ChangeCategory::Feature, QStringLiteral("Aetherial Parametric EQ Smoothing combo"), QStringLiteral("New Smoothing combo on the EQ analyzer with 1/24, 1/12, 1/6, 1/3, 1/1 octave fractional smoothing options (linear-power averaging). Persists via `EqAnalyzerSmoothing`; defaults to 1/12 octave. Audi...")},
            {ChangeCategory::Feature, QStringLiteral("Custom filter edges with persistence"), QStringLiteral("Right-click a filter button → **Set Custom Edges...** opens a 2-spinbox dialog.  Asymmetric `lo:hi` pairs persist in `FilterPresets_<mode>` via a new mixed `width` / `lo:hi` storage format (backwar...")},
            {ChangeCategory::Feature, QStringLiteral("Spot click client-side mode mapping"), QStringLiteral("All client-ingested spots (DXCluster, RBN, POTA, SpotCollector, FreeDV, WSJT-X, manual) now ship with `trigger_action=none`.  The radio no longer auto-acts on stored mode strings (which mishandled ...")},
            {ChangeCategory::Feature, QStringLiteral("ATU per-frequency toggle"), QStringLiteral("ATU button now mirrors SmartSDR's behaviour: first click on a frequency tunes; second click at the same frequency bypasses; freq change resets the toggle so the next click tunes again.")},
            {ChangeCategory::BugFix, QStringLiteral("DAX2 coexistence policy (#2271, jensenpat)"), QStringLiteral("New `DaxTxPolicy.h` decision table: `(reason, platform, mode) → (allowed, note)`.  Centralizes platform-aware DAX TX decisions with explicit reasons (HostedDaxBridge, TciTxAudio, ExternalDaxRouteOn...")},
            {ChangeCategory::BugFix, QStringLiteral("Sequenced radio disconnect teardown (#2247, jensenpat)"), QStringLiteral("Fixes Flex-6300 / 6400 unreachability after reconnect (#2113, #2218). Disconnect now sends `stream remove 0x<id>` and waits up to 2 s for the radio's response before closing TCP.  Without this, fir...")},
            {ChangeCategory::BugFix, QStringLiteral("Worker object shutdown thread affinity (#2248, rfoust)"), QStringLiteral("Eliminates `QObject::killTimer: Timers cannot be stopped from another thread`, `QObject::moveToThread: Current thread is not the object thread`, and `~QObject` warnings on app exit. Worker-thread Q...")},
            {ChangeCategory::BugFix, QStringLiteral("Panadapter zoom keeps spectrum visible (#2246, rfoust)"), QStringLiteral("New `reprojectSpectrum()` mirrors the existing waterfall reprojection. Zoom changes that overlap with the previous range now interpolate existing FFT bins forward instead of clearing them, eliminat...")},
            {ChangeCategory::BugFix, QStringLiteral("TGXL detected via direct TCP only (#2250, chrisb1964)"), QStringLiteral("TUN applet now appears for TGXLs that don't report through the amplifier API (Flex-8600 fw 4.2.18).  New `m_directPresence` fallback in `TunerModel`; `isPresent()` returns true via either path. Han...")},
            {ChangeCategory::BugFix, QStringLiteral("RX slice tab capacity initialization (#2243, chibondking)"), QStringLiteral("RX slice tab capacity is now seeded from the radio model on connect rather than left at the default — fixes empty tabs when reconnecting to a different radio model.")},
            {ChangeCategory::BugFix, QStringLiteral("Radio model label refresh (#2244, jensenpat)"), QStringLiteral("Radio model label refreshes on `info` command response, so reconnecting to a different model immediately reflects the new identity in the UI.")},
            {ChangeCategory::BugFix, QStringLiteral("FreeDV Reporter OS string (#2269, tmiw)"), QStringLiteral("OS field lowercased to match the official FreeDV client convention. Previously AetherSDR users showed as \"other\" in monthly platform reports.  Thanks Mooneer.")},
            {ChangeCategory::BugFix, QStringLiteral("FreeDV Reporter checkbox visibility (#2268, NF0T)"), QStringLiteral("Three checkboxes in the FreeDV Reporter tab were rendering with no visible borders against the dark background.  Applied the standard `ConnectionPanel` checkbox style.")},
            {ChangeCategory::BugFix, QStringLiteral("AppletPanel scrollbar (#2257, Chaosuk97)"), QStringLiteral("AppletPanel scrollbar widened from 6 px to 12 px with a dim handle at rest that brightens to `#4a6880` on hover or drag.  500 ms delay before dimming back so quick scroll gestures don't flicker. Al...")},
            {ChangeCategory::BugFix, QStringLiteral("Sweep 1 — 7 small UI / contrast fixes"), QStringLiteral("`AetherSDR vX.Y.Z` shown again in the in-app title bar — restored after the frameless-window mode regression (#2027). FlexControl knob press tokens are bare `S` / `C` / `L`, not `X4S` / `X4C` / `X4...")},
            {ChangeCategory::BugFix, QStringLiteral("Sweep 2 — 5 small bug fixes"), QStringLiteral("Double-clicking an off-screen slice indicator (`<A` / `A>`) recenters cleanly without retuning to a wrong frequency (#2237).  Qt's press → release → double-click → release sequence was leaving `m_s...")},
        }},
        {QStringLiteral("0.9.4"), QStringLiteral("2026-05-01"), QStringLiteral("AetherSweep, ShackSwitch, and multi-client startup hardening"), {
            {ChangeCategory::Feature, QStringLiteral("AetherSweep — in-panadapter SWR analyzer (#2202, #2220, #2230, jensenpat)"), QStringLiteral("New Start/Clear Sweep buttons under the ANT slice menu, plus a 1-10 W sweep-power slider with cross-panel sync.  Persisted as `SwrSweepPowerWatts` and defaults to 1 W. Walks the current TX band ste...")},
            {ChangeCategory::Feature, QStringLiteral("ShackSwitch antenna switch integration (#2214, #2227, nigelfenton)"), QStringLiteral("New **Peripherals** tab in Radio Setup with auto-discovery and manual-IP connect for ShackSwitch devices via the Antenna Genius (AG) UDP/TCP protocol on port 9007. New **ShackSwitchApplet** — compa...")},
            {ChangeCategory::BugFix, QStringLiteral("Multi-client startup panadapter creation (#2222, jensenpat)"), QStringLiteral("AetherSDR's startup pan creation now holds up cleanly when SmartSDR, DAX, or another GUI/audio client is already connected.  The radio replays status for all current pans/slices/streams on connect;...")},
            {ChangeCategory::BugFix, QStringLiteral("PC audio remote stream ownership (#2226, jensenpat)"), QStringLiteral("Fixes PC Audio failure when SmartSDR / DAX is already running — the shared `remote_audio_rx` pipe was being silently removed because AetherSDR was parsing the `stream create` response body as decim...")},
            {ChangeCategory::BugFix, QStringLiteral("TGXL meter goes silent during bypass"), QStringLiteral("AetherSweep aborted with \"no fresh TGXL SWR meter data\" whenever the TGXL was in OPERATE before sweep start.  The TGXL stops emitting `RL` (return-loss) meter packets while in BYPASS — bypass relay...")},
            {ChangeCategory::BugFix, QStringLiteral("Pop-out applet panel white background on macOS (#2190, Chaosuk97)"), QStringLiteral("macOS DMG builds rendered floating windows with a white background even though the dark-theme stylesheet was applied — the CI-built `libqcocoa.dylib` enforced `Qt::WA_StyledBackground` more strictl...")},
            {ChangeCategory::BugFix, QStringLiteral("Windows build without Qt6::SerialPort (#2195, NF0T)"), QStringLiteral("Compile error introduced by #2147: the `<QElapsedTimer>` include was guarded by `#ifdef HAVE_SERIALPORT` while `m_debounceTimer` is declared under `#if defined(HAVE_SERIALPORT) || defined(Q_OS_WIN)...")},
            {ChangeCategory::BugFix, QStringLiteral("NRL DSP filter visible on 6000-series radios"), QStringLiteral("Fixes regression from #2184 where NRL was incorrectly grouped with the 8000-series-only firmware DSP filters (NRS, RNN, NRF).  NRL is available on 6000-series too — only NRS/RNN/NRF require BigBend...")},
        }},
        {QStringLiteral("0.9.3"), QStringLiteral("2026-04-30"), QStringLiteral("External APD, FreeDV Reporter, Slice Colors, and v4.2 firmware updater"), {
            {ChangeCategory::Feature, QStringLiteral("External APD"), QStringLiteral("New \"APD\" tab in Radio Setup with per-TX-antenna external sampler selection (ANT1 / ANT2 / XVTA / XVTB → INTERNAL / RX_A / RX_B / XVTA / XVTB).  Tab is hidden unless the radio reports `apd configur...")},
            {ChangeCategory::Feature, QStringLiteral("FreeDV Reporter station reporting with RADE integration (#2173, NF0T)"), QStringLiteral("Adds FreeDV Reporter (https://qso.freedv.org) station-reporting support driven by the RADE modem's sync / SNR / freq-offset events. New connection toggle in the DX Cluster dialog plus per-slice rep...")},
            {ChangeCategory::Feature, QStringLiteral("Customizable slice colors (#2155, chibondking)"), QStringLiteral("Per-slice color selection through a new `SliceColorManager` singleton.  Color assignments persist across sessions and are visible in VFO widgets, panadapter overlays, and meter strips.")},
            {ChangeCategory::Feature, QStringLiteral("Native MSI firmware installer support"), QStringLiteral("The firmware updater can now extract `.ssdr` files directly from FlexRadio's v4.2.18+ MSI installer (which switched from PE/COFF self-extracting `.exe` to WiX 6 MSI).  Vendored `libmspack` (LGPL-2....")},
            {ChangeCategory::Feature, QStringLiteral("NAVTEX data-layer plumbing"), QStringLiteral("New `NavtexModel` covers the SmartSDR `navtex` waveform protocol — per-message Pending → Queued → Sent / Error state, status parsing for `navtex` and `navtex sent`, and a `navtex send` command path...")},
            {ChangeCategory::Feature, QStringLiteral("CWX active tracking"), QStringLiteral("`RadioModel` tracks CWX send state so the audio gate stays open during long character sends.")},
            {ChangeCategory::Feature, QStringLiteral("Floating-window dark theme (#2096, AetherClaude)"), QStringLiteral("Pop-out panadapter and floating-container windows now inherit the full dark theme via a new `Theme.h` shared stylesheet, eliminating flash-of-light-theme on window construction.")},
            {ChangeCategory::Feature, QStringLiteral("Center active VFO when zooming in from keyboard (#2183, jensenpat)"), QStringLiteral("The keyboard zoom-in shortcut now centers the active VFO in the viewport (matches the mouse-wheel zoom behavior).")},
            {ChangeCategory::BugFix, QStringLiteral("TX→RX waterfall continuity"), QStringLiteral("Force FFT-fallback rendering on TX→RX transition to prevent a visible gap in the waterfall (#2171). Blank waterfall rows for 400 ms after TX ends so any residual hardware tail doesn't paint over th...")},
            {ChangeCategory::BugFix, QStringLiteral("Hide 8000-series DSP filters on 6000-series radios"), QStringLiteral("`NRL`, `NRS`, `RNN`, `NRF` filters are FLEX-8x00-only.  6000-series radios no longer show greyed-out controls for filters they can't use.")},
            {ChangeCategory::BugFix, QStringLiteral("Stream-status helper consolidation"), QStringLiteral("Deduplicates the per-stream-type status helpers.  Annotates `m_daxTxClientHandle` and documents the TCI receiver-index policy for future contributors.")},
            {ChangeCategory::BugFix, QStringLiteral("TCP close handshake before socket destroy"), QStringLiteral("Wait for `disconnected()` before tearing down the QTcpSocket on user-initiated disconnect.  Fixes occasional \"connection reset\" reports on the radio side.")},
            {ChangeCategory::BugFix, QStringLiteral("rigctld short-form split direction"), QStringLiteral("`S` (set split) and `s` (get split) were transposed in the rigctld mapping table, so external loggers couldn't read or set split.")},
            {ChangeCategory::BugFix, QStringLiteral("CW sidetone on Windows"), QStringLiteral("`startSidetoneStream()` was never called on Windows because the audio backend init order put it after the first key event.  CW sidetone now starts immediately on connect.")},
            {ChangeCategory::BugFix, QStringLiteral("48 kHz RX audio sink on Windows"), QStringLiteral("Prefer 48 kHz over 24 kHz on Windows where WASAPI's resampler introduces audible high-frequency artifacts at 24 kHz.")},
            {ChangeCategory::BugFix, QStringLiteral("Scrollbar styling on applet panels"), QStringLiteral("The applet-panel scroll area was inheriting Qt's default thin grey scrollbar; restyles to the dark-theme bar.")},
            {ChangeCategory::BugFix, QStringLiteral("USB mic level gauge on connect"), QStringLiteral("The USB mic-level gauge wasn't drawn when connecting with `mic source = PC`; gauge now appears immediately.")},
            {ChangeCategory::BugFix, QStringLiteral("VOX phoneStateChanged on keyboard shortcut"), QStringLiteral("VOX setters didn't emit `phoneStateChanged()`, so the UI didn't refresh when VOX was toggled via keyboard shortcut.")},
            {ChangeCategory::BugFix, QStringLiteral("Scroll-wheel debounce"), QStringLiteral("Debounces high-frequency `pixelDelta` scroll events from precision-scroll mice in `VfoWidget` and `SpectrumWidget`.")},
            {ChangeCategory::BugFix, QStringLiteral("Marker-width settings cleanup (#2156, mvanhorn)"), QStringLiteral("`Slice<N>_MarkerThin` setting key is now removed after migration to `Slice<N>_MarkerWidth` instead of being left in place.")},
            {ChangeCategory::BugFix, QStringLiteral("Serial PTT — Win32 WaitCommEvent path (#2147, chibondking)"), QStringLiteral("FTDI VCP drivers only refresh DSR/CTS in the completion of a `WaitCommEvent` call; AetherSDR's polling-based detection missed edges on Windows.  Adds a Win32 native event-wait path on top of Qt's s...")},
            {ChangeCategory::BugFix, QStringLiteral("Applet pop-out persistence race (#2154, chibondking)"), QStringLiteral("Floating applet windows were sometimes restored at the wrong position because the geometry-save signal raced the close event. Systemic fix wires save before destruction.")},
            {ChangeCategory::BugFix, QStringLiteral("Windows build guard for FreeDV Reporter"), QStringLiteral("The FreeDV Reporter `freedvReportingToggled` connect-lambda used RADE-guarded symbols inside an `#ifdef HAVE_WEBSOCKETS` block. Windows has WebSockets but no RADE → undefined-identifier compile err...")},
        }},
        {QStringLiteral("0.9.2.1"), QStringLiteral("2026-04-29"), QStringLiteral("TGXL direct autotune for firmware 4.2 compatibility"), {
            {ChangeCategory::BugFix, QStringLiteral("TGXL TUNE works again on firmware 4.2"), QStringLiteral("When a direct TGXL connection (port 9010) is configured, the TUNE button now sends the native `autotune` command directly to the TGXL instead of routing `tgxl autotune handle=<H>` through the radio...")},
            {ChangeCategory::BugFix, QStringLiteral("Log redaction no longer mangles 4-component version strings"), QStringLiteral("The PII redactor's IPv4 regex matched anything that looked like four dot-separated 1-3-digit numbers, so the application's own version string `0.9.2.1` was being redacted to `*.*.*. 1` in logs and ...")},
        }},
        {QStringLiteral("0.9.2"), QStringLiteral("2026-04-28"), QStringLiteral("WAVE Phase 2, v4.2.18 firmware support, and community polish"), {
            {ChangeCategory::Feature, QStringLiteral("WAVE Phase 2 — applet visualization controls (#2124, jensenpat)"), QStringLiteral("Double-click the WAVE waveform to open a settings drawer (replaces the prior clear-on-double-click behavior).  Drawer is open by default on first launch for discoverability. New compact `View` drop...")},
            {ChangeCategory::Feature, QStringLiteral("DAX-aware TCI multi-stream routing (#2140, jensenpat)"), QStringLiteral("Adapts the DAX/TCI audio path for FlexRadio firmware 4.2.18's explicit stream-ownership reporting.  Filters DAX RX/IQ stream status by `client_handle` so we don't accidentally register another clie...")},
            {ChangeCategory::Feature, QStringLiteral("TCXO frequency-offset calibration (#2119, jensenpat)"), QStringLiteral("Replaces the unsupported `radio calibrate` command (firmware v4.1.5 returns `0x50000016` \"unknown command\") with the documented SmartSDR / FlexLib sequence: `radio set cal_freq=`, `radio set freq_e...")},
            {ChangeCategory::Feature, QStringLiteral("VFO marker controls — tri-state Marker, single Filter Edge"), QStringLiteral("The four-button row (Thin / Thick / Edges / Hide) collapses to two: - **Marker** cycles `Off → 1 px → 3 px` on click.  Off skips both the center line and the top triangle, leaving only the passband...")},
            {ChangeCategory::Feature, QStringLiteral("v4.2.18 discovery beacon parsing (#2138, AetherClaude)"), QStringLiteral("Parses two new fields from the FlexLib v4.2.18 discovery packet: `is_system_model` (flags bench / system-build radios) and `turf_region` (turf region indicator distinct from the existing `region` f...")},
            {ChangeCategory::Feature, QStringLiteral("Other"), QStringLiteral("ATU start added to the keyboard shortcut actions list. Clear action added to the CW decode window context menu (#2116).")},
            {ChangeCategory::BugFix, QStringLiteral("Resampler heap corruption (#2114, NF0T)"), QStringLiteral("`r8b::CDSPResampler24::process()` doesn't bounds-check its `l` parameter; passing a block larger than the constructor's `aMaxInLen` silently overflows internal filter buffers.  Bug manifested as a ...")},
            {ChangeCategory::BugFix, QStringLiteral("Serial PTT input non-functional (#2125, chibondking)"), QStringLiteral("Three layered bugs blocked the serial-PTT path: 1. No way to open the port without restarting AetherSDR — the Serial tab had no Open / Close buttons and the auto-open label implied a restart was re...")},
            {ChangeCategory::BugFix, QStringLiteral("Slice audio loss after band changes (#2128, jensenpat)"), QStringLiteral("After `display pan set band=`, the radio sometimes stops mixing slice audio without echoing `audio_mute=1`, leaving the model in a \"unmuted but silent\" state.  Fix: 300 ms after the band-change com...")},
            {ChangeCategory::BugFix, QStringLiteral("CWX Live toggle and Send action (#2122, jensenpat)"), QStringLiteral("`Live` button is now a true toggle so operators can turn live keying back off (was force-on only). `Send` button now submits the current input when Live is off (matching Enter-key behavior). Setup ...")},
            {ChangeCategory::BugFix, QStringLiteral("Connect-radio dialog grouping polish (#2121, jensenpat)"), QStringLiteral("Scopes the `QFrame` callout stylesheet so child labels and checkboxes don't inherit the panel border / background, fixing nested-bordered-box rendering on macOS, Windows, and Linux. Tightens the Sm...")},
        }},
        {QStringLiteral("0.9.1"), QStringLiteral("2026-04-27"), QStringLiteral("Local iambic CW keyer, unified sidetone controls, and CW transmit reliability"), {
            {ChangeCategory::Feature, QStringLiteral("Local iambic keyer for sub-5 ms paddle sidetone"), QStringLiteral("Software iambic state machine that runs alongside the radio's RF iambic engine.  Both engines see the same paddle inputs at the same WPM and produce identical Morse timing — but the local keyer dri...")},
            {ChangeCategory::Feature, QStringLiteral("Sidetone controls unified"), QStringLiteral("The CW panel previously had three separate sidetone widget groups: the radio's \"Sidetone\" toggle/volume, a \"Local STn\" toggle/volume for the local PortAudio sidetone, and a \"Follow\" pitch row with ...")},
            {ChangeCategory::Feature, QStringLiteral("CW pan slider wired up"), QStringLiteral("The L/R pan slider in the CW panel was a dead UI element with a TODO comment.  Now drives both the radio's `mon_pan_cw` (radio-side sidetone pan within the RX audio stream) and the local sidetone w...")},
            {ChangeCategory::Feature, QStringLiteral("Slice capacity notification"), QStringLiteral("Status-bar warning when adding another panadapter would exceed the radio's slice limit.  Three guard points cover the pre-flight check in the layout dialog, the runtime check in `applyPanLayout`, a...")},
            {ChangeCategory::Feature, QStringLiteral("PortAudio sidetone via JACK on Linux (#2075 follow-up)"), QStringLiteral("The PortAudio sidetone backend (introduced in v0.9.0) now prefers the JACK host API on Linux when available, with `paFramesPerBuffer = 128` + `suggestedLatency = 0` for sub-5 ms quantum.  PipeWire'...")},
            {ChangeCategory::BugFix, QStringLiteral("CW transmit: invalid paddle command form"), QStringLiteral("`RadioModel::sendCwPaddle` was emitting `cw key 1 0` (a 2-arg paddle form) which the radio's protocol does not accept — FlexLib only ever sends single-state `cw key 1` or `cw key 0` and expects the...")},
            {ChangeCategory::BugFix, QStringLiteral("CW transmit: lowercase hex in netcw payload"), QStringLiteral("FlexLib formats `time=0x...` and `client_handle=0x...` with C# `ToString(\"X\")` (uppercase), and the radio's status messages do too (e.g. `S23A59BDF|...`).  Firmware v4.1.5's netcw parser is case-se...")},
            {ChangeCategory::BugFix, QStringLiteral("CW transmit: dead TCP fallback"), QStringLiteral("The post-UDP TCP fallback was sending the netcw-decorated form (`cw key 1 time=0x... index=... client_handle=0x...`) which the radio rejects with `0x50001000` (\"command syntax error\") on TCP. Remov...")},
            {ChangeCategory::BugFix, QStringLiteral("Optimistic updates for CW model setters"), QStringLiteral("`setCwIambic`, `setCwIambicMode`, `setCwSpeed`, and `setCwPitch` on `TransmitModel` previously sent the command to the radio without updating local state, on the assumption that the radio would ech...")},
            {ChangeCategory::BugFix, QStringLiteral("Block on graceful disconnect (#1996, openstreem)"), QStringLiteral("`RadioModel::disconnectFromRadio` now uses `Qt::BlockingQueued Connection` for both the `gracefulDisconnect` lambda and the fallback `disconnectFromRadio` call, matching the destructor's pattern.  ...")},
            {ChangeCategory::BugFix, QStringLiteral("Memory recall: restore repeater offset and tone (#1871, #1965, jensenpat)"), QStringLiteral("Recalled FM repeater memories properly restore `repeater_offset_dir`, `fm_repeater_offset_freq`, derived `tx_offset_freq`, and CTCSS tone state.  Previously, switching from a repeater memory to sim...")},
            {ChangeCategory::BugFix, QStringLiteral("`m_activeTxSlice` initializer mismatch"), QStringLiteral("Default-init was `0` but `clear()` sets to `-1`.  Now both default to `-1` so a fresh `MeterModel` and a cleared one have identical `activeTxSlice()` state.")},
            {ChangeCategory::BugFix, QStringLiteral("Per-slice compression meter resolution"), QStringLiteral("TX-chain `COMPPEAK`, `AFTEREQ`, and `SC_MIC` meters are now resolved per active slice instead of last-match-wins, fixing Multi-Flex setups where the wrong slice's compression value was surfaced in ...")},
            {ChangeCategory::BugFix, QStringLiteral("ContainerWidget restoreState displaces children"), QStringLiteral("`restoreState` re-inserted children at indices 0..N–1 even when they were already in the layout, displacing non-saved children like the CHAIN widget.  `insertChildWidget` is now a no-op when the ch...")},
        }},
        {QStringLiteral("0.9.0"), QStringLiteral("2026-04-25"), QStringLiteral("Aetherial RX Audio Suite, frameless UI, and local CW sidetone"), {
            {ChangeCategory::Feature, QStringLiteral("PooDoo Audio RX chain"), QStringLiteral("Reuses the existing TX DSP modules (Parametric EQ, gate/expander, compressor, tube saturator, PUDU exciter) on the RX path with fully independent state from TX.  Each stage is a tile in the CHAIN w...")},
            {ChangeCategory::Feature, QStringLiteral("Frameless main window with custom title bar"), QStringLiteral("Main window now uses `Qt::FramelessWindowHint` with a custom 20 px title bar carrying drag-to-move via `startSystemMove`, double-click maximise, and a Discord-style minimise / maximise / close trio...")},
            {ChangeCategory::Feature, QStringLiteral("Frameless pop-out windows for applets and panadapters"), QStringLiteral("Every floating applet and panadapter pop-out gains the same frameless title bar with the trio.  Single-click trio actions, drag the bar to move, double-click to maximise.")},
            {ChangeCategory::Feature, QStringLiteral("Frameless editor windows for the entire PooDoo chain"), QStringLiteral("Aetherial Parametric EQ, AGC-T, AGC-C, Tube, PUDU, and Reverb editors all use the same shared title-bar widget with drag and trio.  Title text reads \"Aetherial &lt;Stage&gt; — &lt;Side&gt;\" so the ...")},
            {ChangeCategory::Feature, QStringLiteral("Polish frameless title bar"), QStringLiteral("Title-bar trio refined to a Discord-style sequence; dropped the lightbulb icon in favour of a minimal arrow accent so the trio sits flush against the right edge.")},
            {ChangeCategory::Feature, QStringLiteral("Local CW sidetone with low-latency sink + CWX support"), QStringLiteral("Dedicated low-latency local sidetone path that bypasses the protocol-level monitor for keying feedback that doesn't fight network jitter.  Works for paddle, straight key, and CWX generated transmis...")},
            {ChangeCategory::Feature, QStringLiteral("Two-Tone Tune shortcut (#1995, jensenpat)"), QStringLiteral("New unassigned `Two-Tone Tune` keyboard action under the TX shortcut category.  Sends `transmit set tune_mode=two_tone` before starting Tune, toggles off on a second press, and restores `tune_mode=...")},
            {ChangeCategory::Feature, QStringLiteral("XVTR diagnostic logging"), QStringLiteral("New `xvtr` logging category captures status messages, RF↔IF frequency translation, and pan-bandwidth conversions so XVTR setup issues can be diagnosed from log bundles instead of pcap.")},
            {ChangeCategory::Feature, QStringLiteral("XVTR policy regression tests"), QStringLiteral("Test harness covers transverter active/inactive state, RF↔IF conversion, and the family of pan-recenter cases that used to drop the waterfall when crossing IF/RF boundaries.")},
            {ChangeCategory::BugFix, QStringLiteral("Flex compression meter derivation across radio families (#1992, jensenpat)"), QStringLiteral("FLEX-8000 series exposes `TX/AFTEREQ` at 20 fps as the post-EQ reference for compression display; FLEX-6000 captures don't expose `AFTEREQ`, so the best matching reference is `TX/SC_MIC` at 10 fps ...")},
            {ChangeCategory::BugFix, QStringLiteral("Preserve rtty_mark_default when radio resets mark on band change (#1968, chibondking)"), QStringLiteral("The radio resets `slice rtty_mark` to 2125 in the status broadcast that follows a band change, regardless of the configured `rtty_mark_default`.  `SliceModel::applyStatus()` was accepting the 2125 ...")},
            {ChangeCategory::BugFix, QStringLiteral("Fix choppy/harsh RADE decoded audio: dedicated buffer + sample-wise mix (#1953, NF0T)"), QStringLiteral("RADE decoded audio was sharing the SSB output buffer, producing glitches and harsh artefacts on decoded voice.  Split into a dedicated buffer with per-sample mixing into the speaker output so RADE ...")},
            {ChangeCategory::BugFix, QStringLiteral("Fix CoreMIDI init crash during MIDI auto-connect (#1949, jensenpat)"), QStringLiteral("macOS CoreMIDI initialisation could crash on launch when auto-connect ran before the MIDI client was fully constructed. Init order tightened so the client is ready before any auto-connect attempt.")},
            {ChangeCategory::BugFix, QStringLiteral("Restore shortcuts after slider nudges (#1952, jensenpat)"), QStringLiteral("Shortcut bindings could go stale after the user nudged a slider with the keyboard, because focus stayed on the slider and ate subsequent shortcut keystrokes.  Focus now returns to the main window a...")},
            {ChangeCategory::BugFix, QStringLiteral("Preserve MIDI bindings when saving device settings (#1951, jensenpat)"), QStringLiteral("Saving MIDI device settings was overwriting bindings with an empty map because the save path read from the wrong source. Now the existing binding map is merged into the saved settings.")},
            {ChangeCategory::BugFix, QStringLiteral("Cancel frequency entry with Escape (#1954, jensenpat)"), QStringLiteral("The numeric frequency-entry buffer didn't honour Escape, so a user who started typing a frequency had to clear the buffer manually.  Escape now cancels the entry cleanly.")},
            {ChangeCategory::BugFix, QStringLiteral("Gate XVTR waterfall tile shifts (#1925, jensenpat)"), QStringLiteral("Waterfall tile-shift logic continued to fire on XVTR pans even when the IF↔RF frequency relationship made the shift meaningless, producing visual tearing.  Shifts are now gated on transverter state.")},
            {ChangeCategory::BugFix, QStringLiteral("Close all floating windows when main window closes (#1920, chibondking)"), QStringLiteral("Floating applet pop-outs and panadapter pop-outs were left as orphaned top-level windows when the main window was closed, forcing the user to close each one individually.  All floating children now...")},
        }},
        {QStringLiteral("0.8.22"), QStringLiteral("2026-04-25"), QStringLiteral("Connection visibility plus community polish"), {
            {ChangeCategory::Feature, QStringLiteral("Band-stack auto-save on dwell"), QStringLiteral("New \"Auto-save dwell\" setting in the band-stack ⚙ menu (Off / 10 s / 30 s / 60 s, default Off).  When enabled, the active slice is automatically added to the band stack after dwelling on a frequenc...")},
            {ChangeCategory::Feature, QStringLiteral("LAN and SmartLink client login notifications (#1892, jensenpat)"), QStringLiteral("New connection-event notifications when remote LAN, SmartLink, and Maestro clients log in to the radio.  Surfaced as toast-style banners in the status bar so operators see Multi-Flex / shared-radio...")},
            {ChangeCategory::Feature, QStringLiteral("Remote client disconnect flows (#1889, jensenpat)"), QStringLiteral("Disconnect remote LAN, SmartLink, and Maestro clients from inside AetherSDR.  Operator confirmation prompt + clean protocol teardown so the disconnected client sees a graceful exit rather than a st...")},
            {ChangeCategory::Feature, QStringLiteral("Passive Spots mode (#1897, jensenpat)"), QStringLiteral("New spots mode that ingests spot data without sending any commands to the radio — useful for read-only / observer scenarios where you want spot decorations on the spectrum but don't want the spot s...")},
            {ChangeCategory::Feature, QStringLiteral("Device details in Slice Troubleshooter (#1898, jensenpat)"), QStringLiteral("Slice Troubleshooter now reports audio and control device details alongside the existing slice diagnostics so support bundle reviewers can see the full I/O path from radio to host without a separat...")},
            {ChangeCategory::Feature, QStringLiteral("Find search in help dialogs (#1899, jensenpat)"), QStringLiteral("Help dialogs gain Ctrl+F find with case-insensitive search, F3 for Find Next, and Shift+F3 for Find Previous — the same shortcut behaviour users expect from any text viewer.")},
            {ChangeCategory::BugFix, QStringLiteral("Click-to-tune no longer recentres for clicks inside the visible spectrum"), QStringLiteral("Click-to-tune was triggering a comfort-margin pan recenter even when the click landed well inside the visible spectrum, making the outer 18% of each pan effectively a dead zone that auto-pulled the...")},
            {ChangeCategory::BugFix, QStringLiteral("TCI tx_enable echoes immediately to unblock WSJT-X v3.0.0 TX"), QStringLiteral("WSJT-X v3.0.0 waits for a `tx_enable` ack before keying.  AetherSDR's TCI handler now matches the `cmdTrx` pattern (immediate echo plus pending notification) so WSJT-X TX is unblocked on current bu...")},
            {ChangeCategory::BugFix, QStringLiteral("Fix silent audio in SSB/Digital modes when m_radeMode is active (#1875, #1879, NF0T)"), QStringLiteral("RADE-mode activation toggled a DAX route flag in `AudioEngine` that left SSB and Digital modes silent on TX.  Audio path is now restored when RADE mode is not active.")},
            {ChangeCategory::BugFix, QStringLiteral("MultiFlex PTT label, row-selection, and correct PTT command (#1893, chibondking)"), QStringLiteral("Three related Multi-Flex PTT issues: the PTT button label showed the wrong station (other client's name instead of ours), row selection had no effect, and the wrong protocol command was being sent....")},
            {ChangeCategory::BugFix, QStringLiteral("Network health indicator stabilised for SmartLink and remote paths (#1900, jensenpat)"), QStringLiteral("Status-bar network indicator was twitchy on LTE, SmartLink, and hotel Wi-Fi.  Now uses remote-friendly RTT thresholds, evaluates VITA packet loss over a rolling 10-second window, includes audio int...")},
            {ChangeCategory::BugFix, QStringLiteral("Suppress startup client login replays (#1904, jensenpat)"), QStringLiteral("When AetherSDR subscribed to `client all` after connecting, the radio replayed the current client list as `client ... connected` status messages — which the new notification handler from #1892 trea...")},
            {ChangeCategory::BugFix, QStringLiteral("Persist CAT/TCI/DAX IQ enabled state across restarts (#1890, chibondking)"), QStringLiteral("CAT, TCI, and DAX IQ enabled flags weren't written to AppSettings, so toggling any of them off and restarting brought them back on. Now persisted under their respective enabled keys and restored at...")},
            {ChangeCategory::BugFix, QStringLiteral("Reduce minimum window height"), QStringLiteral("Operators with smaller vertical footprints (low-resolution displays, cramped multi-monitor setups) couldn't shrink the main window below the prior height floor.  Lowered the minimum so small layout...")},
            {ChangeCategory::BugFix, QStringLiteral("What's New release date now visible"), QStringLiteral("The release date line on the What's New dialog was being clipped off the visible area.  Layout now anchors the date inside the dialog frame.")},
            {ChangeCategory::BugFix, QStringLiteral("Restore Flex band-stack selection keys (#1887, jensenpat)"), QStringLiteral("The hardware Flex front-panel band-stack keys had stopped routing to the band-stack selection action after a recent refactor. Re-wired so keypad band selection works again.")},
            {ChangeCategory::BugFix, QStringLiteral("Tighten incremental pan-follow margins"), QStringLiteral("Trigger margin lowered from 12% → 5% and settle from 18% → 6% so trackpad / wheel / arrow-key tuning at the pan edge feels snappier without losing the smooth follow animation.")},
            {ChangeCategory::BugFix, QStringLiteral("Shorten client-connection status message from 10s to 3s"), QStringLiteral("The transient banner shown after a successful connection was staying up too long and obscuring slice activity.")},
            {ChangeCategory::BugFix, QStringLiteral("Fix versioning label issue on Windows"), QStringLiteral("Build version label on Windows could render incorrectly when the build metadata included certain characters.  Resolved.")},
            {ChangeCategory::BugFix, QStringLiteral("Band-stack ⚙ / × / + buttons no longer clip their glyphs"), QStringLiteral("The three control buttons in the band-stack bottom row now have zero internal padding so the multiplication-sign, plus, and gear glyphs render fully inside the small fixed-size buttons regardless o...")},
        }},
        {QStringLiteral("0.8.21"), QStringLiteral("2026-04-23"), QStringLiteral("Recenter policy unification plus Band Stack management"), {
            {ChangeCategory::Feature, QStringLiteral("Unified tuning/recenter policy (#1861, jensenpat)"), QStringLiteral("Large refactor (+1038/-207, new `RECENTER.md` design doc) that moves pan-follow, reveal, and hard-center decisions out of individual call sites and into a shared `MainWindow` policy layer.  Every t...")},
            {ChangeCategory::Feature, QStringLiteral("Band Stack management (#1471 → #1472)"), QStringLiteral("New UI affordances in the Band Stack sidebar: - **× button** — \"Clear All\" with a confirmation dialog, so accidental clears during contests don't wipe a day's work. - **⚙ button** — opens a setting...")},
            {ChangeCategory::BugFix, QStringLiteral("Install tolerant X11 error handler"), QStringLiteral("Qt Multimedia's FFmpeg backend probes VA-API / VDPAU hardware accel via X11 even under native Wayland.  On systems that ship `libvdpau-va-gl` by default (openSUSE Tumbleweed + Packman is the canoni...")},
            {ChangeCategory::BugFix, QStringLiteral("DXCC prefix resolution off GUI thread"), QStringLiteral("0.8.19 regression surfaced at 125 k-QSO log sizes: when the ADIF auto-reloader (new in 0.8.19) re-processed the logbook, `onParseFinished` did O(N) DXCC prefix lookups on the GUI thread, producing ...")},
            {ChangeCategory::BugFix, QStringLiteral("TCI RX meter post-gain (#1716 → #1717)"), QStringLiteral("TCI RX level meter and DAX RX level meter in the same applet panel used different measurement points: TCI was pre-gain, DAX was post-gain.  At default gain 0.5 that made TCI read ~6 dB higher than ...")},
            {ChangeCategory::BugFix, QStringLiteral("XVTR waterfall always black"), QStringLiteral("When a transverter is active the radio reports pan center in RF domain (e.g. 144.2 MHz) but the VITA-49 waterfall tile header carries IF-domain frequencies (e.g. ~28 MHz).  The existing frequency-a...")},
            {ChangeCategory::BugFix, QStringLiteral("PAN freeze in Minimal Mode with popped-out panadapter"), QStringLiteral("`toggleMinimalMode(true)` called `setUpdatesEnabled(false)` on every applet in `m_panStack->allApplets()`, which includes floating (popped-out) pans.  Their content froze even though their window w...")},
            {ChangeCategory::BugFix, QStringLiteral("rigctld TCP CAT per-port slice binding (#1621 → #1623)"), QStringLiteral("WSJT-X sends `V VFOB` on init, which `cmdSetVfo()` was treating as an authoritative slice-index override.  Every WSJT-X instance across multiple rigctld ports then ended up controlling Slice B. `cm...")},
            {ChangeCategory::BugFix, QStringLiteral("RADE TX no-waveform regression from #1780 (#1865, NF0T)"), QStringLiteral("v0.8.19 added `emit daxRouteRequested(on ? 0 : 1)` to `AudioEngine::setRadeMode` on the mistaken theory that RADE wanted the radio's mic path.  On PTT in RADE mode this sent `transmit set dax=0`, w...")},
            {ChangeCategory::BugFix, QStringLiteral("Applet pop-out persistence and app-exit regression (#1860, chibondking)"), QStringLiteral("Four bugs around the migration from the retired `FloatingAppletWindow` (System A) to the new `ContainerManager` (System B): 1. `ContainerGeometry_P/CW` uses `/` which is invalid in AppSettings XML ...")},
            {ChangeCategory::BugFix, QStringLiteral("Arduino WiFiServer TCP deadlock for ShackSwitch R4 (#1859, nigelfenton)"), QStringLiteral("ShackSwitch R4 is an Arduino-based Antenna Genius.  Arduino's `WiFiServer::available()` only returns a connected client once the client has sent data, but the AG protocol is server-speaks-first (`V...")},
        }},
        {QStringLiteral("0.8.20"), QStringLiteral("2026-04-22"), QStringLiteral("Community regression round-up + crash batch"), {
            {ChangeCategory::BugFix, QStringLiteral("Fix RADE RX not decoding on Windows (#1820, NF0T)"), QStringLiteral("On platforms without a DAX audio bridge (Windows, Linux without PipeWire), `startDax()` is compiled out so `stream create dax_rx` is never sent.  RADE mode showed \"activated\" but produced no decode...")},
            {ChangeCategory::BugFix, QStringLiteral("Clamp stale DAX RX backlog on macOS (#1822, jensenpat)"), QStringLiteral("FT8 decodes in WSJT-X / JTDX showed a consistent +1.5 s to +1.6 s `DT` offset on macOS even with healthy LAN latency — severe enough to cause missed QSOs. Root cause: macOS `VirtualAudioBridge` wri...")},
            {ChangeCategory::BugFix, QStringLiteral("Fix TCI DAX audio crosstalk between slices (#1815, Chaosuk97)"), QStringLiteral("Two simultaneous DAX channels (dual-band WSJT-X, satellite full-duplex RX) caused audio from slice A to bleed into slice B. Root cause: `r8brain CDSPResampler` is stateful.  `ClientState` held a si...")},
            {ChangeCategory::BugFix, QStringLiteral("Fix RX applet pan slider broken with NR active (#1799, Chaosuk97)"), QStringLiteral("PR #1460 re-applied pan after client NR mono-mix but only tracked pan changes from the VFO panel slider.  The RX applet, MIDI, and keyboard shortcuts call `SliceModel::setAudioPan()` directly — `Au...")},
            {ChangeCategory::BugFix, QStringLiteral("Fix TCI RX gain default fallback: 1.0 → 0.5 (#1811, NF0T)"), QStringLiteral("`TciServer` fallback for a missing `TciRxGain<n>` key was \"1.0\" while `TciApplet` used \"0.5\".  Fresh install or post-migration without the key ran the server at full gain while the slider showed ha...")},
            {ChangeCategory::BugFix, QStringLiteral("Fix missing HAVE_SERIALPORT guard on m_flexCoalesceTimer (#1812, NF0T)"), QStringLiteral("PR #1606 guarded `m_hidCoalesceTimer` behind `HAVE_HIDAPI` but left `m_flexCoalesceTimer` unguarded on the line above — broke Windows builds without `Qt6::SerialPort`.  Wrapped the reference in `HA...")},
            {ChangeCategory::BugFix, QStringLiteral("Seamless ADIF logbook auto-reload for DXCC spot colouring (#1801, Chaosuk97)"), QStringLiteral("Spot colours only updated when the user manually clicked Browse and re-selected the log file.  The \"Auto-Reload Log\" toggle defaulted off and broke in edge cases: atomic rename (N1MM, Log4OM), Wind...")},
            {ChangeCategory::BugFix, QStringLiteral("Fix crash when reordering applets with floating containers (#1745 → #1746)"), QStringLiteral("Dragging an applet title bar to reorder crashed AetherSDR when any other applet was popped out into a floating window. Root cause: `QWidget::mapTo()` traverses the parent chain looking for a common...")},
            {ChangeCategory::BugFix, QStringLiteral("Lazy-build RadioSetupDialog tabs (#1776 → #1777)"), QStringLiteral("Opening Settings → Radio Setup / FlexControl / USB Cables crashed on some Wayland / Qt 6.11 configurations (openSUSE Tumbleweed with Packman non-free FFmpeg 8.1).  Real crash cause is `libvdpau_va_...")},
            {ChangeCategory::BugFix, QStringLiteral("Fix UI freeze when PanAdapter is popped out (#1668 → #1669)"), QStringLiteral("Content inside a popped-out panadapter froze — waterfall stopped scrolling, spectrum stopped updating — while the main window remained fully responsive.  Re-docking restored normal operation. Two p...")},
            {ChangeCategory::BugFix, QStringLiteral("Minor Canadian band-plan corrections (#1817, VE3NEM)"), QStringLiteral("RAC band plan updates from a Canadian ham: label corrections on overlapping CW/DIG segments (1.800, 7.035), WSPR 80 m frequency corrected (3.5926 → 3.5686 MHz), obsolete CW calling frequencies remo...")},
        }},
        {QStringLiteral("0.8.19"), QStringLiteral("2026-04-20"), QStringLiteral("Community contributions landfest + macOS MNR + quick memory actions"), {
            {ChangeCategory::Feature, QStringLiteral("macOS MMSE-Wiener spectral noise reduction (MNR) (#1672, Chaosuk97)"), QStringLiteral("New macOS-exclusive client-side NR mode accelerated via Apple Accelerate (vDSP / AMX on Apple Silicon).  Decision-directed MMSE-Wiener filter with 25-frame minimum-statistics noise-floor tracking. ...")},
            {ChangeCategory::Feature, QStringLiteral("PooDoo™ TX Reverb (#1741) — carried forward, chain now complete"), QStringLiteral("Already shipped in v0.8.18 final release notes; listed here for completeness since it rolls into the 0.8.19 post-release chain view.")},
            {ChangeCategory::Feature, QStringLiteral("Quick memory browse + save on slice overlays (#1781, jensenpat)"), QStringLiteral("Two new actions in the slice overlay rail between Display and DAX: - **MEM▸** opens a compact 252×430 browser drawer, frequency-sorted, pre-scrolled to the active slice's nearest memory.  Click-to-...")},
            {ChangeCategory::Feature, QStringLiteral("Improved TNF notch interaction (#1547, rfoust)"), QStringLiteral("TNF markers no longer extend into the waterfall — they stop at the bottom of the spectrum pane so the waterfall stays a historical signal record. Depth visualisation via hatching (12/8/5 px spacing...")},
            {ChangeCategory::Feature, QStringLiteral("RAC Canada Band Plan corrected + expanded (#1709, VE3NEM)"), QStringLiteral("VHF / UHF bands added (6 m, 2 m, 1.25 m, 70 cm) — prior file was HF-only, so 4 new ham-band coverage tiers arrive in this update. 60 m channels updated to match the Feb 2026 ISED regulation change ...")},
            {ChangeCategory::Feature, QStringLiteral("Frameless Window toggle (View menu, Ctrl+Shift+F)"), QStringLiteral("View → Frameless Window applies Qt's `FramelessWindowHint` so users gain ~32 px of vertical screen space at the cost of the OS title bar. Works cross-platform (X11, Wayland, Windows, macOS) — KDE w...")},
            {ChangeCategory::Feature, QStringLiteral("CHAIN-driven TX DSP (v0.8.18 rebase — carried forward)"), QStringLiteral("Already in v0.8.18 release notes; called out here because subsequent releases continue to build on the CHAIN ordering.")},
            {ChangeCategory::BugFix, QStringLiteral("NR2 crackling on strong signals (#1696, Chaosuk97)"), QStringLiteral("Cap `gainMax` default 1.5 → 1.0.  The MMSE-LSA estimator is a noise *reducer* — the old 1.5 default allowed 50% amplification on strong signals, which pushed output past ±1.0 float32 full scale and...")},
            {ChangeCategory::BugFix, QStringLiteral("Audio pan broken with client NR active (#1685, Chaosuk97) + per-slice AF mute persistence"), QStringLiteral("Client-side NR (NR2 / RN2 / NR4 / DFNR) mono-mixes L+R before processing, destroying the radio-applied pan.  New `AudioEngine::setRxPan` atomic + static `applyRxPanInPlace` helper re-applies a line...")},
            {ChangeCategory::BugFix, QStringLiteral("VFO / TNF / filter-edge ±1 px jitter during tuning (#1703, Chaosuk97) + cursor-snap-to-VFO"), QStringLiteral("`mhzToX()` switched from integer truncation to `std::round` so the same frequency always maps to the same pixel column regardless of pan-center fractional drift during continuous tune.  One-line fi...")},
            {ChangeCategory::BugFix, QStringLiteral("Zoom +/- can't reach full panadapter bandwidth (#1697, Chaosuk97)"), QStringLiteral("Replace early-return guard in `emitZoom` / keyboard zoom with `std::clamp(newBw, minBwMhz, maxBwMhz)` so the final click/keypress snaps to exactly the radio's min/max — matching the mouse-drag beha...")},
            {ChangeCategory::BugFix, QStringLiteral("PC Audio button shows wrong state on connect (#1695, Chaosuk97)"), QStringLiteral("`onConnectionStateChanged` now reads `PcAudioEnabled` once and calls both `m_titleBar->setPcAudioEnabled(...)` (sync button, with QSignalBlocker) and `audioStartRx()` from the same decision. Previo...")},
            {ChangeCategory::BugFix, QStringLiteral("Waterfall filter-passband fill removed (#1701, Chaosuk97)"), QStringLiteral("The coloured filter-passband fill in the waterfall was only rendered by the software-fallback paintEvent path — the GPU renderer already skipped it.  Dropping the software `fillRect` brings the two...")},
            {ChangeCategory::BugFix, QStringLiteral("TCI DAX duplicate stream causing 2× audio speed (#1679 / #1678)"), QStringLiteral("Two code paths both subscribing to `dax_rx` for the same channel made `daxAudioReady` fire twice per period, doubling WSJT-X's apparent sample rate and killing FT8 decode.  Registration now evicts ...")},
            {ChangeCategory::BugFix, QStringLiteral("TCI TX no RF output"), QStringLiteral("Always emit `transmit set dax=1` on PTT regardless of the previous Low-Latency DAX menu toggle.  The `dax=0` path was advertised for external FreeDV over virtual audio cable but never actually work...")},
            {ChangeCategory::BugFix, QStringLiteral("Low-Latency DAX consolidated into RADE mode"), QStringLiteral("The \"Low-Latency DAX (FreeDV)\" menu item was removed.  RADE mode's activation now emits a `daxRouteRequested` signal that MainWindow routes to `transmit set dax=N`, flipping the route atomically wi...")},
            {ChangeCategory::BugFix, QStringLiteral("Bandwidth corrections silently dropped during pan-follow"), QStringLiteral("The stale-echo guard in `setFrequencyRange` bypassed *any* echo during an in-flight pan-center animation when the center matched. If the echo also carried a bandwidth correction (e.g. post-resize `...")},
            {ChangeCategory::BugFix, QStringLiteral("Pan-follow VFO edge-tracking after manual drag"), QStringLiteral("After a manual spectrum drag, `PanadapterModel::centerMhz` lagged the visible pan center by up to one frame.  `panFollowVfo` now reads the live center from the spectrum widget instead of the model,...")},
            {ChangeCategory::BugFix, QStringLiteral("Propagation overlay click tunes VFO by accident"), QStringLiteral("Clicking the K/SFI propagation overlay was also triggering the release-to-tune path.  Set `m_spotClickConsumed = true` after the `propForecastClicked` emission so the subsequent release event can s...")},
            {ChangeCategory::BugFix, QStringLiteral("Off-screen slice click tunes wrong frequency"), QStringLiteral("Same pattern as the prop-overlay fix: clicking an off-screen slice indicator now suppresses release-to-tune so the tuning action doesn't pile on top of the slice-activate.")},
            {ChangeCategory::BugFix, QStringLiteral("rigctld TCP server not accepting IPv4 on Windows"), QStringLiteral("`QHostAddress::Any` sometimes binds IPv6-only on Windows, leaving IPv4 clients unable to connect.  Use `QHostAddress::AnyIPv4` explicitly.")},
            {ChangeCategory::BugFix, QStringLiteral("Serial port parameters (data bits, parity, stop bits) not persisted"), QStringLiteral("RadioSetupDialog's serial tab built the combos with defaults but never read from AppSettings or connected the save handler.  Added both.")},
            {ChangeCategory::BugFix, QStringLiteral("Compression meter shows raw dBFS instead of gain reduction"), QStringLiteral("COMPPEAK is the compressor *output* level; AFTEREQ is the *input*. Meter was displaying raw COMPPEAK, which just shows \"how loud is the output\" instead of \"how much is the compressor reducing\".  No...")},
            {ChangeCategory::BugFix, QStringLiteral("macOS audio output device silently overridden"), QStringLiteral("On macOS, AudioEngine replaced the user's chosen output device with `QMediaDevices::defaultAudioOutput()` whenever Qt reported 48 kHz as unsupported — but that happens on some normal CoreAudio outp...")},
        }},
        {QStringLiteral("0.8.18"), QStringLiteral("2026-04-20"), QStringLiteral("PooDoo™ TX Audio — complete six-stage DSP chain, click-bypass, reverb"), {
            {ChangeCategory::Feature, QStringLiteral("PooDoo™ Audio: Gate, DeEss, Tube, PUDU DSPs + editors (#1661 phases 2-5, #1734)"), QStringLiteral("Four new client-side TX DSP stages complete the chain started in v0.8.17: - **ClientGate** — downward expander / noise gate with Expander↔Gate mode toggle, threshold/ratio/attack/hold/release/floor...")},
            {ChangeCategory::Feature, QStringLiteral("PUDU Monitor — post-chain record + playback"), QStringLiteral("Two icon buttons in the CHAIN header (⏺ record, ▶ play) capture up to 30 seconds of **post-PUDU TX audio** without transmitting.  Recording writes an int16 stereo 24 kHz WAV to `/tmp/pudu_monitor.w...")},
            {ChangeCategory::Feature, QStringLiteral("Reverb (Freeverb) stage + CHAIN integration"), QStringLiteral("New **ClientReverb** DSP — Jezar's canonical Freeverb algorithm: 8 parallel lowpass-feedback comb filters summed through 4 series allpass filters, stereo-spread by 23 samples between channels. Pre-...")},
            {ChangeCategory::Feature, QStringLiteral("CHAIN gesture set — single-click bypass, double-click edit"), QStringLiteral("CHAIN widget rewires the click gestures: - **Single-click** a stage → toggle bypass. - **Double-click** a stage → open its floating editor. - **Drag** a stage → reorder the chain. - **Right-click**...")},
            {ChangeCategory::Feature, QStringLiteral("Applet-tile knob rows"), QStringLiteral("Every DSP node's applet tile grows a compact 4-5 knob tuning row at PUDU-style 38×48 px so common parameters can be tweaked without opening the full editor: - **GATE**: Thresh · Ratio · Attack · Re...")},
            {ChangeCategory::Feature, QStringLiteral("Applet-stack order mirrors CHAIN order"), QStringLiteral("Dragging a stage to a new position in CHAIN automatically reorders the sub-container tiles inside the PooDoo™ Audio group to match. On startup the persisted chain order drives the initial tile orde...")},
            {ChangeCategory::Feature, QStringLiteral("Visual TX signal chain applet"), QStringLiteral("New CHAIN tile renders the full TX DSP path as a row of labelled boxes — MIC → GATE → EQ → DESS → COMP → TUBE → ENH → TX — with live bypass state, drag-drop reordering, per-stage click-to-edit and ...")},
            {ChangeCategory::Feature, QStringLiteral("Unified TX DSP container"), QStringLiteral("CHAIN, CEQ and CMP are now three sections of a single TXDSP tile in the applet tray.  Each section has its own titlebar with its own float / close buttons so individual sections can pop out indepen...")},
            {ChangeCategory::Feature, QStringLiteral("Applet panel pop-out"), QStringLiteral("View → \"Pop Out Applet Panel\" (Ctrl+Shift+S) floats the entire right-side panel into its own top-level window.  Splitter slot collapses to zero; spectrum takes the reclaimed width.  Close the float...")},
            {ChangeCategory::Feature, QStringLiteral("Post-fader level meters on DAX / TCI"), QStringLiteral("MeterSlider now multiplies its displayed RMS by the gain thumb before rendering, so moving the fader on a TCI or DAX channel gives immediate visual feedback instead of showing the raw pre-fader level.")},
            {ChangeCategory::Feature, QStringLiteral("Consistent meter motion — MeterSmoother"), QStringLiteral("Extracted the asymmetric attack/release smoothing pattern used by HGauge into a shared MeterSmoother helper (30 ms attack, 180 ms release, polled at 120 Hz).  Applied to HGauge, ClientCompMeter, Cl...")},
            {ChangeCategory::BugFix, QStringLiteral("Digital-mode TX routing"), QStringLiteral("AudioEngine bypasses client-side Comp + EQ on the DAX/TCI TX path so WSJT-X and fldigi tones reach the radio unshaped.  Mic voice TX continues to run through the full client-side DSP chain. Fixed l...")},
            {ChangeCategory::BugFix, QStringLiteral("Compressor editor polish"), QStringLiteral("Threshold fader on the left of the compressor editor replaces the old numeric label + tiny triangle with a combined input- level meter + draggable slider, matching the Client EQ output fader visual...")},
            {ChangeCategory::BugFix, QStringLiteral("CEQ cascade Q honouring"), QStringLiteral("Fix regression where HP/LP bands ignored the user-set Q on cascade slopes.  Cascade-slope refactor had hardcoded the family Q; new `resonanceScale = userQ / 0.707` multiplier restores user control ...")},
            {ChangeCategory::BugFix, QStringLiteral("PUDU monitor audio routing"), QStringLiteral("Earlier implementation mixed recorded int16 directly into the sink (wrong format, blew out speakers) and interleaved playback with live RX audio.  Fixed: convert int16→float32 before enqueueing at ...")},
        }},
        {QStringLiteral("0.8.17"), QStringLiteral("2026-04-19"), QStringLiteral("Client-side TX Compressor (Pro-XL-style, Phase 1) + Fully-interactive 10-band Client EQ"), {
            {ChangeCategory::Feature, QStringLiteral("Client EQ — 10-band parametric, 4 filter families, output fader (#1650 / #1651 / #1658 / #1660)"), QStringLiteral("Client-side 10-band parametric EQ on both RX (post-NR, pre-sink) and TX (post-mic, pre-VITA-49) paths.  Default layout: HP / Low Shelf / 6× Peak / High Shelf / LP, all disabled on first launch Each...")},
            {ChangeCategory::Feature, QStringLiteral("Client-side TX compressor (Pro-XL-style, Phase 1)"), QStringLiteral("Feed-forward compressor with soft-knee quadratic interpolation, linear- domain peak envelope detection, stereo-linked gain application, and a brickwall peak limiter on the output Parameters: thresh...")},
            {ChangeCategory::Feature, QStringLiteral("Docked Compressor applet + floating editor"), QStringLiteral("View-only CMP tile in the applet tray: live transfer curve with a glowing ball that slides along the curve at the current envelope level, horizontal GR strip beneath, Bypass toggle + Edit… button F...")},
            {ChangeCategory::Feature, QStringLiteral("DSP test harness"), QStringLiteral("tests/client_comp_test.cpp — 11 standalone smoke tests: bypass, below-threshold passthrough, static ratio (4:1 and 20:1), makeup, limiter ceiling, stereo linking, attack timing, soft-knee monotonic...")},
            {ChangeCategory::Feature, QStringLiteral("Settings"), QStringLiteral("Client EQ: ClientEq{Rx,Tx}_Enabled / ActiveBandCount, per-band ClientEq{Rx,Tx}_Band{N}_* (type, freq, gain, Q, slope, enabled), ClientEq{Rx,Tx}_FilterFamily, ClientEq{Rx,Tx}_MasterGain, plus Client...")},
            {ChangeCategory::Feature, QStringLiteral("Docs"), QStringLiteral("docs/tx-audio-signal-path.md — new \"Client-side TX DSP\" section showing mic → ClientEq + ClientComp → VITA-49 → radio firmware chain docs/architecture-pipelines.md — TX pipeline diagram shows apply...")},
        }},
        {QStringLiteral("0.8.16"), QStringLiteral("2026-04-18"), QStringLiteral("DIGI Applet Split, TCI Audio Reliability, PGXL-Aware S-Meter, Community Contributions"), {
            {ChangeCategory::Feature, QStringLiteral("DIGI applet split into CAT, DAX, TCI, IQ"), QStringLiteral("Four independent applets replace the monolithic DIGI tile Each has its own toggle button, drag-reorder slot, and float/dock affordance TCI gains per-channel RX gain (TciRxGain1-4) and a decoupled T...")},
            {ChangeCategory::Feature, QStringLiteral("PGXL standby-aware S-Meter"), QStringLiteral("VU / S-Meter switches between barefoot and PGXL 2kW scales automatically In STANDBY the meter shows exciter FWDPWR on the barefoot scale In OPERATE the meter shows amp output on the 2kW scale Scale...")},
            {ChangeCategory::Feature, QStringLiteral("Refined HF Propagation Dashboard (#1626, community: jensenpat)"), QStringLiteral("Five color-coded metric cards for K, A, SFI, SSN, X-ray with plain-language summaries Teaching panels for Solar/Lunar (VIEW/HF/MOON) and VHF (AURORA/E-SKIP) Cropped lunar bitmap fills the frame; em...")},
            {ChangeCategory::Feature, QStringLiteral("Pan-follows-VFO with 5% edge threshold (#1476/#1477)"), QStringLiteral("Opt-in toggle; VFO slides freely through the center 90% of the pan Pan only shifts when the VFO crosses the 5% edge on either side")},
            {ChangeCategory::Feature, QStringLiteral("Memory dialog CSV bulk import"), QStringLiteral("Import memory channels from SmartSDR-compatible 22-column CSV with progress feedback Bulk selection and editing flow improvements (#1522)")},
            {ChangeCategory::Feature, QStringLiteral("RAC (Canada) band plan overlay"), QStringLiteral("")},
            {ChangeCategory::Feature, QStringLiteral("Audio Boost toggle"), QStringLiteral("Optional gain boost in Radio Setup for low-volume AGC audio")},
            {ChangeCategory::Feature, QStringLiteral("Configurable audio buffer cap"), QStringLiteral("User-tunable RX buffer size; DSP bypass for external TX paths")},
            {ChangeCategory::Feature, QStringLiteral("Per-slice VFO marker style toggles (#1526/#1614)"), QStringLiteral("Thin / thick / edges / hide choices per slice (restricted to CW/CWL modes)")},
            {ChangeCategory::Feature, QStringLiteral("Extended frequency line"), QStringLiteral("")},
            {ChangeCategory::Feature, QStringLiteral("rigctl TX PWR / SWR meters for WSJT-X and JTDX"), QStringLiteral("Expose transmit power and SWR over rigctld")},
            {ChangeCategory::Feature, QStringLiteral("StreamController TX audio source selector"), QStringLiteral("MOX and PTT actions in the StreamController plugin can pick DAX vs mic source")},
            {ChangeCategory::Feature, QStringLiteral("Smooth S-meter peak hold animation"), QStringLiteral("")},
            {ChangeCategory::BugFix, QStringLiteral("TCI audio reliability for WSJT-X / JTDX"), QStringLiteral("Fix TCI TX timing for WSJT-X (#1624) — 2048/48000 = 21.333ms cadence was rounding to 21ms Fix TCI audio for multi-slice: per-channel accumulation buffers (#1595/#1596) Fix TCI audio for WSJT-X: DAX...")},
            {ChangeCategory::BugFix, QStringLiteral("macOS Tahoe pop-out / re-dock UI freeze (#1344/#1345)"), QStringLiteral("Clears stale native window state on reparent")},
            {ChangeCategory::BugFix, QStringLiteral("rigctld shutdown stability"), QStringLiteral("Fix rigctld shutdown crash with active CAT clients (#1601) Schedule rigctld client sockets for deletion on server stop (#1605)")},
            {ChangeCategory::BugFix, QStringLiteral("PC Audio persistence and recovery"), QStringLiteral("Fix PC Audio mute state not persisting across restarts or sleep/wake (#1571/#1572) Fix PC Audio silence after Teams/Zoom reconfigures audio endpoint (#1569/#1570) Keep local audio sink off when PC ...")},
            {ChangeCategory::BugFix, QStringLiteral("Panadapter and spectrum"), QStringLiteral("Fix RF Gain echo-driven command loop (#1498/#1612) Fix RF gain / WNB controls affecting wrong panadapter (#1548) Fix VFO disappearing from pan area on startup (#1493/#1495) Preserve waterfall histo...")},
            {ChangeCategory::BugFix, QStringLiteral("FlexControl tuning jitter"), QStringLiteral("Resolve optimistic/confirmed frequency race that caused knob jitter (#1524)")},
            {ChangeCategory::BugFix, QStringLiteral("CW"), QStringLiteral("Fix CW macros requiring CWX window open and ESC not stopping TX (#1552/#1553) Fix CW decoder header jitter when WPM changes (#1546) Fix Autotune label leaking to non-CW modes and show marker row al...")},
            {ChangeCategory::BugFix, QStringLiteral("Mic and TX"), QStringLiteral("Fix Mic Gain resetting when toggling Processor state (#1573) Disable NR2 when Opus audio compression is active (#1597)")},
            {ChangeCategory::BugFix, QStringLiteral("TGXL and band selection"), QStringLiteral("Fix TGXL showing wrong SWR after tune and meter freeze (#1530/#1531) Map WWV/GEN to their band-stack slot indexes (#1540/#1211 follow-up) (#1616)")},
            {ChangeCategory::BugFix, QStringLiteral("TNF on macOS"), QStringLiteral("Split setGlobalEnabled into status and command paths so TNF tracks on macOS")},
            {ChangeCategory::BugFix, QStringLiteral("MQTT TLS with OpenSSL 3.5+ (#1483/#1484)"), QStringLiteral("")},
            {ChangeCategory::BugFix, QStringLiteral("Radio connection UX"), QStringLiteral("Better panadapter feedback during connect Larger connection dialog buttons and window Clarify disabled CAT/DAX menu items on unsupported platforms (#1556/#1557)")},
            {ChangeCategory::BugFix, QStringLiteral("Windows build"), QStringLiteral("Guard autoCatAction / autoDaxAction refs in placeholder-action loop (#1618/#1619) Guard m_hidCoalesceTimer reference behind HAVE_HIDAPI (#1606)")},
        }},
        {QStringLiteral("0.8.15"), QStringLiteral("2026-04-15"), QStringLiteral("Waterfall Scrollback, Pan-Follow Animation, TCI-via-DAX, Community Contributions"), {
            {ChangeCategory::Feature, QStringLiteral("Waterfall history scrollback (#1432, community: rfoust)"), QStringLiteral("20-minute ring buffer with scrub controls on the right-side time strip Pull up on the time strip to scroll back in time; LIVE button to return Paused viewport ages forward as new rows arrive; repro...")},
            {ChangeCategory::Feature, QStringLiteral("HF Propagation Dashboard"), QStringLiteral("Click K/A/SFI on the spectrum overlay to open a rich dashboard 3-day Kp forecast, band conditions, VHF, X-ray, solar wind, SDO solar images, lunar phase")},
            {ChangeCategory::Feature, QStringLiteral("Memory dialog search and CSV export (#1436, #1438, community: jensenpat)"), QStringLiteral("Search field filters memories by name as you type Arrow key navigation with Enter to apply; double-click for inline edit CSV export in SmartSDR-compatible 22-column format")},
            {ChangeCategory::Feature, QStringLiteral("5-8 pan layouts for FLEX-6700"), QStringLiteral("New layouts: 3h2, 2x3, 4h3, 2x4 for up to 8 panadapters")},
            {ChangeCategory::Feature, QStringLiteral("Active Slice Follows TX toggle"), QStringLiteral("Switch active/displayed slice when TX moves externally (e.g. WSJT-X) Mutually exclusive with TX Follows Active Slice; both can be off")},
            {ChangeCategory::Feature, QStringLiteral("TCI audio via DAX"), QStringLiteral("TCI audio feeds from DAX instead of main RX path Muting PC audio no longer kills TCI client audio DAX RX streams created/torn down automatically on audio_start/stop")},
            {ChangeCategory::Feature, QStringLiteral("Configurable frequency grid spacing"), QStringLiteral("Manual grid spacing option in spectrum overlay menu")},
            {ChangeCategory::Feature, QStringLiteral("System sleep inhibition"), QStringLiteral("Prevent system sleep while connected to radio (macOS/Windows/Linux)")},
            {ChangeCategory::Feature, QStringLiteral("Activate slice on passband click"), QStringLiteral("Left-click a slice passband to make it active")},
            {ChangeCategory::Feature, QStringLiteral("S-meter color gradient"), QStringLiteral("SmartSDR-style green→yellow→red gradient on VFO and slice flag S-meter bars")},
            {ChangeCategory::BugFix, QStringLiteral("Fix panadapter/VFO scroll jitter, blank spectrum, and floor level revert (#989, community: chibondking)"), QStringLiteral("Smooth 200ms pan-follow animation with QVariantAnimation Stale echo-back guard prevents pre-animation center from reversing animation Per-pan levelChanged signal prevents stale echo-backs from reve...")},
            {ChangeCategory::BugFix, QStringLiteral("Fix duplicate wirePanadapter connections (#989, community: chibondking)"), QStringLiteral("Bulk disconnect(this) guard replaces scattered per-signal disconnects")},
            {ChangeCategory::BugFix, QStringLiteral("Fix MQTT \"Socket is not connected\" on macOS"), QStringLiteral("Handle MOSQ_ERR_CONN_PENDING for non-blocking connect Handle ENOTCONN on Unix/macOS in vendored libmosquitto")},
            {ChangeCategory::BugFix, QStringLiteral("Audio liveness watchdog"), QStringLiteral("Restart RX stream after 15 seconds of no audio data (fixes silent audio loss after hours of idle)")},
            {ChangeCategory::BugFix, QStringLiteral("Fix SIGSEGV on band change with 2 panadapters"), QStringLiteral("Null-guard spectrumForSlice() during pan creation/destruction")},
            {ChangeCategory::BugFix, QStringLiteral("Fix manual grid spacing"), QStringLiteral("Grid lines respect user's manual choice; only labels thin at narrow spacing")},
            {ChangeCategory::BugFix, QStringLiteral("Fix Windows audio silence"), QStringLiteral("Remove IdleState restart logic that caused audio restart loops on Windows")},
            {ChangeCategory::BugFix, QStringLiteral("Fix K-index regex capturing UTC time"), QStringLiteral("Anchor on \"was\" keyword to avoid capturing UTC time as K value")},
            {ChangeCategory::BugFix, QStringLiteral("Fix DAX TX stream conflict with SmartSDR DAX on Windows"), QStringLiteral("DAX RX stream lifecycle managed by TCI server; streams cleaned up on disconnect")},
        }},
        {QStringLiteral("0.8.12.2"), QStringLiteral("2026-04-14"), QStringLiteral("Enhancements"), {
            {ChangeCategory::BugFix, QStringLiteral("Fix two-slice band-change crash"), QStringLiteral("Null-guard all spectrumForSlice() dereferences during pan creation/destruction")},
            {ChangeCategory::BugFix, QStringLiteral("Fix CW key/paddle not triggering TX"), QStringLiteral("Track CW key/paddle state in interlock handler so break-in keying isn't killed")},
            {ChangeCategory::BugFix, QStringLiteral("Fix popout panadapter frozen after reparent"), QStringLiteral("GPU render pipeline self-heals: re-initializes lazily on next render frame")},
            {ChangeCategory::BugFix, QStringLiteral("Fix WSJT-X DT drift on DAX RX audio after TX cycles"), QStringLiteral("PipeWireAudioBridge: silence fill during TX keeps pipe clock advancing VirtualAudioBridge: wall-clock-accurate silence fill (eliminates QTimer jitter)")},
            {ChangeCategory::BugFix, QStringLiteral("Fix CW auto-tune buttons not shown for SSDR+ users"), QStringLiteral("Set license flag before VFO widget builds filter buttons")},
            {ChangeCategory::BugFix, QStringLiteral("Fix FlexControl knob press not returning to frequency mode"), QStringLiteral("Knob press while in Volume/Power wheel mode returns to Frequency")},
            {ChangeCategory::BugFix, QStringLiteral("Fix audio stopping after idle/screensaver with USB devices"), QStringLiteral("Zombie sink watchdog, IdleState handler, device list monitor (Windows only) PipeWire-safe: WASAPI-specific recovery gated behind Q_OS_WIN")},
            {ChangeCategory::BugFix, QStringLiteral("Send graceful client disconnect before closing TCP"), QStringLiteral("Prevents Maestro lockup on reconnect with same GUIClientID")},
            {ChangeCategory::BugFix, QStringLiteral("Fix panadapter bandwidth limits"), QStringLiteral("Per-model pcap-verified values; 8400 correctly grouped as single-SCU")},
            {ChangeCategory::BugFix, QStringLiteral("Fix waterfall tooltip descriptions"), QStringLiteral("Black level and rate tooltips corrected")},
            {ChangeCategory::BugFix, QStringLiteral("Fix AppImage crash on Arch/Fedora"), QStringLiteral("Removed bundled OpenSSL from AppImage (ABI conflicts with host PipeWire)")},
        }},
        {QStringLiteral("0.8.12.1"), QStringLiteral("2026-04-14"), QStringLiteral(""), {
            {ChangeCategory::BugFix, QStringLiteral("Fix startup crash on Windows"), QStringLiteral("Bundle `libssl-3-x64.dll` and `libcrypto-3-x64.dll` in Windows installer and portable ZIP Bundle `libssl.so.3` and `libcrypto.so.3` in AppImage MQTT TLS support added in v0.8.12 linked OpenSSL dyna...")},
        }},
        {QStringLiteral("0.8.12"), QStringLiteral("2026-04-14"), QStringLiteral("Enhancements"), {
            {ChangeCategory::Feature, QStringLiteral("QSO audio recorder"), QStringLiteral("Client-side WAV recording with auto-record on TX and idle timeout Radio Side / Client Side mode selector in Radio Setup → Audio Playback of last recording through speaker with live RX muted during ...")},
            {ChangeCategory::Feature, QStringLiteral("TCI IQ stream support"), QStringLiteral("Stream DAX IQ data to TCI clients for panadapter/waterfall display Enables TCI Remote Android app (by ON7OFF) to show live spectrum")},
            {ChangeCategory::Feature, QStringLiteral("Profile-based memory channel filtering"), QStringLiteral("Filter memories by global profile in the Memory Channels dialog New memories auto-tagged with active profile name")},
            {ChangeCategory::Feature, QStringLiteral("Configuring AetherSDR Controls help guide"), QStringLiteral("Comprehensive 659-line offline guide covering keyboard shortcuts, FlexControl, MIDI, USB HID devices, Stream Deck, and serial PTT/CW (community: jensenpat)")},
            {ChangeCategory::BugFix, QStringLiteral("Fix XWayland GLX crash when opening child dialogs"), QStringLiteral("Auto-detect Wayland sessions and set QT_QPA_PLATFORM=wayland Add AETHER_NO_GPU=1 runtime fallback for software rendering PII redaction regex objects survive abnormal teardown")},
            {ChangeCategory::BugFix, QStringLiteral("Fix TCI audio corruption"), QStringLiteral("Header length now reports per-channel sample count per TCI v2.0 spec Detect mono vs stereo TX audio from WSJT-X Handle string format names in audio_stream_sample_type negotiation")},
            {ChangeCategory::BugFix, QStringLiteral("Fix antenna dropdown not populating in overlay menu"), QStringLiteral("Moved antenna list wiring to per-pan in wirePanadapter() with initial sync")},
            {ChangeCategory::BugFix, QStringLiteral("Fix band button reselection"), QStringLiteral("Removed same-band guard that blocked re-selecting GEN/transverter bands")},
            {ChangeCategory::BugFix, QStringLiteral("Fix frequency offset calibration Start button"), QStringLiteral("Added missing `radio calibrate` command")},
            {ChangeCategory::BugFix, QStringLiteral("Filter RX-only antenna ports from TX selector"), QStringLiteral("Skip ports starting with \"RX\" in TX antenna dropdown")},
            {ChangeCategory::BugFix, QStringLiteral("Guard QImage::scaled() null waterfall"), QStringLiteral("Prevents permanent waterfall loss during rapid resize on compositing WMs")},
            {ChangeCategory::BugFix, QStringLiteral("Fix audio input device combo"), QStringLiteral("Shows active device instead of system default in Radio Setup")},
        }},
        {QStringLiteral("0.8.11"), QStringLiteral("2026-04-12"), QStringLiteral("Contributors"), {
            {ChangeCategory::Feature, QStringLiteral("MQTT station device integration"), QStringLiteral("Subscribe to MQTT topics and display device status in the applet panel User-defined publish buttons for rotator, antenna, and device control Panadapter overlay: prefix topics with * to show values ...")},
            {ChangeCategory::BugFix, QStringLiteral("Antenna Genius race condition"), QStringLiteral("Always send saved antenna preference on band change; don't skip when AG coincidentally reports the correct antenna first (community: scott-mss)")},
            {ChangeCategory::BugFix, QStringLiteral("Heat map QPainter fallback"), QStringLiteral("Added heat map gradient to the QPainter spectrum path for systems where GPU rendering is unavailable (Windows Intel iGPU)")},
            {ChangeCategory::BugFix, QStringLiteral("DAX channel persistence"), QStringLiteral("Per-slice DAX channel saved/restored across restarts via AppSettings DAX IQ channel tracked on PanadapterModel from radio status; overlay synced")},
            {ChangeCategory::BugFix, QStringLiteral("macOS DAX audio corrupted"), QStringLiteral("VirtualAudioBridge and PCC_IF_NARROW DAX path updated to float32 Fixes VARA HF / WSJT-X audio corruption on macOS (community: pepefrog1234)")},
            {ChangeCategory::BugFix, QStringLiteral("VFO filter label mismatch"), QStringLiteral("VFO header now shows filter width (hi − lo), not filter edge value")},
            {ChangeCategory::BugFix, QStringLiteral("TX power meter jitter"), QStringLiteral("Asymmetric smoothing: fast attack, slow decay for stable TX meter")},
            {ChangeCategory::BugFix, QStringLiteral("Oscillator live-update"), QStringLiteral("Radio Setup oscillator status updates live when external 10 MHz reference is plugged/unplugged (community fix: NF0T)")},
            {ChangeCategory::BugFix, QStringLiteral("K-index rounding"), QStringLiteral("Parse K/A-index as double for decimal XML values from hamqsl.com")},
            {ChangeCategory::BugFix, QStringLiteral("IQ Enable button state"), QStringLiteral("Reset DAX IQ buttons on reconnect (streams are per-session)")},
            {ChangeCategory::BugFix, QStringLiteral("File dialog z-order"), QStringLiteral("Background image chooser uses spectrum widget as parent")},
            {ChangeCategory::BugFix, QStringLiteral("Status bar cleanup"), QStringLiteral("Removed redundant \"RADIO:\" prefix label")},
            {ChangeCategory::BugFix, QStringLiteral("Cursor button removed"), QStringLiteral("Removed from Display panel (redundant with Tune Guides feature)")},
            {ChangeCategory::BugFix, QStringLiteral("NR2 + Opus documented"), QStringLiteral("Help docs updated: NR2 incompatible with Opus, use RN2/NR4/DFNR instead")},
            {ChangeCategory::BugFix, QStringLiteral("macOS NSLocalNetworkUsageDescription"), QStringLiteral("Required for UDP broadcast radio discovery on modern macOS")},
        }},
        {QStringLiteral("0.8.10"), QStringLiteral("2026-04-11"), QStringLiteral("Contributors"), {
            {ChangeCategory::Feature, QStringLiteral("Pop-out panadapters (community: rfoust)"), QStringLiteral("SmartSDR-style title bar with pop-out (⬈), maximize (□), and close (×) icons Right-click \"Pop out\" in spectrum context menu to detach into floating window Dock back via title bar button; multi-pan ...")},
            {ChangeCategory::Feature, QStringLiteral("VPN source-path binding (#1218, community: jensenpat)"), QStringLiteral("Manual connections can select a specific network interface for TCP+UDP Auto-follows TCP path for VPN/routed radios on multihomed systems Per-target profile persistence in RoutedProfilesJson Network...")},
            {ChangeCategory::Feature, QStringLiteral("Show Tune Guides"), QStringLiteral("Visual tune guides in spectrum/waterfall context menu")},
            {ChangeCategory::Feature, QStringLiteral("Reset settings cleanup flow"), QStringLiteral("Guided settings reset from Help menu")},
            {ChangeCategory::Feature, QStringLiteral("Expanded Slice Troubleshooter"), QStringLiteral("Additional diagnostics in troubleshooter dialog")},
            {ChangeCategory::Feature, QStringLiteral("Center startup slice"), QStringLiteral("Slice centered on panadapter at launch")},
            {ChangeCategory::Feature, QStringLiteral("Center go-to-frequency"), QStringLiteral("Frequency entry centers pan on target")},
            {ChangeCategory::BugFix, QStringLiteral("AGC off level in VFO widget (#1217, community: jensenpat)"), QStringLiteral("AGC slider routes to `agc_off_level` when AGC mode is Off Tooltip and accessible name update to match active mode")},
            {ChangeCategory::BugFix, QStringLiteral("AGC off level in RX applet"), QStringLiteral("Same fix for the RX applet AGC slider")},
            {ChangeCategory::BugFix, QStringLiteral("CW decoder broken by float32"), QStringLiteral("Float32→int16 conversion at ggmorse boundary")},
            {ChangeCategory::BugFix, QStringLiteral("DAX audio path"), QStringLiteral("DAX emission converted to float32 for TCI/PipeWire clients PipeWire bridge reads float32 input correctly (fixes pumping audio)")},
            {ChangeCategory::BugFix, QStringLiteral("CW autotune stop"), QStringLiteral("Separated one-shot and loop commands; stop now sends `int=0`")},
            {ChangeCategory::BugFix, QStringLiteral("TX audio broken by float32"), QStringLiteral("Restored TX mic path to int16; separate RX/TX audio formats")},
            {ChangeCategory::BugFix, QStringLiteral("Auto-save command"), QStringLiteral("Uses correct `profile autosave on/off` command; reads from radio status Support dialog made modeless")},
            {ChangeCategory::BugFix, QStringLiteral("Multi-Flex UDP port conflict"), QStringLiteral("Countdown fallback tries ports 4991, 4990, 4989...")},
            {ChangeCategory::BugFix, QStringLiteral("Band menu reopen"), QStringLiteral("Fixed band menu not reopening after close")},
            {ChangeCategory::BugFix, QStringLiteral("Stop persisting LCK"), QStringLiteral("Lock state no longer saved across restarts")},
            {ChangeCategory::BugFix, QStringLiteral("What's New auto-popup removed"), QStringLiteral("No longer pops up on launch; available via Help → What's New")},
        }},
        {QStringLiteral("0.8.9"), QStringLiteral("2026-04-11"), QStringLiteral("Float32 Audio, Display Panel Redesign, Community PR Blitz"), {
            {ChangeCategory::Feature, QStringLiteral("Float32 audio pipeline"), QStringLiteral("End-to-end float32 from radio to speaker — eliminates distortion at high AF slider levels All DSP filters (NR2, RN2, NR4, DFNR, BNR) now process in native float32 Resampler converted to float32 I/O...")},
            {ChangeCategory::Feature, QStringLiteral("Adjustable spectrum trace thickness"), QStringLiteral("GPU triangle strip expansion for variable-width spectrum line (0.5–5.0 px) Line Width slider in Display panel, persisted per-pan Setting to \"Off\" hides trace entirely, showing only fill gradient")},
            {ChangeCategory::Feature, QStringLiteral("Display panel redesign"), QStringLiteral("Compact layout: sliders grouped at top, toggle buttons condensed into labeled row Heat Map, Grid, Weighted Average, Cursor Freq as toggle buttons with color state Removed separator between FFT and ...")},
            {ChangeCategory::Feature, QStringLiteral("Band Stack panel with frequency bookmarks"), QStringLiteral("Vertical bookmark strip alongside panadapter with FRStack-style save/restore Saves frequency, mode, filter, antennas, AGC, volume, NB/NR/WNB per bookmark Color-coded by band plan segment, persisted...")},
            {ChangeCategory::Feature, QStringLiteral("CPU and memory indicator"), QStringLiteral("Cross-platform process CPU and RSS in status bar")},
            {ChangeCategory::Feature, QStringLiteral("Panadapter zoom buttons"), QStringLiteral("−/+ buttons and macOS trackpad pinch-to-zoom (community: rfoust)")},
            {ChangeCategory::Feature, QStringLiteral("Collapsible VFO flags"), QStringLiteral("Click slice letter badge to collapse/expand (community: rfoust)")},
            {ChangeCategory::Feature, QStringLiteral("VU meter popout"), QStringLiteral("S-Meter can be detached into floating window (community: chibondking)")},
            {ChangeCategory::Feature, QStringLiteral("Off-screen slice context menu"), QStringLiteral("Right-click off-screen indicators: Close, Move Here, Center (community: rfoust)")},
            {ChangeCategory::BugFix, QStringLiteral("Radio-authoritative band changes"), QStringLiteral("Band buttons use `display pan set band=` — radio manages band stack Bandwidth drag no longer snaps pan center to VFO")},
            {ChangeCategory::BugFix, QStringLiteral("PC Audio toggle vs StreamDeck TCI"), QStringLiteral("StreamDeck no longer overrides user's explicit PC Audio off toggle")},
            {ChangeCategory::BugFix, QStringLiteral("VFO drag snap"), QStringLiteral("Passband drag moves VFO relative to grab point, no initial frequency jump")},
            {ChangeCategory::BugFix, QStringLiteral("DFM/FM filter centering"), QStringLiteral("FM/NFM/DFM filters centered symmetrically instead of USB-style offset")},
            {ChangeCategory::BugFix, QStringLiteral("Multi-pan band change crash"), QStringLiteral("Null guards for spectrum() during pan reassignment")},
            {ChangeCategory::BugFix, QStringLiteral("HGauge meter colors"), QStringLiteral("Green/yellow/red zones with white labels for readability Yellow caution zones added to all gauges")},
            {ChangeCategory::BugFix, QStringLiteral("Logging checkboxes not persisting"), QStringLiteral("Fixed dots in XML keys; default Discovery/Commands/Status to enabled")},
            {ChangeCategory::BugFix, QStringLiteral("Status bar RADIO label"), QStringLiteral("Renamed STATION to RADIO; station name defaults to OS hostname for multi-flex")},
            {ChangeCategory::BugFix, QStringLiteral("Aurora power meter"), QStringLiteral("AU- models show 0-600W scale instead of 0-120W")},
            {ChangeCategory::BugFix, QStringLiteral("FlexControl stale QSY"), QStringLiteral("Reset target on external frequency change; added AGC/volume button actions")},
            {ChangeCategory::BugFix, QStringLiteral("macOS CoreAudio crashes"), QStringLiteral("Guard stop() in StoppedState; route device switch to audio thread; restart on silent stop")},
            {ChangeCategory::BugFix, QStringLiteral("PROC turning off on VOX toggle"), QStringLiteral("Investigated: cannot reproduce on firmware v4.1.5")},
            {ChangeCategory::BugFix, QStringLiteral("Additional fixes"), QStringLiteral("Mic bias/boost UI update (#1045) DFNR reset button (#1055) Support bundle firmware version (#1057) GuardedSlider lock bypass (#1060) VFO filter BW for DIGU/DIGL (#1066) CMake GPU/DFNR diagnostics (...")},
            {ChangeCategory::Infrastructure, QStringLiteral("Vendored RADE Opus snapshot"), QStringLiteral("Offline builds — no network download needed (community: jensenpat)")},
            {ChangeCategory::Infrastructure, QStringLiteral("A-index in propagation overlay"), QStringLiteral("Shows alongside K-index and SFI (community: jensenpat)")},
        }},
        {QStringLiteral("0.8.8"), QStringLiteral("2026-04-10"), QStringLiteral("Band Stack Panel, Radio-Authoritative Bands, Cross-Platform CPU Monitor"), {
            {ChangeCategory::Feature, QStringLiteral("Band Stack panel"), QStringLiteral("Vertical bookmark strip alongside the panadapter, toggled by 3-dot icon in status bar Click \"+\" to save current frequency, click a bookmark to recall, right-click to delete Saves/restores: frequenc...")},
            {ChangeCategory::Feature, QStringLiteral("CPU and memory indicator"), QStringLiteral("Process CPU usage and RSS memory in the status bar Cross-platform: getrusage (Linux/macOS), GetProcessTimes (Windows) Color-coded: blue < 50%, yellow 50-79%, red 80%+")},
            {ChangeCategory::Feature, QStringLiteral("Panadapter zoom buttons"), QStringLiteral("−/+ buttons below S/B for stepped bandwidth zoom (1.5× per click) macOS trackpad pinch-to-zoom with cursor-anchored zooming Per-radio bandwidth limits (FLEX-8600: 14 MHz, 8400: 7 MHz, others: 5.4 MHz)")},
            {ChangeCategory::Feature, QStringLiteral("Grid lines toggle"), QStringLiteral("Show/hide grid lines via Display panel toggle Persisted per-panadapter")},
            {ChangeCategory::Feature, QStringLiteral("AGC Threshold tooltip"), QStringLiteral("Dynamic tooltip showing current AGC threshold value on slider hover")},
            {ChangeCategory::BugFix, QStringLiteral("Radio-authoritative band changes"), QStringLiteral("Band buttons now use `display pan set band=` — radio manages its own band stack Removed ~200 lines of client-side BandStack AppSettings save/restore Bandwidth drag no longer snaps pan center to VFO")},
            {ChangeCategory::BugFix, QStringLiteral("FlexControl stale QSY + missing buttons"), QStringLiteral("Dial no longer jumps back to previous band after external frequency change Added ToggleAgc, VolumeUp, VolumeDown to FlexControl button actions")},
            {ChangeCategory::BugFix, QStringLiteral("TCI PC Audio button state"), QStringLiteral("PC Audio button now reflects TCI-forced stream state Button returns to saved preference when TCI disconnects")},
            {ChangeCategory::BugFix, QStringLiteral("TGXL connection errors"), QStringLiteral("Shows error text in Peripherals tab when TCP connection fails Pre-fills radio-discovered TGXL IP as fallback")},
            {ChangeCategory::BugFix, QStringLiteral("Logging checkboxes not persisting"), QStringLiteral("Fixed: dots in category IDs were rejected by XML element name validator Checkboxes now initialize from saved state on dialog open Discovery, Commands, Status default to enabled")},
            {ChangeCategory::BugFix, QStringLiteral("Mic bias/boost UI not updating"), QStringLiteral("Added missing phoneStateChanged() emit after optimistic update")},
            {ChangeCategory::BugFix, QStringLiteral("DFNR reset button"), QStringLiteral("Wired onReset callback in both right-click popup and DSP dialog")},
            {ChangeCategory::BugFix, QStringLiteral("Support bundle firmware version"), QStringLiteral("Reports both protocol version and firmware version")},
            {ChangeCategory::BugFix, QStringLiteral("macOS CoreAudio crash"), QStringLiteral("Guard stop() calls against StoppedState when switching audio devices")},
            {ChangeCategory::BugFix, QStringLiteral("GuardedSlider lock bypass"), QStringLiteral("Block mouse drag on sliders when controls are locked (was only blocking wheel)")},
            {ChangeCategory::BugFix, QStringLiteral("VFO filter BW label for DIGU/DIGL"), QStringLiteral("Show actual bandwidth instead of upper filter edge for digital modes")},
            {ChangeCategory::BugFix, QStringLiteral("CMake diagnostics for GPU/DFNR"), QStringLiteral("Properly disable GPU rendering when Qt6GuiPrivate not found Improved DFNR disabled message pointing to setup-deepfilter.sh")},
            {ChangeCategory::BugFix, QStringLiteral("Windows invisible cursor"), QStringLiteral("Re-apply cursor in QRhiWidget::initialize() after HWND creation")},
            {ChangeCategory::BugFix, QStringLiteral("Manual radio probe identity"), QStringLiteral("Read S radio status line for real model/serial/nickname on VPN connections")},
        }},
        {QStringLiteral("0.8.7"), QStringLiteral("2026-04-09"), QStringLiteral("Removed"), {
            {ChangeCategory::Feature, QStringLiteral("Elgato Stream Deck plugin"), QStringLiteral("Native plugin for the official Elgato Stream Deck app (macOS/Windows) 43 actions: TX, bands, modes, DSP, audio, slice controls, DVK Pre-built distributable — download and double-click to install St...")},
            {ChangeCategory::Feature, QStringLiteral("Help guides"), QStringLiteral("Understanding Noise Cancellation guide (NR, NR2, RN2, NR4, DFNR, BNR comparison) Configuring Data Modes guide (CAT, TCI, DAX walkthroughs for WSJT-X, JTDX, fldigi)")},
            {ChangeCategory::Feature, QStringLiteral("DEXP persistence"), QStringLiteral("Optimistic updates for DEXP on/off and level (no status echo from radio) Saved to AppSettings on change, restored on connect")},
            {ChangeCategory::BugFix, QStringLiteral("TCI audio absent when using Radio Audio"), QStringLiteral("remote_audio_rx stream always created on connect regardless of PC Audio state PC Audio toggle now only controls local playback, never removes the stream TCI clients receive audio whether PC Audio i...")},
            {ChangeCategory::BugFix, QStringLiteral("Keyboard tuning fixes"), QStringLiteral("Right arrow tuned wrong direction (stale base frequency) Arrow keys now auto-repeat when held for continuous tuning Uses tuneAndRecenter() for proper auto-center behavior")},
            {ChangeCategory::BugFix, QStringLiteral("TX Delay not persisting"), QStringLiteral("Wired editingFinished signals for all interlock timing fields (ACC TX, TX Delay, RCA TX1/2/3, Timeout)")},
            {ChangeCategory::BugFix, QStringLiteral("Duplicate Preferences menu entry"), QStringLiteral("Set PreferencesRole directly on Radio Setup action — one entry on all platforms")},
            {ChangeCategory::BugFix, QStringLiteral("TUN/AMP/AG applet state not persisting"), QStringLiteral("Save checked state to AppSettings on toggle (was missing for hardware-conditional applets)")},
            {ChangeCategory::BugFix, QStringLiteral("Mic bias/boost not reflecting toggle"), QStringLiteral("Optimistic update for mic bias and mic boost (radio sends no status echo)")},
            {ChangeCategory::BugFix, QStringLiteral("RADE status label not appearing"), QStringLiteral("Used vfoWidget(sliceId) instead of null vfoWidget() alias Label now inline with frequency display instead of separate row")},
            {ChangeCategory::BugFix, QStringLiteral("Finer mouse-wheel steps for Controls sliders"), QStringLiteral("Use singleStep (1) instead of pageStep (10) for slider wheel events")},
            {ChangeCategory::BugFix, QStringLiteral("DFNR model not found on system install"), QStringLiteral("Added XDG, /usr/share, /usr/local/share search paths CMake install() rule for model file")},
            {ChangeCategory::BugFix, QStringLiteral("Intel Mac GPU rendering"), QStringLiteral("Disabled QRhiWidget on Intel Mac DMG builds (rendering issues on older Metal/OpenGL)")},
        }},
        {QStringLiteral("0.8.6"), QStringLiteral("2026-04-08"), QStringLiteral("DFNR AI Noise Reduction, Propagation Overlay, Community PR Blitz"), {
            {ChangeCategory::Feature, QStringLiteral("DFNR (DeepFilterNet3) AI noise reduction"), QStringLiteral("Fifth client-side NR option using deep neural filtering CPU-only, ~10ms latency, excellent in high-noise HF environments Configurable attenuation limit and post-filter beta Full mutual exclusion wi...")},
            {ChangeCategory::Feature, QStringLiteral("HF propagation conditions overlay"), QStringLiteral("K-index and Solar Flux Index displayed on panadapter Fetched from hamqsl.com, cached hourly, persists across restarts Toggle via View > Propagation Conditions")},
            {ChangeCategory::Feature, QStringLiteral("Peripherals tab in Radio Setup"), QStringLiteral("Manual IP connect for TGXL, PGXL, and Antenna Genius Auto-connect on radio connect, live status indicators Pre-fills IP/port from discovered connections")},
            {ChangeCategory::Feature, QStringLiteral("License Info section in Radio Setup"), QStringLiteral("Shows subscription tier (SmartSDR/SmartSDR+/SmartSDR+ EA) Radio ID, expiration date, licensed firmware version Parses all 3 license tiers from subscription status messages")},
            {ChangeCategory::Feature, QStringLiteral("UI scale keyboard shortcuts"), QStringLiteral("Ctrl+=/Ctrl+- zoom in/out, Ctrl+0 reset to 100% Restart prompt to apply scale change")},
            {ChangeCategory::Feature, QStringLiteral("Reset to Defaults button"), QStringLiteral("One-click reset of all display settings Clear button restores default logo background")},
            {ChangeCategory::Feature, QStringLiteral("Autostart immediate toggle"), QStringLiteral("CAT, DAX, rigctld start/stop immediately when toggled")},
            {ChangeCategory::BugFix, QStringLiteral("TX audio always Opus"), QStringLiteral("Radio requires Opus for remote_audio_tx regardless of setting Fixed regression where SmartLink TX required DAX ON")},
            {ChangeCategory::BugFix, QStringLiteral("Crash with 2+ panadapters"), QStringLiteral("Per-stream waterfall frame assembly prevents cross-stream corruption")},
            {ChangeCategory::BugFix, QStringLiteral("Crash on VPN connect"), QStringLiteral("UDP stream setup result check with graceful disconnect on failure 10-second health check warns if no VITA-49 data received")},
            {ChangeCategory::BugFix, QStringLiteral("Floating applets losing state"), QStringLiteral("Four distinct bugs fixed: toggle off/on, window drift, WM centering race, shutdown race P/CW applet '/' in ID was silently corrupting entire settings file")},
            {ChangeCategory::BugFix, QStringLiteral("FlexControl VFO jitter"), QStringLiteral("Track absolute target frequency instead of re-reading slice during coalesce")},
            {ChangeCategory::BugFix, QStringLiteral("Serial PTT/CW non-functional on macOS"), QStringLiteral("QSerialPort parent ownership for proper moveToThread behavior")},
            {ChangeCategory::BugFix, QStringLiteral("Space bar triggering Tune when shortcuts disabled"), QStringLiteral("Space always consumed to prevent accidental button activation")},
            {ChangeCategory::BugFix, QStringLiteral("RADE selecting DIGL on 60m"), QStringLiteral("Uses band definitions for correct sideband (60m is USB)")},
            {ChangeCategory::BugFix, QStringLiteral("FM tone/repeater UI not updating after memory recall"), QStringLiteral("Normalize tone mode/value/offset to match combo box item data")},
            {ChangeCategory::BugFix, QStringLiteral("macOS menu Preferences opened Shortcuts dialog"), QStringLiteral("Explicit QAction::PreferencesRole wiring for Radio Setup")},
            {ChangeCategory::BugFix, QStringLiteral("macOS DMG workflow YAML parse error"), QStringLiteral("Indented heredoc XML to fix GitHub Actions YAML parser")},
            {ChangeCategory::BugFix, QStringLiteral("FLEX-6500 capped at 2 panadapters"), QStringLiteral("Added FLEX-6500 to dual-SCU model list for 4 pan support")},
        }},
        {QStringLiteral("0.8.5"), QStringLiteral("2026-04-06"), QStringLiteral(""), {
            {ChangeCategory::Feature, QStringLiteral("Poppable applets"), QStringLiteral("Float/dock applets with persistent geometry across restarts")},
            {ChangeCategory::Feature, QStringLiteral("StreamController plugin full action set"), QStringLiteral("40+ TCI actions for Elgato Stream Deck")},
            {ChangeCategory::Feature, QStringLiteral("DXLab SpotCollector integration"), QStringLiteral("UDP listener for spot forwarding")},
            {ChangeCategory::Feature, QStringLiteral("Peak hold reset button"), QStringLiteral("Tie hold time to decay presets")},
        }},
        {QStringLiteral("0.8.4"), QStringLiteral("2026-04-06"), QStringLiteral(""), {
            {ChangeCategory::Feature, QStringLiteral("NR4 (Specbleach) noise reduction"), QStringLiteral("AetherDSP settings dialog with NR4 controls and tooltips NR2 gain/NPE/AE filter controls ported from WDSP")},
            {ChangeCategory::Feature, QStringLiteral("Intel Mac DMG build"), QStringLiteral("Qt built from source for macOS 13 (Ventura) support Cached Qt source builds in CI")},
            {ChangeCategory::Feature, QStringLiteral("Waterfall color scheme selection"), QStringLiteral("Multiple color schemes with persistence")},
            {ChangeCategory::Feature, QStringLiteral("Offline help system"), QStringLiteral("Operator guides bundled with the app")},
            {ChangeCategory::Feature, QStringLiteral("Compact DSP overlay"), QStringLiteral("Toggle-only buttons in single row at spectrum top")},
        }},
        {QStringLiteral("0.8.3"), QStringLiteral("2026-04-05"), QStringLiteral(""), {
            {ChangeCategory::Feature, QStringLiteral("Radio hardware meter applet"), QStringLiteral("PA temperature, supply voltage, fan speed display")},
            {ChangeCategory::Feature, QStringLiteral("Slice troubleshooting diagnostics"), QStringLiteral("Debug dialog for slice state inspection")},
            {ChangeCategory::Feature, QStringLiteral("Spot lifetime slider"), QStringLiteral("Non-linear stepped slider for shorter spot lifetimes")},
        }},
        {QStringLiteral("0.8.2"), QStringLiteral("2026-04-05"), QStringLiteral("Build System"), {
            {ChangeCategory::Feature, QStringLiteral("PGXL direct connection"), QStringLiteral("Direct TCP connection to PowerGeniusXL on port 9008 Amplifier applet: drain current, mains voltage, MEffA status OPERATE/STANDBY button matching TGXL style S-Meter power gauge fed from PGXL peakfwd...")},
            {ChangeCategory::Feature, QStringLiteral("UTC date in status bar"), QStringLiteral("Stacked date (yyyy-MM-dd) above time in status bar")},
            {ChangeCategory::Feature, QStringLiteral("4m band support"), QStringLiteral("70 MHz band definition for FLEX-6700 XVTR (Region 1)")},
            {ChangeCategory::Feature, QStringLiteral("Memory spots on panadapter"), QStringLiteral("Saved memories as clickable spots, toggle in SpotHub Display")},
            {ChangeCategory::BugFix, QStringLiteral("PROC/compander fix"), QStringLiteral("PROC button now sends compander commands (was speech_processor) NOR/DX/DX+ slider sends compander_level Mic profile PROC level restores correctly")},
            {ChangeCategory::BugFix, QStringLiteral("S-Meter reads 0W during TX with PGXL"), QStringLiteral("Exciter txMetersChanged skipped when amplifier present S-Meter TX mode driven from PGXL state, not moxChanged")},
            {ChangeCategory::BugFix, QStringLiteral("GPU rendering polish"), QStringLiteral("Cursor frequency label restored in GPU mode WNB/RF gain indicators update correctly (markOverlayDirty) Waterfall syncs on band change Fill gradient matches QPainter fallback (bright at line, dark a...")},
            {ChangeCategory::BugFix, QStringLiteral("Batch memory spot removal"), QStringLiteral("Signal blocked during loop, single rebuild after")},
            {ChangeCategory::BugFix, QStringLiteral("Dark/Light theme toggle removed"), QStringLiteral("Placeholder menu item was confusing users")},
        }},
        {QStringLiteral("0.8.1"), QStringLiteral("2026-04-05"), QStringLiteral("Build System"), {
            {ChangeCategory::BugFix, QStringLiteral("GPU rendering on macOS Metal"), QStringLiteral("Set Metal API explicitly and WA_NativeWindow for QRhiWidget QRhiWidget in a RasterSurface hierarchy silently fails without a native NSView macOS workflows switched from aqtinstall to Homebrew-only ...")},
            {ChangeCategory::BugFix, QStringLiteral("Memory spots on panadapter"), QStringLiteral("Radio memories displayed as clickable spots on the panadapter Click to tune + apply mode/step/filter from saved memory Toggle in SpotHub Display settings Full sync: add/edit/remove in Memory dialog...")},
        }},
        {QStringLiteral("0.8.0"), QStringLiteral("2026-04-04"), QStringLiteral("GPU-Accelerated Rendering — The Performance Release"), {
            {ChangeCategory::Feature, QStringLiteral("TCI WebSocket server"), QStringLiteral("Full TCI v2.0 protocol: 72 command handlers over a single WebSocket connection CAT control, RX/TX audio streaming with r8brain resampling, IQ streaming Sensor telemetry, CW keyer, spot injection DI...")},
            {ChangeCategory::Feature, QStringLiteral("Customizable filter width presets"), QStringLiteral("Right-click any filter button to set custom width in Hz Per-mode persistence, synced between VFO widget and RX applet")},
            {ChangeCategory::Feature, QStringLiteral("Scrollable stepped controls"), QStringLiteral("Step size, TX Low Cut/High Cut, RTTY Mark/Space respond to scroll wheel")},
            {ChangeCategory::Feature, QStringLiteral("PA temperature unit toggle"), QStringLiteral("Click PA temp label to toggle °F/°C")},
            {ChangeCategory::BugFix, QStringLiteral("RTTY mark/space overlay"), QStringLiteral("RF_frequency in RTTY mode is the mark (radio applies IF shift) Green dashed M line, red dashed S line (MMTTY convention) VFO flag offset past filter edge, \"Shift\" → \"Space\" label")},
            {ChangeCategory::BugFix, QStringLiteral("Combo box scroll wheel regression"), QStringLiteral("Popup visibility check: ignore wheel when closed, allow when open")},
            {ChangeCategory::BugFix, QStringLiteral("Step size not applied on restart"), QStringLiteral("syncStepFromSlice missing emit stepSizeChanged")},
            {ChangeCategory::BugFix, QStringLiteral("Clock 00:00:00z with GPS no lock"), QStringLiteral("GPS time only used when GPSDO installed AND locked")},
            {ChangeCategory::BugFix, QStringLiteral("DAX TX latency on PipeWire"), QStringLiteral("2KB pipe buffer, 5ms precise timer, drain loop")},
            {ChangeCategory::BugFix, QStringLiteral("AI-assisted issue reporter"), QStringLiteral("Bug report + feature request templates, duplicate check")},
            {ChangeCategory::BugFix, QStringLiteral("RADE DSP guard"), QStringLiteral("NR2/RN2/BNR disabled when RADE active")},
            {ChangeCategory::BugFix, QStringLiteral("RADE RX audio fix"), QStringLiteral("Prevents VITA-49 audio fighting RADE decoded speech")},
            {ChangeCategory::BugFix, QStringLiteral("DXCC ADIF parsing"), QStringLiteral("Static regex caching bug, band normalization, mode inference")},
            {ChangeCategory::BugFix, QStringLiteral("Shortcut dedup"), QStringLiteral("Duplicate key bindings resolved on load")},
        }},
        {QStringLiteral("0.7.19"), QStringLiteral("2026-04-04"), QStringLiteral("Community Contributions"), {
            {ChangeCategory::Feature, QStringLiteral("TCI WebSocket server"), QStringLiteral("Full TCI v2.0 protocol: 72 command handlers over a single WebSocket connection CAT control, RX/TX audio streaming, IQ streaming, CW keying, spot injection Per-client audio format negotiation via r8...")},
            {ChangeCategory::Feature, QStringLiteral("Customizable filter width presets"), QStringLiteral("Right-click any filter button to set a custom width in Hz Exact value stored, display rounded (e.g. 1234 → 1.2K) Per-mode persistence via AppSettings VFO widget (8 presets) and RX applet (6 presets...")},
            {ChangeCategory::Feature, QStringLiteral("Scrollable stepped controls"), QStringLiteral("Step size, TX Low Cut, TX High Cut respond to mouse scroll wheel RTTY Mark and Space labels respond to mouse scroll wheel")},
            {ChangeCategory::BugFix, QStringLiteral("RTTY mark/space overlay at wrong frequency"), QStringLiteral("RF_frequency in RTTY mode is the mark (radio applies IF shift), not the carrier Green dashed mark line, red dashed space line (MMTTY convention) VFO center line hidden in RTTY/DIGL, replaced by M/S...")},
            {ChangeCategory::BugFix, QStringLiteral("Combo box scroll wheel regression"), QStringLiteral("GuardedComboBox now checks popup visibility: ignore wheel when closed, allow when open Fixes both accidental value changes and broken dropdown scrolling")},
            {ChangeCategory::BugFix, QStringLiteral("Step size not applied to scroll-to-tune on restart"), QStringLiteral("syncStepFromSlice was missing emit stepSizeChanged — SpectrumWidget kept stale value")},
            {ChangeCategory::BugFix, QStringLiteral("Clock showing 00:00:00z when GPS has no lock"), QStringLiteral("GPS time only used when GPSDO is installed AND locked, otherwise system UTC")},
            {ChangeCategory::BugFix, QStringLiteral("AI-assisted issue reporter improvements"), QStringLiteral("Supports both bug reports and feature requests Mandatory duplicate check, acceptance criteria section Split submit buttons: \"Submit Your Idea\" / \"Report a Bug\" with correct templates")},
            {ChangeCategory::BugFix, QStringLiteral("DAX TX latency on Linux/PipeWire"), QStringLiteral("Reduced pipe buffer to 2KB, 5ms precise timer, drain loop")},
            {ChangeCategory::BugFix, QStringLiteral("RADE DSP guard"), QStringLiteral("Client-side DSP (NR2/RN2/BNR) disabled when RADE is active")},
        }},
        {QStringLiteral("0.7.18.6"), QStringLiteral("2026-04-04"), QStringLiteral("Native StreamDeck, HID Encoders & UX Polish"), {
            {ChangeCategory::Feature, QStringLiteral("Native StreamDeck support"), QStringLiteral("Auto-detect all Elgato StreamDeck models (Mini through Plus XL) Live key rendering at 10 Hz: frequency, mode, band, TX state, DSP toggles Button actions: band select, mode select, SPLIT, LOCK, MUTE...")},
            {ChangeCategory::Feature, QStringLiteral("USB HID encoder support"), QStringLiteral("Icom RC-28, Griffin PowerMate, Contour ShuttleXpress/ShuttlePro v2 Auto-detect, 5ms poll, hotplug reconnect, configurable buttons udev rules for non-root access")},
            {ChangeCategory::Feature, QStringLiteral("Scrollable RIT/XIT controls"), QStringLiteral("Scroll wheel over Hz label adjusts by 10 Hz steps")},
            {ChangeCategory::Feature, QStringLiteral("Filter passband edge drag"), QStringLiteral("Click near left/right edge to drag independently in FilterPassbandWidget")},
            {ChangeCategory::Feature, QStringLiteral("GuardedComboBox across all applets"), QStringLiteral("Combo boxes consume scroll wheel events at boundaries")},
            {ChangeCategory::BugFix, QStringLiteral("External 10MHz reference showing as TCXO"), QStringLiteral("Radio sends `state=external`, we only checked `state=ext`")},
            {ChangeCategory::BugFix, QStringLiteral("TGXL power meter zero with PGXL"), QStringLiteral("Re-scan AMP meter routing when TGXL handle arrives")},
            {ChangeCategory::BugFix, QStringLiteral("Squelch disabled in CW mode"), QStringLiteral("Radio locks squelch in CW; controls now dimmed")},
            {ChangeCategory::BugFix, QStringLiteral("AmpApplet double title bar"), QStringLiteral("Removed internal title bar, uses AppletPanel drag title bar")},
            {ChangeCategory::BugFix, QStringLiteral("Platform-aware title bar layout"), QStringLiteral("Centered on Linux/Windows, left-aligned on macOS")},
            {ChangeCategory::BugFix, QStringLiteral("Feature request dialog modeless"), QStringLiteral("Dialog stays open while using AI in browser Clipboard not overwritten on reopen")},
            {ChangeCategory::BugFix, QStringLiteral("Paginated issue fetch in lightbulb prompt"), QStringLiteral("AI assistants now paginate through all open issues")},
            {ChangeCategory::BugFix, QStringLiteral("Step cycle wraps"), QStringLiteral("Dial push step size cycles from max back to min")},
        }},
        {QStringLiteral("0.7.18.3"), QStringLiteral("2026-04-03"), QStringLiteral("Community PRs"), {
            {ChangeCategory::Feature, QStringLiteral("Background image behind FFT spectrum"), QStringLiteral("Custom background image with adjustable dark overlay (0-100% opacity) Bundled AetherSDR logo as default background Choose/Clear buttons in Display overlay panel, persisted across sessions")},
            {ChangeCategory::Feature, QStringLiteral("Dynamic contributors in About dialog"), QStringLiteral("Live contributor list fetched from GitHub API with scrollable inset Falls back to hardcoded list when offline")},
            {ChangeCategory::Feature, QStringLiteral("GPG signed commits badge"), QStringLiteral("README badge linking to commit history")},
            {ChangeCategory::BugFix, QStringLiteral("External 10MHz reference showing as TCXO"), QStringLiteral("Radio sends `state=external` but we only checked `state=ext`")},
            {ChangeCategory::BugFix, QStringLiteral("TGXL power meter zero with PGXL connected"), QStringLiteral("AMP meter definitions arrived before TGXL handle was known, routing all meters to PGXL. Re-scan meter routing when handle arrives.")},
            {ChangeCategory::BugFix, QStringLiteral("TX Band Settings grid lines and dedup"), QStringLiteral("Visible grid lines on all cells, removed duplicate dialog from Radio Setup")},
            {ChangeCategory::BugFix, QStringLiteral("GuardedComboBox across all applets"), QStringLiteral("All combo boxes now consume scroll wheel events at boundaries Prevents accidental mode/antenna/profile changes from scroll leaking")},
        }},
        {QStringLiteral("0.7.18.2"), QStringLiteral("2026-04-03"), QStringLiteral("Quality of Life & Issue Triage"), {
            {ChangeCategory::Feature, QStringLiteral("Clickable multiFLEX badge"), QStringLiteral("Click the green \"multiFLEX\" badge in the title bar to open the dashboard Hover highlight and pointing hand cursor")},
            {ChangeCategory::Feature, QStringLiteral("SAM, NFM, DFM keyboard shortcuts"), QStringLiteral("All radio-supported modes now bindable in View → Configure Shortcuts")},
            {ChangeCategory::Feature, QStringLiteral("Auto-switch mode on spot click"), QStringLiteral("Clicking a spot with Auto Mode enabled switches slice to match the spot's mode Mode extracted from comment text (RBN/POTA/Cluster) or inferred from frequency Toggle in SpotHub → Display tab, Mode c...")},
            {ChangeCategory::Feature, QStringLiteral("TX Follows Active Slice"), QStringLiteral("Optional mode in Radio Setup → TX: switching active slice auto-assigns TX Disabled during Split to preserve intentional RX/TX separation")},
            {ChangeCategory::Feature, QStringLiteral("Upgrade check on launch"), QStringLiteral("Checks GitHub releases for newer version, shows What's New with green \"Upgrade\" button Nags on every launch until user upgrades; \"Got it — 73!\" dismisses temporarily")},
            {ChangeCategory::Feature, QStringLiteral("Lightbulb hint in What's New"), QStringLiteral("\"Found a bug or have an idea? Click the lightbulb button\" in dialog header")},
            {ChangeCategory::Feature, QStringLiteral("Version check before issue submission"), QStringLiteral("Warns if running an old version when clicking the lightbulb button")},
            {ChangeCategory::BugFix, QStringLiteral("Squelch controls disabled in CW mode"), QStringLiteral("Radio locks squelch at fixed level in CW; controls now dimmed in both VFO and RX applet")},
            {ChangeCategory::BugFix, QStringLiteral("RADE audio mute not cleared on quick-mode switch"), QStringLiteral("Quick-mode buttons now properly deactivate RADE before mode change")},
            {ChangeCategory::BugFix, QStringLiteral("Memory dialog now modeless"), QStringLiteral("No longer blocks panadapter interaction while open")},
            {ChangeCategory::BugFix, QStringLiteral("TX Band Settings grid lines and dedup"), QStringLiteral("Grid lines visible on all cells; removed duplicate dialog from Radio Setup")},
        }},
        {QStringLiteral("0.7.18.1"), QStringLiteral("2026-04-03"), QStringLiteral(""), {
            {ChangeCategory::Feature, QStringLiteral("SmartLink credential persistence via OS keychain"), QStringLiteral("Auth0 refresh token stored securely in OS keychain (Linux/macOS/Windows) Auto-login from stored token on launch — no re-entering credentials Email pre-filled (Base64-encoded in settings) on next la...")},
            {ChangeCategory::Feature, QStringLiteral("Cursor frequency label on panadapter"), QStringLiteral("Hover frequency displayed near cursor on both FFT and waterfall Toggle via Display panel → \"Cursor Freq\" button, persisted")},
            {ChangeCategory::Feature, QStringLiteral("Dynamic RF gain range from radio"), QStringLiteral("Query `display pan rfgain_info` on connect for actual gain range Slider adapts to radio model (FLEX-6300 vs 8600 vs 6600M)")},
            {ChangeCategory::Feature, QStringLiteral("Right-click VFO \"Add Spot\""), QStringLiteral("Right-click the VFO frequency display → context menu → Add Spot Pre-filled with exact tuned frequency")},
            {ChangeCategory::Feature, QStringLiteral("macOS TX audio entitlement (PR #592 by @boydsoftprez)"), QStringLiteral("Add `com.apple.security.device.audio-input` entitlement for hardened runtime Fixes silent mic when launched from Finder (Fixes #543)")},
            {ChangeCategory::BugFix, QStringLiteral("RF gain slider not updating on antenna switch"), QStringLiteral("`PanadapterModel::rfGainChanged` was not wired to overlay slider Gain now updates immediately when switching RX antennas")},
            {ChangeCategory::BugFix, QStringLiteral("SpotHub Enter key activating random buttons"), QStringLiteral("Disabled `autoDefault` on all QPushButtons in the SpotHub dialog")},
            {ChangeCategory::BugFix, QStringLiteral("GuardedSlider across all applets"), QStringLiteral("All sliders now consume wheel events at boundaries Prevents VFO scroll leak from overlay and title bar sliders")},
            {ChangeCategory::BugFix, QStringLiteral("RADE audio mute not cleared on quick-mode switch"), QStringLiteral("Quick-mode buttons bypassed RADE deactivation, leaving slice muted Now properly emits `radeActivated(false)` before mode change")},
            {ChangeCategory::BugFix, QStringLiteral("Memory dialog now modeless"), QStringLiteral("No longer blocks panadapter interaction while open Singleton pattern with QPointer (same as SpotHub, Radio Setup)")},
            {ChangeCategory::BugFix, QStringLiteral("Auto-select first discovered radio"), QStringLiteral("New users no longer confused by unselected radio in connection panel")},
            {ChangeCategory::BugFix, QStringLiteral("Enter key triggers SmartLink login"), QStringLiteral("Press Enter in password field to log in")},
            {ChangeCategory::BugFix, QStringLiteral("What's New dialog updated"), QStringLiteral("Regenerated from CHANGELOG.md — was blank since v0.7.16")},
        }},
        {QStringLiteral("0.7.18"), QStringLiteral("2026-04-02"), QStringLiteral("Tuner Controls, Band Panel & UI Polish"), {
            {ChangeCategory::Feature, QStringLiteral("TGXL 3x1 antenna switch (PR #573 by @boydsoftprez)"), QStringLiteral("ANT 1/2/3 buttons in Tuner Applet for TGXL 3x1 models Protocol: `activate ant=N` via direct TGXL TCP port 9010 Only visible on 3x1 models (`one_by_three=1`), hidden on SO2R")},
            {ChangeCategory::Feature, QStringLiteral("Clickable AMP/TUN status bar indicators (PR #579 by @boydsoftprez)"), QStringLiteral("TUN: click cycles OPERATE → BYPASS → STANDBY (green/amber/grey) AMP: click toggles OPERATE ↔ STANDBY (green/grey) Resolves remote PGXL recovery (#479)")},
            {ChangeCategory::Feature, QStringLiteral("XVTR bands in Band panel"), QStringLiteral("Configured transverter bands appear as cyan buttons in the Band grid XVTR button opens Radio Setup → XVTR tab Band panel dynamically rebuilds on XVTR config changes")},
            {ChangeCategory::BugFix, QStringLiteral("Connection panel now a proper dialog"), QStringLiteral("Converted from frameless popup to QDialog with title bar and close button No longer disappears on focus loss or lacks window decorations")},
            {ChangeCategory::BugFix, QStringLiteral("Spot click precision"), QStringLiteral("Clicking a spot label now tunes to the exact spot frequency Previously the mouseReleaseEvent overrode with a step-snapped position")},
            {ChangeCategory::BugFix, QStringLiteral("Aurora/AU-520 power meter scaling"), QStringLiteral("AU-520 reports `max_internal_pa_power=500` in slice status Power gauge now uses the higher of max_power_level and max_internal_pa_power for correct 0-600W Aurora scale")},
            {ChangeCategory::BugFix, QStringLiteral("CW decode hint"), QStringLiteral("Added \"(requires PC Audio)\" hint to CW decode panel header")},
        }},
        {QStringLiteral("0.7.17.6"), QStringLiteral("2026-04-02"), QStringLiteral(""), {
            {ChangeCategory::BugFix, QStringLiteral("Fix disconnect crash on macOS"), QStringLiteral("`PanadapterStream::stop()` was called from the main thread, crashing on macOS by closing the socket from the wrong thread Route `stop()` through BlockingQueuedConnection to network thread")},
            {ChangeCategory::BugFix, QStringLiteral("Fix VITA-49 not resuming after reconnect"), QStringLiteral("On reconnect, `start()` was skipped because the socket was still bound from the previous session. Always call `start()` on connect — it closes and rebinds the socket, re-registering the UDP port wi...")},
            {ChangeCategory::BugFix, QStringLiteral("Clear spectrum and waterfall on disconnect"), QStringLiteral("FFT and waterfall display now blanks immediately on disconnect Prevents the display from appearing frozen")},
        }},
        {QStringLiteral("0.7.17.4"), QStringLiteral("2026-04-02"), QStringLiteral("UI Polish & Stability"), {
            {ChangeCategory::BugFix, QStringLiteral("Disconnect button unresponsive"), QStringLiteral("Clicking a radio in the list while connected disabled the Disconnect button One-character fix: `!m_connected &&` → `m_connected ||`")},
            {ChangeCategory::BugFix, QStringLiteral("PanadapterStream thread affinity"), QStringLiteral("Socket/timer signal connections moved to init() on the network thread Fixes \"Cannot create children for a parent in a different thread\" on macOS Prevents duplicate socket notifiers on reconnect")},
            {ChangeCategory::BugFix, QStringLiteral("Scroll wheel 8x on Linux Mint"), QStringLiteral("Added 50ms debounce for desktops that send multiple events per notch Combined with existing ±1 clamp for inflated single events")},
            {ChangeCategory::BugFix, QStringLiteral("VFO slider wheel leak (#547 BUG-002)"), QStringLiteral("Sliders in VFO panel no longer leak wheel events to frequency scroll Scroll over frequency display tunes by step size Scroll elsewhere on VFO is consumed (dead zone)")},
            {ChangeCategory::BugFix, QStringLiteral("B/S zoom toggle (#547 ENH-001)"), QStringLiteral("B and S buttons now toggle: first press zooms, second press restores")},
        }},
        {QStringLiteral("0.7.17.3"), QStringLiteral("2026-04-02"), QStringLiteral("Filter Widget, DSP Cleanup & Diagnostics"), {
            {ChangeCategory::Feature, QStringLiteral("Visual filter passband widget"), QStringLiteral("SmartSDR-style filter control in the RX applet with static trapezoid shape Horizontal drag shifts passband, vertical drag adjusts bandwidth Numeric labels: lo, hi, bandwidth, and center offset from...")},
            {ChangeCategory::Feature, QStringLiteral("Frequency reference status bar"), QStringLiteral("Status bar shows actual frequency reference: GPS (with satellite count and lock status), Ext 10M, or TCXO No more perpetual \"Warming\" on radios without a GPS antenna Uses oscillator status messages...")},
            {ChangeCategory::BugFix, QStringLiteral("Stale process on exit"), QStringLiteral("Suppress reconnect dialog during app shutdown — was keeping the process alive on Windows after the main window closed Added 3-second timeouts to all thread wait() calls in destructors")},
            {ChangeCategory::BugFix, QStringLiteral("DAX TX level meter"), QStringLiteral("P/CW mic gauge now shows DAX TX input level when DAX is active Previously showed nothing because the mic input path returns early in DAX mode")},
            {ChangeCategory::BugFix, QStringLiteral("DSP toggles removed from RX applet"), QStringLiteral("9 buttons and 27 sync points eliminated between RxApplet, VfoWidget, and spectrum overlay DSP controls remain in VFO DSP tab and spectrum overlay menu Eliminates triple-state tracking bugs (NR/NR2,...")},
        }},
        {QStringLiteral("0.7.17.2"), QStringLiteral("2026-04-01"), QStringLiteral(""), {
            {ChangeCategory::BugFix, QStringLiteral("rigctld cross-band tune"), QStringLiteral("`set_freq` now uses `tuneAndRecenter` so the panadapter follows when WSJT-X or other CAT apps change bands (was using `autopan=0`)")},
            {ChangeCategory::BugFix, QStringLiteral("Mouse 8x step on KDE/Cinnamon/Linux Mint"), QStringLiteral("Clamp scroll wheel to ±1 step per event in all scroll handlers: SpectrumWidget (VFO tune), RxApplet (freq label), HGauge (TGXL relays)")},
            {ChangeCategory::BugFix, QStringLiteral("Slider labels stale on connect"), QStringLiteral("VFO slider labels (AF Gain, SQL, AGC-T, Pan, ESC, APF) now update immediately when the radio pushes status on connect")},
            {ChangeCategory::BugFix, QStringLiteral("S-Meter frozen during TUNE"), QStringLiteral("TUNE bypassed `m_txRequested` so the interlock TRANSMITTING state was rejected. S-Meter now switches to forward power during TUNE and MOX. Also fixes #499 (power meter no output)")},
            {ChangeCategory::BugFix, QStringLiteral("DAX auto-activate in digital modes"), QStringLiteral("Radio-side DAX flag (`transmit set dax=1`) auto-toggles on mode change to/from DIGU/DIGL/RTTY — P/CW DAX button follows automatically Client-side DSP (NR2/RN2/BNR) auto-disabled in digital modes to...")},
            {ChangeCategory::BugFix, QStringLiteral("Interlock timing fields not populated"), QStringLiteral("RCA TX1, TX Delay, ACC TX, Timeout now show correct radio values in Radio Setup → TX tab (parser existed but was never wired up)")},
            {ChangeCategory::Feature, QStringLiteral("MIDI relative knob mode"), QStringLiteral("Relative CC encoding support (1-63 CW, 65-127 CCW) 20ms coalesce timer batches rapid steps into single radio commands 3-tier acceleration: slow spin = ½ rate (fine tune), fast = 4× Snap to step gri...")},
        }},
        {QStringLiteral("0.7.17.1"), QStringLiteral("2026-04-01"), QStringLiteral("Accurate Network Diagnostics"), {
            {ChangeCategory::Improvement, QStringLiteral("Kernel TCP_INFO RTT"), QStringLiteral("RTT now read from the kernel's TCP congestion control (TCP_INFO), completely immune to Qt event loop delays Linux: `getsockopt(TCP_INFO)` → `tcpi_rtt` macOS: `getsockopt(TCP_CONNECTION_INFO)` → `tc...")},
            {ChangeCategory::Improvement, QStringLiteral("Per-stream Network Diagnostics"), QStringLiteral("Network Diagnostics dialog now shows per-stream-type rates: Audio, FFT, Waterfall, Meters, DAX Per-stream packet loss tracking with sequence error counts TX byte counter wired to actual UDP sends T...")},
            {ChangeCategory::Improvement, QStringLiteral("Packet loss accuracy"), QStringLiteral("Sequence tracking moved after ownership filter — no longer counts other clients' streams in Multi-Flex mode Meter broadcast packets excluded (unreliable sequence counter) Eliminates false 28% packe...")},
            {ChangeCategory::Improvement, QStringLiteral("DAX stream cleanup"), QStringLiteral("Disabling DAX now sends `stream remove` to the radio and unregisters streams from PanadapterStream Previously DAX streams persisted until app restart")},
            {ChangeCategory::Improvement, QStringLiteral("Socket write flush"), QStringLiteral("`flush()` after `write()` in RadioConnection ensures ping and command packets are sent immediately, not buffered by Qt")},
        }},
        {QStringLiteral("0.7.17"), QStringLiteral("2026-04-01"), QStringLiteral("Network & Input Responsiveness"), {
            {ChangeCategory::Improvement, QStringLiteral("RadioConnection worker thread"), QStringLiteral("TCP command/status I/O runs on a dedicated worker thread Ping RTT now measures actual network latency, not main thread load Fixes \"Poor\" network status despite zero packet drops Response callbacks ...")},
            {ChangeCategory::Improvement, QStringLiteral("External controllers worker thread"), QStringLiteral("FlexControl USB tuning knob, MIDI controllers, and serial port PTT/CW keying now run on a shared ExtControllers worker thread USB serial I/O, RtMidi callbacks, and poll timers no longer compete wit...")},
        }},
        {QStringLiteral("0.7.16"), QStringLiteral("2026-03-31"), QStringLiteral("World-First TGXL Relay Control & Global Band Plans"), {
            {ChangeCategory::Feature, QStringLiteral("Manual TGXL Pi Network Relay Control"), QStringLiteral("Scroll over the C1, L, or C2 relay bars in the Tuner applet to adjust relay positions one step at a time AetherSDR auto-connects directly to the TGXL on TCP port 9010 Real-time relay updates with ~...")},
            {ChangeCategory::Feature, QStringLiteral("Selectable IARU Band Plans"), QStringLiteral("View → Band Plan now includes a region selector: ARRL (US), IARU Region 1, Region 2, Region 3 Region 1: Europe, Africa, Middle East (80m stops at 3.800, 40m at 7.200) Region 2: Americas (80m to 4.0...")},
            {ChangeCategory::Feature, QStringLiteral("multiFLEX Dashboard"), QStringLiteral("Settings → multiFLEX opens a live station dashboard Per-client: station name, program, TX antenna, TX frequency LOCAL PTT status, enable/disable toggle")},
            {ChangeCategory::BugFix, QStringLiteral("Default MTU reduced to 1450"), QStringLiteral("VITA-49 FFT/waterfall packets are 1436 bytes at MTU 1500, exceeding most VPN/SD-WAN tunnel MTUs (WireGuard 1420, OpenVPN 1400) Confirmed fix by user on Cisco Meraki SD-WAN Adjustable in Radio Setup...")},
            {ChangeCategory::BugFix, QStringLiteral("FFTW thread safety for NR2"), QStringLiteral("Added mutex to serialize all FFTW plan creation/destruction Prevents potential crashes when switching DSP modes or regenerating wisdom `fftw_execute()` left unlocked (thread-safe per FFTW spec)")},
            {ChangeCategory::BugFix, QStringLiteral("Removed broken release.yml workflow"), QStringLiteral("GitHub Actions release workflow removed (GITHUB_TOKEN PRs can't trigger CI) Ship/release now handled locally via Claude using gh CLI and GPG-signed tags")},
        }},
        {QStringLiteral("0.7.15"), QStringLiteral("2026-03-30"), QStringLiteral("Digital-Friendly Minimal Mode"), {
            {ChangeCategory::Feature, QStringLiteral("Minimal Mode"), QStringLiteral("Ctrl+M or ↙ button in the title bar collapses AetherSDR to a 260px-wide applet-only strip — just your VFO, RX controls, TX controls, and meters Spectrum, waterfall, and status bar are hidden; title...")},
            {ChangeCategory::Feature, QStringLiteral("multiFLEX Dashboard"), QStringLiteral("Settings → multiFLEX opens a live dashboard showing all connected stations Per-client display: station name, program, TX antenna, TX frequency LOCAL PTT status with checkmarks and \"Enable Local PTT...")},
            {ChangeCategory::Infrastructure, QStringLiteral("CI/CD Pipeline"), QStringLiteral("Docker-based CI builds in ~3.5 minutes (down from 3–19 min variable) CodeQL analysis runs in parallel without blocking merge `git ship` squashes local commits into single auto-merge PR `git release...")},
        }},
        {QStringLiteral("0.7.12"), QStringLiteral("2026-03-29"), QStringLiteral(""), {
            {ChangeCategory::Feature, QStringLiteral("GPG Release Signing"), QStringLiteral("Linux AppImage and source archives are GPG-signed with detached `.asc` signatures SHA256SUMS.txt generated and signed for each release macOS artifacts signed via Apple codesign + notarization (unch...")},
            {ChangeCategory::Feature, QStringLiteral("Commit Signing"), QStringLiteral("All commits to `main` require GPG signatures (branch protection enforced) Contributor setup guide in CONTRIBUTING.md")},
            {ChangeCategory::Feature, QStringLiteral("Configurable Band Plan Size"), QStringLiteral("View → Band Plan submenu: Off, Small (6pt), Medium (10pt), Large (12pt), Huge (16pt) Strip height scales with font size Replaces the previous on/off checkbox")},
            {ChangeCategory::Feature, QStringLiteral("Visual Keyboard Shortcut Manager"), QStringLiteral("View → Configure Shortcuts opens a visual keyboard map dialog Full ANSI keyboard layout with keys color-coded by action category Click any key to assign/change/clear its binding ~45 bindable action...")},
            {ChangeCategory::Feature, QStringLiteral("Click-and-Drag VFO Tuning"), QStringLiteral("Click inside the filter passband and drag left/right to tune the VFO Frequency snaps to step size during drag Filter edge drag (resize) takes priority within ±5px grab zone")},
            {ChangeCategory::Feature, QStringLiteral("Go to Frequency (G key)"), QStringLiteral("Press G to open the VFO direct frequency entry field Pre-fills with current frequency, selected for easy overtype")},
            {ChangeCategory::Feature, QStringLiteral("Space PTT Hold-to-Transmit"), QStringLiteral("Hold Space to transmit, release to return to RX (true momentary PTT) Works regardless of which UI widget has focus Properly syncs TX state with TX applet and status bar")},
            {ChangeCategory::BugFix, QStringLiteral("NR2/RN2/BNR Crash on DSP Mode Switch"), QStringLiteral("SEGV in SpectralNR::process() when switching from BNR to NR2 Root cause: enabled flag was set before the DSP object was constructed; audio arriving during the transition called process() on a null ...")},
            {ChangeCategory::BugFix, QStringLiteral("FlexControl UI Lag"), QStringLiteral("Each encoder step sent a separate TCP command, flooding the radio Fix: coalesce rapid encoder steps into a single command every 20ms")},
            {ChangeCategory::BugFix, QStringLiteral("FlexControl Menu Stub"), QStringLiteral("Settings → FlexControl showed \"not implemented\" Fix: wired to open Radio Setup dialog on the Serial tab")},
            {ChangeCategory::BugFix, QStringLiteral("TNF Crash on +TNF Click"), QStringLiteral("SIGBUS on macOS when clicking +TNF after a panadapter layout change Root cause: rebuildTnfMarkers lambda captured raw SpectrumWidget pointer that became dangling when the pan was removed Fix: captu...")},
            {ChangeCategory::BugFix, QStringLiteral("FlexControl ToggleMox/ToggleTune Stuck in TX"), QStringLiteral("Pressing the FlexControl button a second time didn't toggle TX off Root cause: `applyTransmitStatus()` never parsed `mox=` from radio status, so `isMox()` always returned false Fix: parse `mox=` ke...")},
            {ChangeCategory::BugFix, QStringLiteral("VFO Lock Icon Not Updating"), QStringLiteral("Lock icon on VFO overlay didn't update when toggled via FlexControl or RxApplet Root cause: VfoWidget::setSlice() connected 30+ SliceModel signals but was missing `lockedChanged` Fix: added the mis...")},
            {ChangeCategory::BugFix, QStringLiteral("Mouse Wheel 8x Step on KDE/Cinnamon"), QStringLiteral("Tuning steps were 8x the selected step size on Linux Mint, Cinnamon, and KDE Plasma Root cause: these desktops send high-resolution angleDelta (960 per notch instead of 120) Fix: accumulate angleDe...")},
            {ChangeCategory::BugFix, QStringLiteral("ESC Gain Slider Black Thumb"), QStringLiteral("ESC gain slider thumb was invisible (black) on macOS and Windows Root cause: kSliderStyle only defined horizontal handle rules; ESC gain slider is vertical Fix: added vertical groove and handle QSS...")},
            {ChangeCategory::BugFix, QStringLiteral("RxApplet NR2 Button Not Working"), QStringLiteral("Cycling the RX panel NR button to NR2 didn't enable noise reduction Root cause: the `nr2CycleToggled` handler only synced the VFO button visual but never called `enableNr2WithWisdom()` Fix: NR2 now...")},
            {ChangeCategory::BugFix, QStringLiteral("Split Slice on Wrong Pan"), QStringLiteral("In multi-pan mode, clicking SPLIT could create the TX slice on the wrong panadapter Root cause: split used `m_activePanId` (global) instead of the RX slice's actual pan Fix: use `rxSlice->panId()` ...")},
        }},
        {QStringLiteral("0.7.11"), QStringLiteral("2026-03-29"), QStringLiteral("Platform Notes"), {
            {ChangeCategory::Feature, QStringLiteral("Panadapter Click-to-Spot"), QStringLiteral("Right-click on the panadapter to create a spot marker with callsign, comment, and configurable lifetime Optionally forward spots to your connected DX cluster Right-click on existing spots: Tune, Co...")},
            {ChangeCategory::Feature, QStringLiteral("Per-Slice Record & Play"), QStringLiteral("Record (⏺) and Play (▶) buttons on each VFO flag, matching SmartSDR placement Record button pulses red while recording Play disabled until a recording exists (radio-managed) TX playback: press MOX ...")},
            {ChangeCategory::Feature, QStringLiteral("DAX IQ Streaming"), QStringLiteral("Raw I/Q data from the radio's DDC to external SDR apps (SDR#, GQRX, GNU Radio) 4 IQ channels at 24/48/96/192 kHz via PulseAudio virtual capture devices DIGI applet: per-channel rate dropdown, level...")},
            {ChangeCategory::Feature, QStringLiteral("Applet Panel Collapse"), QStringLiteral("☰ hamburger icon in the status bar toggles the right applet panel Spectrum/waterfall expands to full width when panel is hidden Also available via View → Applet Panel checkbox Custom painted +PAN s...")},
            {ChangeCategory::Feature, QStringLiteral("Drag-Reorderable Applets"), QStringLiteral("Drag applets by their ⋮⋮ grip title bars to reorder in the panel Order persists across sessions View → Reset Applet Order to restore defaults Built on QDrag framework (future-proofs for pop-out to ...")},
            {ChangeCategory::Feature, QStringLiteral("Opus Codec Independent of RADE"), QStringLiteral("SmartLink compressed audio now works without the RADE digital voice module System libopus detected via pkg-config when RADE is disabled Windows: setup-opus.ps1 builds static opus from source")},
            {ChangeCategory::Feature, QStringLiteral("Modeless Dialogs"), QStringLiteral("SpotHub, Radio Setup, and MIDI Mapping dialogs no longer block the main window Interact with the radio while dialogs are open")},
            {ChangeCategory::BugFix, QStringLiteral("BNR Crash Fix"), QStringLiteral("r8brain resampler buffer overflow — BNR output can return up to 9,600 samples at once but the resampler was allocated for 4,096. Increased to 16,384. Found and verified via ASAN.")},
            {ChangeCategory::BugFix, QStringLiteral("AppSettings Corruption Prevention"), QStringLiteral("Atomic save: write to .tmp file, validate XML, rename over original Backup recovery: auto-recover from .bak if main file is corrupt Count guard: refuse to save if settings count dropped below half ...")},
            {ChangeCategory::BugFix, QStringLiteral("RadioModel Shutdown Use-After-Free"), QStringLiteral("Disconnect all signals from RadioConnection before member destruction Prevents accessing destroyed XVTR map and slice models during teardown Found via AddressSanitizer (ASAN) build")},
            {ChangeCategory::BugFix, QStringLiteral("Opus SSE Alignment"), QStringLiteral("Copy input data to alignas(16) buffers before opus_encode/opus_decode Prevents SEGV on SSE-optimized RADE opus builds")},
            {ChangeCategory::BugFix, QStringLiteral("Spot Label Deconfliction"), QStringLiteral("Re-scan all placed labels after each nudge to properly stack across all levels Spots no longer overlap when multiple labels are close in frequency")},
            {ChangeCategory::BugFix, QStringLiteral("Other Fixes"), QStringLiteral("Duplicate spot-to-cluster sends fixed (disconnect before reconnect in wirePanadapter) DIGI applet section headers use distinct label style (not confused with draggable title bars) DIGI applet layou...")},
        }},
        {QStringLiteral("0.7.4"), QStringLiteral("2026-03-26"), QStringLiteral("VOX Support — #253"), {
            {ChangeCategory::BugFix, QStringLiteral("Full Changelog"), QStringLiteral("")},
        }},
        {QStringLiteral("0.4.0"), QStringLiteral("2026-03-17"), QStringLiteral("Downloads"), {
            {ChangeCategory::Feature, QStringLiteral("Tracking Notch Filters (TNF)"), QStringLiteral("Right-click spectrum or waterfall to add/remove TNFs Drag markers to reposition, adjust width and depth via context menu Color-coded: yellow = temporary, green = permanent (survives power cycles)")},
            {ChangeCategory::Feature, QStringLiteral("SmartLink Remote Operation (beta)"), QStringLiteral("Log in with your FlexRadio SmartSDR+ account Radio auto-discovered via SmartLink relay server Full command channel over TLS — tune, change modes, all controls work remotely UDP streaming (FFT, wate...")},
            {ChangeCategory::Feature, QStringLiteral("Manual (Routed) Connection"), QStringLiteral("Connect to radios on different subnets/VLANs where UDP broadcast doesn't reach Enter IP address, AetherSDR probes the radio and adds it to the list")},
            {ChangeCategory::Feature, QStringLiteral("Audio Settings Tab"), QStringLiteral("Line out gain/mute, headphone gain/mute, front speaker mute PC audio input/output device selection (live-switching)")},
            {ChangeCategory::Feature, QStringLiteral("4-Channel CAT Control"), QStringLiteral("Independent rigctld TCP server per slice (ports 4532-4535) PTY symlinks per channel (/tmp/AetherSDR-CAT-A through -D) PTT auto-switches TX to the keyed channel's slice")},
            {ChangeCategory::Feature, QStringLiteral("Cross-Platform Builds"), QStringLiteral("Linux AppImage, macOS universal DMG (Intel + Apple Silicon), Windows ZIP All auto-built via GitHub Actions on tagged releases")},
            {ChangeCategory::Feature, QStringLiteral("Other Improvements"), QStringLiteral("Dynamic mode list from radio (supports FDVU, FDVM, and future modes) Mode-aware DSP controls for FreeDV digital voice modes Security hardening: redacted credentials in logs, restricted log file per...")},
        }},
        {QStringLiteral("0.3.0"), QStringLiteral("2026-03-17"), QStringLiteral("Profile Management"), {
            {ChangeCategory::BugFix, QStringLiteral("Profile load crash (SEGV)"), QStringLiteral("")},
            {ChangeCategory::BugFix, QStringLiteral("No audio after profile load"), QStringLiteral("")},
            {ChangeCategory::BugFix, QStringLiteral("No FFT after profile load"), QStringLiteral("")},
        }},
        {QStringLiteral("0.2.2"), QStringLiteral("2026-03-16"), QStringLiteral("Full Changelog"), {
            {ChangeCategory::BugFix, QStringLiteral("Fix:"), QStringLiteral("")},
        }},
    };
    return entries;
}

} // namespace AetherSDR
