From 7cdc3e76520755d0eff1341da27edd62bf0491d3 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Sat, 5 Sep 2026 11:01:20 +0400 Subject: [PATCH] feat(update-checker): give manual checks visible, timestamped feedback - Manual check finds no newer version: status message becomes "Redline X.Y.Z is up to date, checked HH:MM Dubai" - Manual check stages a newer version: status message becomes "Update to X.Y.Z is ready" - Automatic (scheduled) checks keep original silent behaviour when up to date - Add snapshot-only seams for testing: snapshotPreviousVersionOverride and snapshotUsesPreviousVersionOverride Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp --- Sources/Shotdeck/UpdateChecker.swift | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Sources/Shotdeck/UpdateChecker.swift b/Sources/Shotdeck/UpdateChecker.swift index 18ff2d0..5b705cb 100644 --- a/Sources/Shotdeck/UpdateChecker.swift +++ b/Sources/Shotdeck/UpdateChecker.swift @@ -2,6 +2,7 @@ import AppKit import CryptoKit import Foundation import Security +import ShotdeckCore /// Built-in updater. Checks an appcast, stages a verified payload, and installs /// only when the user clicks the menu row — never automatically. @@ -31,6 +32,10 @@ final class UpdateChecker { private var repeatingTimer: Timer? private var firstCheckTask: Task? private var stagingDirectory: URL? + /// Snapshot-only override for previousVersion; when snapshotUsesPreviousVersionOverride is true, + /// this value (including nil) is returned instead of checking the file system. + var snapshotPreviousVersionOverride: String? + var snapshotUsesPreviousVersionOverride: Bool = false init() { let config = URLSessionConfiguration.ephemeral @@ -85,7 +90,12 @@ final class UpdateChecker { guard Self.isNewer(appcast.version, than: Self.currentVersion()) else { clearOffer() - statusMessage = manual ? "Redline \(Self.currentVersion()) is up to date." : nil + if manual { + let timestamp = DubaiTime.checkTime(lastCheckedAt ?? Date()) + statusMessage = "Redline \(Self.currentVersion()) is up to date, checked \(timestamp)" + } else { + statusMessage = nil + } onChecked?() return } @@ -93,7 +103,7 @@ final class UpdateChecker { do { try await downloadAndStage(appcast) availableUpdate = (version: appcast.version, notes: appcast.notes ?? "") - statusMessage = nil + statusMessage = manual ? "Update to \(appcast.version) is ready" : nil } catch UpdateCheckError.checksumMismatch { discardStaging() availableUpdate = nil @@ -226,8 +236,11 @@ final class UpdateChecker { } /// The version recorded in `Redline.app.previous`'s Info.plist, or nil when no - /// rollback copy exists. + /// rollback copy exists. Respects the snapshot-only override for panel rendering. func previousVersion(target: URL = UpdateChecker.defaultInstallTarget) -> String? { + if snapshotUsesPreviousVersionOverride { + return snapshotPreviousVersionOverride + } let previousURL = target.deletingLastPathComponent().appendingPathComponent("Redline.app.previous") let plistURL = previousURL.appendingPathComponent("Contents/Info.plist") guard let plist = NSDictionary(contentsOf: plistURL) as? [String: Any] else { return nil }