From 3a45e2b680cb33f79267be672106a01117baa78f Mon Sep 17 00:00:00 2001 From: SusieeTheFemboy <96988330+SusieeTheLinuxUser@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:16:07 +0100 Subject: [PATCH] fix(update): a failed check is not "you're on the latest version" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check_update turned a GitHub error into None, which Settings → About reads as up to date and which also cleared an update already offered in the sidebar. The command now rejects on failure, and the sidebar keeps the offer it was already showing. Co-Authored-By: grok-4.7 --- CHANGELOG.md | 1 + renderer/app.js | 19 ++++++++++++------ src-tauri/src/main.rs | 40 +++++++++++++++++++++++++++++++------ tools/preview/selfcheck.mjs | 12 +++++++++-- 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2d4464..5b08268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to DealDrop are documented in this file. ## [Unreleased] +- Fixed: Settings → About → **Check for updates** says GitHub couldn't be reached when the check fails, instead of "You're on the latest version." A failed check also no longer hides an update that was already offered. - Linux: DealDrop is now a **Flatpak** (`DealDrop__x86_64.flatpak`) instead of an AppImage. Install it once by hand; settings, sign-ins and "Start when I log in" carry over from the AppImage (same `~/.config/dealdrop/`). The AppImage gets no more updates. - **Update to vX.Y.Z** in the sidebar when a newer release is out (checked at start and every 6 hours). The Flatpak downloads and installs it and restarts; Windows and macOS open the release page. - Settings → About has a **Check for updates** button that says whether you're on the latest version (or that GitHub couldn't be reached). diff --git a/renderer/app.js b/renderer/app.js index d370244..0ffc338 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -2014,19 +2014,26 @@ async function copyError () { announceStatus('Copied') } +// A check that couldn't reach GitHub is not "no update": keep the offer already shown. +function updateAfterCheck (shown, answer, reached) { + return reached ? answer : shown +} + // "Update to vX.Y.Z" above the error box when GitHub has a newer release for this platform. // The Flatpak installs it and restarts; other builds open the release page. -// Returns false when the check itself failed; the 6-hourly calls stay silent about that. +// Returns false when the check itself failed; the 6-hourly calls stay silent about that, +// and an update already offered stays up. async function checkUpdate () { - let ok = true - state.update = await window.api.checkUpdate().catch(() => { ok = false; return null }) - if (ok && !state.update && state.updateError) { state.updateError = null; renderErrors() } // the failed update is gone (installed by hand, or withdrawn) + let reached = true + const answer = await window.api.checkUpdate().catch(() => { reached = false; return null }) + state.update = updateAfterCheck(state.update, answer, reached) + if (reached && !state.update && state.updateError) { state.updateError = null; renderErrors() } // the failed update is gone (installed by hand, or withdrawn) const box = document.getElementById('sidebar-update') const btn = document.getElementById('btn-update') - if (!box || !btn || btn.disabled) return ok + if (!box || !btn || btn.disabled) return reached box.style.display = state.update ? 'block' : 'none' if (state.update) btn.textContent = `Update to v${state.update.version}` - return ok + return reached } // Settings → About: a check the user asked for, so it says what happened (textContent: the version comes from the network). diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a609801..9494281 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1042,13 +1042,21 @@ fn get_autostart() -> bool { autostart::get() } #[tauri::command] fn set_autostart(on: bool) -> Result<(), String> { autostart::set(on) } +/// `Ok(None)` means this platform is up to date. An error stays an error: the UI reads `None` +/// as "You're on the latest version", so a GitHub failure must not look like that. +fn offered_update(found: Result, String>, in_flatpak: bool) -> Result, String> { + found.map(|up| up.map(|u| Update { can_install: u.bundle.is_some() && in_flatpak, ..u })) +} + /// A newer release for this platform, if any (the UI asks at start and every 6 hours). +/// Errors reject the command so Settings → About can say GitHub couldn't be reached. #[tauri::command] -async fn check_update() -> Option { - match latest_release().await { - Ok(up) => up.map(|u| Update { can_install: u.bundle.is_some() && update::in_flatpak(), ..u }), - Err(e) => { eprintln!("[update check] {e}"); None } - } +async fn check_update() -> Result, String> { + let found = match latest_release().await { + Ok(up) => Ok(up), + Err(e) => { eprintln!("[update check] {e}"); Err(e) } + }; + offered_update(found, update::in_flatpak()) } /// Flatpak only: downloads the newest release's bundle, installs it and restarts into it. @@ -1217,9 +1225,29 @@ fn main() { #[cfg(test)] mod tests { - use super::{LiveSale, days_from_civil, sale_soon_notice, sale_starting_tomorrow, SALES, clamp_cut, dedupe_free, failure, free_notice, fresh_big_deals, fresh_free, fresh_wishlist, is_hidden, keep_or_fail, keep_prices, carry_cheapest, carry_deal_lowest, normalize_hidden, normalize_targets, target_hits, target_notice, price_failure, CS_BLOCKED, normalize_title, remember_big_deals, remember_free, remember_wishlist, restore_by_prefix, safe_https, sale_key, should_prime, store_label, tray_counts, tray_tooltip, with_count, wishlist_notice, load_db, proton_suffix, Acct, Db, Deal, Free, Price, Prime, Settings, SteamProfile, Tokens, WishItem, accounts, same_refresh, BIG_DEAL_CUTS, WISHLIST_CUTS}; + use super::{LiveSale, days_from_civil, sale_soon_notice, sale_starting_tomorrow, SALES, clamp_cut, dedupe_free, failure, free_notice, fresh_big_deals, fresh_free, fresh_wishlist, is_hidden, keep_or_fail, keep_prices, carry_cheapest, carry_deal_lowest, normalize_hidden, normalize_targets, target_hits, target_notice, price_failure, CS_BLOCKED, normalize_title, remember_big_deals, remember_free, remember_wishlist, restore_by_prefix, safe_https, sale_key, should_prime, store_label, tray_counts, tray_tooltip, with_count, wishlist_notice, load_db, proton_suffix, Acct, Db, Deal, Free, Price, Prime, Settings, SteamProfile, Tokens, Update, WishItem, accounts, offered_update, same_refresh, BIG_DEAL_CUTS, WISHLIST_CUTS}; use std::collections::{HashMap, HashSet}; + #[test] + fn a_failed_update_check_is_not_the_same_as_being_up_to_date() { + let bundle = Some("https://github.com/SusieeTheLinuxUser/dealdrop/releases/download/v0.10.0/DealDrop_0.10.0_x86_64.flatpak".into()); + let up = Update { + version: "0.10.0".into(), + page: "https://github.com/SusieeTheLinuxUser/dealdrop/releases/tag/v0.10.0".into(), + bundle: bundle.clone(), + can_install: false, + }; + let flatpak = offered_update(Ok(Some(up.clone())), true).unwrap().unwrap(); + assert!(flatpak.can_install); // only the Flatpak installs the bundle itself + assert_eq!(flatpak.bundle, bundle); + let other = offered_update(Ok(Some(up)), false).unwrap().unwrap(); + assert!(!other.can_install); // Windows, macOS, and a non-Flatpak Linux build open the release page + assert!(!offered_update(Ok(Some(Update { bundle: None, ..other })), true).unwrap().unwrap().can_install); + assert_eq!(offered_update(Ok(None), true).unwrap(), None); // a real "nothing newer" + // the About button reads a rejected command as "Couldn't reach GitHub", not as up to date + assert_eq!(offered_update(Err("HTTP 503".into()), true), Err("HTTP 503".into())); + } + #[cfg(unix)] #[test] fn a_config_that_cant_be_moved_aside_stops_saving() { diff --git a/tools/preview/selfcheck.mjs b/tools/preview/selfcheck.mjs index c8c03b7..3325e0c 100644 --- a/tools/preview/selfcheck.mjs +++ b/tools/preview/selfcheck.mjs @@ -31,10 +31,11 @@ vm.runInContext([ between('function nextTheme (', '\nfunction syncSaleTheme'), between('function wishlistLowestTag (', '\n// ─── Price targets'), between('const saleKey =', '\n// Live sale →'), + between('function updateAfterCheck (', '\n// "Update to v'), between('const targetKey', '\nfunction wishlistTargetTag'), between('const nextIndex', '\n// Moves focus between'), between('const fmt$ =', '\n// CSP forbids inline onerror'), - 'globalThis.helpers = { SALES, state, filteredSorted, storeKeyFor, escHtml, updateRefreshAge, mergeUiPrefs, dealShops, parseClaimed, pruneClaimed, toggleClaimed, claimedLast, normalizeTitle, hiddenTitle, searchRank, parseSeen, newIds, mergeSeen, wishlistKey, seenIds, salesSeenIds, PAGE_SIZE, pageSlice, nextTheme, targetKey, targetPrice, targetHit, parseTarget, protonTier, protonText, protonTag, nextIndex, fmt$, salePriceDescription, dataSig, timeLeft, endsFirst, endDateLabel, startDateLabel, fmtChecked, gameStoreName, srcIcon, wishlistLowestTag, saleKey, BASE_THEMES, SALE_THEMES }', + 'globalThis.helpers = { SALES, state, filteredSorted, storeKeyFor, escHtml, updateRefreshAge, mergeUiPrefs, dealShops, parseClaimed, pruneClaimed, toggleClaimed, claimedLast, normalizeTitle, hiddenTitle, searchRank, parseSeen, newIds, mergeSeen, wishlistKey, seenIds, salesSeenIds, PAGE_SIZE, pageSlice, nextTheme, targetKey, targetPrice, targetHit, parseTarget, protonTier, protonText, protonTag, nextIndex, fmt$, salePriceDescription, dataSig, timeLeft, endsFirst, endDateLabel, startDateLabel, fmtChecked, gameStoreName, srcIcon, wishlistLowestTag, saleKey, updateAfterCheck, BASE_THEMES, SALE_THEMES }', ].join('\n'), context) const h = context.helpers @@ -360,6 +361,13 @@ assert.equal(h.saleKey({ name: 'Autumn Sale' }), 'autumn') assert.equal(h.saleKey({ name: 'Weekly Deals' }), undefined) assert.equal(h.saleKey({ name: 'Steam Scream Fest' }), undefined) // a fest, not a seasonal sale +// A failed update check keeps the offer already shown; only a reached answer replaces it +const offered = { version: '0.10.0' } +assert.equal(h.updateAfterCheck(offered, null, false), offered) +assert.equal(h.updateAfterCheck(null, null, false), null) +assert.equal(h.updateAfterCheck(offered, null, true), null) // reached, and nothing newer: the offer is gone +assert.equal(h.updateAfterCheck(null, offered, true), offered) + // Countdowns: the d/h/soon split is the floor of the exact instant delta (never a wall-clock // day count, which would shift across a DST night); relative durations keep this runnable any day for (const [ms, label] of [[2.5 * 86400000, '2d left'], [86400000 + 60000, '1d left'], [86400000 - 60000, '23h left'], [5 * 3600000 - 60000, '4h left'], [3600000 + 60000, '1h left'], [3600000 - 60000, 'ending soon']]) { @@ -381,4 +389,4 @@ for (const t of jsThemes) { for (const t of cssThemes) assert.equal(jsThemes.has(t), true, `style.css theme "${t}" is missing from BASE_THEMES/SALE_THEMES in app.js`) assert.equal(jsThemes.has('midnight'), true) // and the root default is offered in Settings -console.log('Renderer self-check passed (price targets, escaping, hidden titles, search ranking, filters, store filter, UI preference merge, claimed games, seen deals, list paging, next sale, refresh age, sale themes, J/K navigation, money and date labels, poll data signature, ProtonDB tiers, lowest-ever rule, sale keys, countdown thresholds, theme/CSS consistency).') +console.log('Renderer self-check passed (price targets, escaping, hidden titles, search ranking, filters, store filter, UI preference merge, claimed games, seen deals, list paging, next sale, refresh age, sale themes, J/K navigation, money and date labels, poll data signature, ProtonDB tiers, lowest-ever rule, sale keys, countdown thresholds, theme/CSS consistency, failed update check).')