From df7ca8f21c526f8f125e9783c0ec7fd543109abf Mon Sep 17 00:00:00 2001 From: DC0SK Date: Sun, 26 Jul 2026 22:00:51 +0200 Subject: [PATCH] feat(update): automatic update check with a top-area notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FR-UI-UPD-02, implementing the requirement recorded in the previous commit. A default-on preference (opt-out under Settings) runs one update check at start-up, off the UI thread, reusing the operator-initiated path (update::check_now) - so the whole never-spurious numeric comparison from FR-UI-UPD-01 comes for free. When a substantiated newer release is found, a clickable "● update " link appears in the top status bar beside the connection indicator, opening the release page. It is silent otherwise: checking, up-to-date and failed show nothing. The metered-link caution that made FR-UI-UPD-01 manual-only is honoured by construction rather than overridden - one request per launch, opt-out, and nothing shown unless there is a real update. Tested at the config layer (default-on preference that survives a save/load round-trip) and verified on screen: the header link renders for an Available status, the Settings toggle is present, and the start-up check was seen overwriting a seeded status - confirming it actually runs. 327 tests. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01KiAgfnGv746wwVBSfFkoRY --- CHANGELOG.md | 10 +++ app/src/main.rs | 94 ++++++++++++++++++++---- crates/k4-config/src/lib.rs | 6 ++ crates/k4-config/tests/config.rs | 20 +++++ docs/requirements/system-requirements.md | 5 +- docs/test/coverage.generated.md | 2 +- docs/test/test-strategy.md | 6 +- 7 files changed, 125 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1c0684..801ad2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,16 @@ during the 0.4.0 release, so earlier detail lives in the git history and in the change ledgers under [`docs/test/test-strategy.md`](docs/test/test-strategy.md) and [`docs/requirements/system-requirements.md`](docs/requirements/system-requirements.md). +## [Unreleased] + +### Added + +- **Automatic update check.** The app now checks GitHub for a newer release + once at start-up and, if there is one, shows a clickable link in the top + status bar beside the connection indicator. On by default; turn it off under + Settings. It stays silent unless there is a real update, and makes at most + one request per launch. + ## [0.8.0] — 2026-07-25 ### Added diff --git a/app/src/main.rs b/app/src/main.rs index dbe3e08..dd81f96 100644 --- a/app/src/main.rs +++ b/app/src/main.rs @@ -317,6 +317,9 @@ struct App { ptt_toggle: bool, // Mode-adaptive UI: per-mode control emphasis (docs/concept/mode-aware-ui.md). mode_aware_ui: bool, + /// Automatically check for a newer release at start-up, opt-out preference + /// (FR-UI-UPD-02). Mirrors `Prefs::auto_update_check`. + auto_update_check: bool, // Elecraft K-Pod USB control surface enabled (runtime opt-in, FR-KPOD-04). kpod_enabled: bool, // K-Pod function-switch macro table: 16 slots (F1–F8 × tap/hold), each a CAT @@ -632,6 +635,8 @@ enum Message { ToggleArm, TogglePttMode, ToggleModeAwareUi, + /// Auto update check on/off (FR-UI-UPD-02). + ToggleAutoUpdate, ToggleKpod, /// Run an assigned macro from the on-screen MACROS tab (FR-MACRO-01): send /// its CAT string, gated by the TX arm exactly as the K-Pod press is. @@ -873,6 +878,7 @@ impl App { let ptt_hotkey = prefs.ptt_hotkey.clone(); let ptt_toggle = prefs.ptt_toggle; let mode_aware_ui = prefs.mode_aware_ui; + let auto_update_check = prefs.auto_update_check; let diag_enabled = prefs.diagnostics_window; let tooltips = prefs.tooltips; @@ -1037,6 +1043,7 @@ impl App { ptt_hotkey, ptt_toggle, mode_aware_ui, + auto_update_check, kpod_enabled, kpod_buttons, capturing_hotkey: false, @@ -1060,6 +1067,12 @@ impl App { loaded_commands: Vec::new(), backup_status: String::new(), }; + // One silent update check per launch when opted in (FR-UI-UPD-02). It + // reuses the operator-initiated path; only a found update surfaces + // anything, in the header status area. + if app.auto_update_check { + window_tasks.push(update_check_task()); + } (app, Task::batch(window_tasks)) } @@ -1339,6 +1352,7 @@ impl App { ptt_hotkey: self.ptt_hotkey.clone(), ptt_toggle: self.ptt_toggle, mode_aware_ui: self.mode_aware_ui, + auto_update_check: self.auto_update_check, kpod_enabled: self.kpod_enabled, kpod_buttons: self.kpod_buttons.clone(), ..Default::default() @@ -2025,6 +2039,10 @@ impl App { self.mode_aware_ui = !self.mode_aware_ui; self.save_config(); } + Message::ToggleAutoUpdate => { + self.auto_update_check = !self.auto_update_check; + self.save_config(); + } Message::ToggleKpod => { self.kpod_enabled = !self.kpod_enabled; self.send(WorkerCmd::SetKpodEnabled(self.kpod_enabled)); @@ -2251,20 +2269,11 @@ impl App { Message::OpenUrl(url) => open_url(url), Message::OpenUrlOwned(url) => open_url(&url), Message::CheckUpdate => { - // Operator-initiated only; never on a timer. The request is - // blocking, so it runs off the UI thread (FR-UI-UPD-01). + // The blocking request runs off the UI thread. The About box + // shows "Checking…"; the start-up auto-check uses the same task + // without setting that visible state (FR-UI-UPD-01/02). self.update_status = update::UpdateStatus::Checking; - let current = ui::app_version().to_string(); - return Task::perform( - async move { - tokio::task::spawn_blocking(move || update::check_now(¤t)) - .await - .unwrap_or_else(|e| { - update::UpdateStatus::Failed(format!("check did not run: {e}")) - }) - }, - Message::UpdateChecked, - ); + return update_check_task(); } Message::UpdateChecked(status) => self.update_status = status, Message::AfRecord => self.send(WorkerCmd::AfRecord), @@ -6120,11 +6129,39 @@ impl App { // is a bad trade, so this stays as it is until the status message has // somewhere else to go. let status_strip = Text::new(status_bits.join(" · ")).size(12).color(dim); + // Update notification (FR-UI-UPD-02): only a *found* update surfaces + // anything, and it lives here in the top status area beside the + // connection indicator, as a clickable link to the release page. + // Silent otherwise — checking, up-to-date and failed show nothing. + let update_note: Element = match &self.update_status { + update::UpdateStatus::Available { version, url } => { + let accent = role_color(ui::ColorRole::VfoA); + let url = url.clone(); + Button::new( + Text::new(format!("● update {version}")) + .size(12) + .color(accent), + ) + .style(move |_t: &Theme, status: button::Status| button::Style { + background: None, + text_color: match status { + button::Status::Hovered | button::Status::Pressed => Color::WHITE, + _ => accent, + }, + ..button::Style::default() + }) + .padding(0) + .on_press(Message::OpenUrlOwned(url)) + .into() + } + _ => Space::with_width(Length::Shrink).into(), + }; let header = Row::new() .spacing(12) .align_y(Alignment::Center) .push(Text::new("K4 REMOTE").size(20)) .push(status_ind) + .push(update_note) .push( Text::new(self.ui.status.clone()) .size(12) @@ -7519,6 +7556,22 @@ impl App { .color(dim), ), ) + .push( + Row::new() + .spacing(8) + .align_y(Alignment::Center) + .push(small_btn_pair( + self.auto_update_check, + "Update check: ON", + "Update check: OFF", + Message::ToggleAutoUpdate, + )) + .push( + Text::new("check GitHub for a newer release at start-up") + .size(10) + .color(dim), + ), + ) .push( Row::new() .spacing(8) @@ -8422,6 +8475,21 @@ fn meter_style(strong: bool) -> impl Fn(&Theme) -> progress_bar::Style { /// Receiver badge: a filled tag in the VFO's semantic colour with dark text, /// like the K4's corner `A`/`B` markers (FR-UI-10). +/// A background task that runs the blocking GitHub release check off the UI +/// thread and reports the result (FR-UI-UPD-01/02). Shared by the About-box +/// button and the start-up auto-check. +fn update_check_task() -> Task { + let current = ui::app_version().to_string(); + Task::perform( + async move { + tokio::task::spawn_blocking(move || update::check_now(¤t)) + .await + .unwrap_or_else(|e| update::UpdateStatus::Failed(format!("check did not run: {e}"))) + }, + Message::UpdateChecked, + ) +} + fn badge(label: &'static str, role: ui::ColorRole) -> Element<'static, Message> { Container::new(Text::new(label).size(15).color(Color::BLACK)) .style(move |_theme: &Theme| container::Style { diff --git a/crates/k4-config/src/lib.rs b/crates/k4-config/src/lib.rs index 3871ab1..d97622c 100644 --- a/crates/k4-config/src/lib.rs +++ b/crates/k4-config/src/lib.rs @@ -116,6 +116,11 @@ pub struct Prefs { /// Use the mode-adaptive UI (per-mode control emphasis). Default on. #[serde(default = "default_true")] pub mode_aware_ui: bool, + /// Automatically check for a newer release at start-up (FR-UI-UPD-02). + /// Default on; opt-out here. One check per launch, silent unless an update + /// is found. + #[serde(default = "default_true")] + pub auto_update_check: bool, /// Enable the Elecraft K-Pod USB control surface. Default off (opt-in); the /// app runs normally whether or not a K-Pod is attached. #[serde(default)] @@ -360,6 +365,7 @@ impl Default for Prefs { ptt_hotkey: default_ptt_hotkey(), ptt_toggle: true, mode_aware_ui: true, + auto_update_check: true, kpod_enabled: false, kpod_buttons: default_kpod_buttons(), } diff --git a/crates/k4-config/tests/config.rs b/crates/k4-config/tests/config.rs index 0d0dd45..c1386f4 100644 --- a/crates/k4-config/tests/config.rs +++ b/crates/k4-config/tests/config.rs @@ -247,3 +247,23 @@ fn fr_kpod_06_button_macros_seed_and_persist() { "edited table must persist" ); } + +/// The automatic update check is opt-out: default on, and it survives a +/// save/load round-trip so an operator's choice sticks. +/// trace: FR-UI-UPD-02 +#[test] +fn fr_ui_upd_02_auto_update_check_defaults_on_and_persists() { + assert!( + Prefs::default().auto_update_check, + "default opt-in: the check is on unless turned off" + ); + + // A round-trip through TOML preserves an explicit opt-out. + let prefs = Prefs { + auto_update_check: false, + ..Default::default() + }; + let toml = toml::to_string(&prefs).expect("serialize"); + let back: Prefs = toml::from_str(&toml).expect("deserialize"); + assert!(!back.auto_update_check, "opt-out is remembered"); +} diff --git a/docs/requirements/system-requirements.md b/docs/requirements/system-requirements.md index 7709445..3a5e004 100644 --- a/docs/requirements/system-requirements.md +++ b/docs/requirements/system-requirements.md @@ -1,7 +1,7 @@ --- title: "System Requirements Specification" status: Draft -version: "0.50" +version: "0.51" updated: 2026-07-26 authors: - Simon Keimer (DC0SK) @@ -226,7 +226,7 @@ Interface Spec v1.03; Owner's Manual Rev F) for hands-on VFO selection + tuning. | `FR-UI-17` | offer a **theme selector** cycling **Dark → Light → Contrast → System**, applied live to the whole UI; `System` follows the OS light/dark preference. Each theme resolves the surface-shade and semantic-role palettes (`FR-UI-10/15`). | STK-11 | C | T/D | `ThemeMode` cycles the four modes with distinct labels and resolves to a concrete palette (`System` per the detected OS preference) (test); each theme renders coherently (demo). | | `FR-UI-18` | provide an **About** affordance showing the author, the **software version**, the license, the project URL, and a **donate** link; the license, project URL, and donate entries shall open in the OS browser when activated. | STK-11 | C | T/D | The About constants (author/license+URL/project URL/donate URL) and `app_version()` are present (test); the About box shows them, the links open externally, and it dismisses (demo). | | `FR-UI-UPD-01` | offer an operator-initiated check in the About box for a newer release on the project's GitHub, showing the new version number as a link to its download page, or that the build is current, or why the check failed. The check shall run **only** when requested — never on a timer or at start-up — and shall never report an update it cannot substantiate. | STK-16 | C | T/D | Version comparison is numeric (`0.10.0` newer than `0.9.0`), an equal or older upstream is not an update, and an unparseable version or a reply without a tag yields *not-an-update* / *failed* rather than success (test); the button reports the current release against a live GitHub and the link opens that release's page (demo). | -| `FR-UI-UPD-02` | **automatically check for a newer release** on start-up, **on by default**, with an opt-out control in the Settings menu (a persisted preference). Only a **substantiated** newer release surfaces anything — the same numeric comparison as `FR-UI-UPD-01`, never a spurious update. When one is found, show a **notification in the top-most status area of the main window**, beside the connection-state indicator, as a **clickable link** that opens that release's download page. The automatic check shall run **at most once per application start** (not on a timer, to respect a metered remote link), and shall be **silent** when disabled, when the build is current, or when the check fails. | STK-16 | C | T/D | The persisted preference defaults to on and the Settings toggle flips it; with it off no automatic check runs; a substantiated newer release yields a status-area link to the release page while an equal, older, or failed check yields no notification (test); the notification appears beside the connection indicator on a real newer release and the link opens the page (demo). | +| `FR-UI-UPD-02` | **automatically check for a newer release** on start-up, **on by default**, with an opt-out control in the Settings menu (a persisted preference). Only a **substantiated** newer release surfaces anything — the same numeric comparison as `FR-UI-UPD-01`, never a spurious update. When one is found, show a **notification in the top-most status area of the main window**, beside the connection-state indicator, as a **clickable link** that opens that release's download page. The automatic check shall run **at most once per application start** (not on a timer, to respect a metered remote link), and shall be **silent** when disabled, when the build is current, or when the check fails. | STK-16 | S | T/D | `Prefs::auto_update_check` defaults to **on** and survives a save/load round-trip (test); the check reuses `FR-UI-UPD-01`'s numeric comparison so an equal/older/unparseable upstream is never an update (test); the start-up task runs once, only when the preference is on, and the top-area notification renders only for an `Available` status as a link to the release page (demo); the Settings toggle flips and persists the preference (demo). | | `FR-UI-TIP-01` | show an explanatory tooltip when the pointer rests on a control for **500 ms**, naming what the control does and the CAT command behind it, switchable by a persisted preference (default on) in the settings screen. A control with no tip written shall show nothing rather than an empty tooltip. | STK-16 | C | T/D | Tips are unique-keyed, single-sentence, non-trivial, and most name a CAT mnemonic; lookup of an unknown id yields `None`; the delay constant is 500 ms (test); tips appear after the dwell and vanish when switched off (demo). | | `FR-UI-HOLD-01` | distinguish a **tap** from a **hold** on controls whose K4 counterpart carries both, using the radio's own threshold of about half a second (D14 p.359), and perform the radio's documented hold function rather than an app-specific one. A press whose start is unknown shall count as a tap. For a control with a settings popup (`FR-UI-POPUP-01`), the radio's documented hold function **is** "bring up the controls" (D14 p.1318, p.1368), so the hold shall open that popup; the app shall not invent a stepping behaviour in its place. | STK-16 | S | T/D | `is_hold` classifies at the 500 ms boundary and treats an unknown duration as a tap; `agc_tap` never reaches AGC-off, so a tap cannot disable AGC by accident (test); every `tap_hold` site wraps a non-interactive visual and all six popup chips are covered (source-level test); holding each chip on the radio opens its panel and a tap still performs the switch's own function (demo). | | `FR-UI-POPUP-01` | open a **settings popup** for a receiver control from its chip, carrying that control's paired-switch settings (level, mode, on/off) as the K4's own panel does (D14 p.1318 attenuator, p.1368 noise blanker), opened by a **hold** (the radio's own idiom) or a right-click, and dismiss it on a click outside, on `ESC`, or from an explicit dismiss button. The popup shall open **at the control** rather than centred on the window, and shall stay fully within the window. A value the radio quantises shall be snapped to its ladder before being sent. A popup slider shall track the drag from a **local mirror** and shall not re-query the radio mid-drag, so a lagging read-back cannot pull it back to the previous level. | STK-16 | S | T/D | `atten_snap` never yields a level off the radio's 3 dB ladder or above 21 dB, reaches every step, and covers the radio's whole ladder; `popup_origin` tracks the pointer, keeps every popup inside the window from any corner, and pins the top-left when the window is too small; each popup has a distinct title (test); holding or right-clicking each chip opens its panel at that chip, dragging the attenuator holds the dragged level mid-drag, and the three dismissals work (demo). | @@ -380,3 +380,4 @@ syntax per the Programmer's Reference D12, cross-checked vs QK4 (`R-EXT-03`).* | 2026-07-25 | 0.48 | DC0SK | Added FR-FM-02 as a **DTMF keypad** (`DM`) — the part of the gap-analysis item that is both buildable and useful remotely: sending DTMF for repeater/link control cannot be done any other way over the link. A 4×4 popup opened from the FM panel, one `DM;` per key. **Scoped down from the gap analysis on purpose:** the '6 stored DTMF sequences' are config work deferred to a follow-up, and the **1750 Hz tone burst has no documented CAT command** in D12 (searched), so it is not buildable now rather than guessed at — the `RO`/`RA` lesson. `send_dtmf` refuses a non-DTMF character rather than emitting a malformed `DM`. | | 2026-07-25 | 0.49 | DC0SK | Added FR-XVTR-01 (transverter band setup), the last substantial backlog item — complex and niche (transverter operators), but fully documented so buildable without hardware-guessing. Six `XV*` encoders (`XVN`/`XVM`/`XVR`/`XVI`/`XVO`/`XVP`), a read-back parser for each field, and a setup form on the BAND screen. The design turns on `XVN` being **stateful** — it selects the band the other commands target — so every field send is prefixed with `XVN`, and the form re-reads all fields when a band is picked, keyed on the `XVN` the radio confirms so a stale value never lands. The form outgrew the fixed-height config-screen slot and clipped; fixed by compacting it to three rows and wrapping the BAND screen in a scrollable. Deferred, and said so: the **mW power scale on XVTR bands** (showing mW instead of W when operating on a configured transverter band) — it needs the current-band-is-XVTR state wired through, and is an operating-display concern separate from this setup form. | | 2026-07-26 | 0.50 | DC0SK | Added FR-UI-UPD-02 (automatic update check + top-area notification), requested by DC0SK; **recorded, not yet implemented**. It is a deliberate, operator-chosen relaxation of FR-UI-UPD-01, which made the update check *manual-only* on the reasoning that "a radio-control app should not make unannounced outbound connections, and a remote station may be on a metered link." The automatic check is therefore constrained to bound that cost: default-on but **opt-out in Settings**, **once per start** rather than on a timer, and **silent** unless it finds a substantiated newer release — so a metered link sees at most one small request per launch, and only a real update ever draws attention. The notification lives in the top status area beside the connection indicator (not a modal), as a clickable link to the release page, reusing FR-UI-UPD-01's numeric, never-spurious comparison. Also fixed a stale/duplicated `version` block in this document's YAML frontmatter (a merge artifact: two `version:` keys) — set to 0.50 / 2026-07-26. | +| 2026-07-26 | 0.51 | DC0SK | **Implemented FR-UI-UPD-02** (recorded in 0.50). Default-on `auto_update_check` preference with a Settings toggle; a single start-up check reusing the operator-initiated path (`update::check_now`), off the UI thread; and a top-area notification beside the connection indicator — a clickable `● update ` link, shown only for an `Available` status, silent for checking/up-to-date/failed. Raised from priority C to **S** now that it is built and tested. The metered-link concern from FR-UI-UPD-01 is honoured by construction: one request per launch, opt-out, and nothing shown unless there is a real update. Verified the link and the toggle on screen; the start-up check was seen overwriting a seeded status, confirming it actually runs. | diff --git a/docs/test/coverage.generated.md b/docs/test/coverage.generated.md index 8a8c72c..09eb4df 100644 --- a/docs/test/coverage.generated.md +++ b/docs/test/coverage.generated.md @@ -162,7 +162,7 @@ Legend: ✅ test-traced · 🟡 waived (see r3-waivers.md) · ⚪ not test-requi | `FR-UI-TIP-01` | C | T/D | ✅ | | `FR-UI-TX-01` | M | T/D | ✅ | | `FR-UI-UPD-01` | C | T/D | ✅ | -| `FR-UI-UPD-02` | C | T/D | ⚪ | +| `FR-UI-UPD-02` | S | T/D | ✅ | | `FR-VFO-01` | M | T | ✅ | | `FR-VFO-02` | M | T | ✅ | | `FR-VFO-03` | M | T | ✅ | diff --git a/docs/test/test-strategy.md b/docs/test/test-strategy.md index 8039ce0..08448a7 100644 --- a/docs/test/test-strategy.md +++ b/docs/test/test-strategy.md @@ -1,8 +1,8 @@ --- title: "Test Strategy & Traceability" status: Draft -version: "4.0" -updated: 2026-07-22 +version: "4.1" +updated: 2026-07-26 authors: - Simon Keimer (DC0SK) owns: [TC] @@ -147,6 +147,7 @@ Maintained partly by hand (design intent) and verified/augmented by `xtask trace | FR-PAN-11 | STK-09 | ARC-11 | TC-PAN-11 (bin→x round-trips against column_to_bin, bins are cells not points, scaling independent of bin count) | L1 | V\* | | FR-ATU-01 | STK-10 | ARC-04 | TC-ATU-01 (`AT1/AT2/AT/` encode; `AT0` unrepresentable) | L1 | V\* | | FR-UI-UPD-01 | STK-16 | ARC-15 | TC-UPD-01 (numeric version compare, `v` prefix, pre-release suffix, junk rejected), TC-UPD-02 (release-reply parse: escapes, missing tag, rate-limit reply, `html_url` fallback) | L1 | V\* | +| FR-UI-UPD-02 | STK-16 | ARC-08 | TC-AUTOUPD-01 (`auto_update_check` defaults on and round-trips; reuses `is_newer` so no spurious update) | L1/L4 | V\* | | FR-TX-TUNE-01 | STK-10/13 | ARC-04/06 | TC-TUNE-01 (`TU0–TU4` encode + `transmits()`), TC-TUNE-02 (arm gate, exit ungated, mic path stays closed, e-stop ends a tune) | L1 | V\* | | FR-UI-TIP-01 | STK-16 | ARC-15 | TC-TIP-01 (tips usable + single-line + end in a stop), TC-TIP-02 (ids unique), TC-TIP-03 (most name a CAT mnemonic), TC-TIP-04 (lookup, unknown id → None), TC-TIP-05 (500 ms delay) | L1 | V\* | | FR-UI-HOLD-01 | STK-16 | ARC-15 | TC-HOLD-01 (500 ms boundary, unknown press = tap), TC-HOLD-02 (AGC tap never reaches off; hold toggles; the two differ while AGC is on) | L1 | V\* | @@ -434,3 +435,4 @@ FR-SES-MULTI, FR-DIAG-02, etc. — get `TC` IDs when promoted to `Approved`.)* | 2026-07-25 | 3.11 | DC0SK | **FR-XVTR-01 — transverter band setup**, the last substantial backlog item. Six `XV*` encoders + per-field read-back parser + a setup form on the BAND screen. Complex and niche but fully documented, so buildable without hardware-guessing (unlike the audio-character and message items, still hardware-blocked). Design point: `XVN` is **stateful** (selects the band the rest target), so each field send is prefixed with `XVN` and the form reloads on band-select keyed on the `XVN` the radio confirms — a stale value can never land in a field. The form clipped the fixed-height config slot at first; compacted to three rows and the BAND screen wrapped in a scrollable. R5 caught three encoders passed as bare `.map()` references with no paren-call site — switched to closures so the capability is genuinely reachable, not just defined. Deferred, and recorded: the mW power scale on XVTR bands (an operating-display concern needing current-band state, separate from setup). Verified: all six encoders and the read-back parse by test; the form on screen in `--demo`. 325 tests. | | 2026-07-25 | 3.12 | DC0SK | **Transverter setup moved to a second column** after DC0SK found the single stacked column grew a vertical scrollbar in the config slot — an ergonomics problem (scrolling a setup form is worse than the space it saves). The BAND screen is now two columns: HF/6 m band selection left, transverter select + setup form right, with the form's four numeric fields laid two-per-row (`Lower`/`IF`, `Offset`/`Power`) now that the half-width column gives horizontal room. No scrollbar; everything fits the fixed-height slot. Verified on screen in `--demo`. | | 2026-07-25 | 4.0 | DC0SK | Release **v0.8.0**. Minor: six backlog features and two operating fixes since 0.7.0, nearly all validated on DC0SK's live K4. Added: VFO lock read-back + tuning refusal, DATA rate select, `ACN` antenna names, on-screen macros (Fn → MACROS, reusing the K-Pod table), a DTMF keypad, and transverter band setup (two-column BAND screen). Fixed: TX TEST now flashes distinct from a real transmit (finishing FR-TX-TUNE-01's flashing indication), and DATA sub-mode/rate switching lag — the same read-back fight the sliders had, fixed with the standing optimistic-override pattern. **What is left is now honestly the hard part:** the audio-character (`MX`/`BL`/`FX`/`AL`) and message (`DARM`) items are blocked on two hardware questions only the operator can answer — whether radio-side audio settings reach the remote stream, and whose microphone `DARM` records. Version bumped in Cargo.toml (workspace), lockfile, README, user manual. 326 tests. | +| 2026-07-26 | 4.1 | DC0SK | **FR-UI-UPD-02 implemented** — automatic update check + top-area notification. Default-on preference (opt-out in Settings), one start-up check off the UI thread reusing `update::check_now` (so the whole never-spurious comparison from FR-UI-UPD-01 comes for free), and a clickable `● update ` link beside the connection indicator, shown only for an `Available` result. The metered-link caution FR-UI-UPD-01 was written around is met by construction, not overridden: one request per launch, opt-out, silent unless there is a real update. Tested at the config layer (default-on + persistence) and verified on screen — including watching the start-up check overwrite a seeded status, which confirmed it runs. 327 tests. |