From c6372ece05b865e0fdc3cbb2121a15ed525f9e35 Mon Sep 17 00:00:00 2001 From: maxsoch Date: Thu, 9 Jul 2026 19:27:41 +0200 Subject: [PATCH] skip refreshes while offline, catch up when the connection returns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit brew outdated never touches the network: offline, a refresh silently succeeded on stale local tap data and reported "all up to date". An NWPathMonitor now gates refreshAll — offline shows a dedicated status instead, and every offline→online transition triggers a refresh, which covers the launch refresh, scheduled refreshes skipped during an outage, and clearing the offline status when the connection returns. Co-Authored-By: Claude Fable 5 --- BrewBar/BrewBarApp.swift | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/BrewBar/BrewBarApp.swift b/BrewBar/BrewBarApp.swift index b56da5a..79ef580 100644 --- a/BrewBar/BrewBarApp.swift +++ b/BrewBar/BrewBarApp.swift @@ -1,4 +1,5 @@ import Cocoa +import Network import UserNotifications @main @@ -43,6 +44,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { var cachedOutdated: OutdatedPackages = .none var lastNotifiedOutdated: Set = [] + let pathMonitor = NWPathMonitor() + /// false until the monitor's first update, so the launch refresh waits + /// until the network state is actually known. Main-queue only. + var isOnline = false + // MARK: - Menu func applicationDidFinishLaunching(_: Notification) { @@ -91,7 +97,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { statusItem.menu = menu - refreshAll() + // no refreshAll() here: the monitor fires once immediately after + // start(), and the first online update triggers the launch refresh + pathMonitor.pathUpdateHandler = { path in + // updates arrive on the monitor's queue; hop to main for AppKit + DispatchQueue.main.async { + self.networkPathChanged(online: path.status == .satisfied) + } + } + pathMonitor.start(queue: DispatchQueue(label: "fr.socheleau.BrewBar.network")) + refreshBrewVersion() refreshTimer = Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { _ in @@ -431,7 +446,30 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + /// Refreshing offline would silently succeed on stale data: brew update + /// fails but its exit code was never checked, and brew outdated compares + /// against the local taps without touching the network — so the app + /// claimed "all up to date" no matter what. Every offline→online + /// transition refreshes, which also covers the refreshes skipped below. + func networkPathChanged(online: Bool) { + let wasOnline = isOnline + isOnline = online + + guard online else { + statusMenuItem.title = "📡 Offline" + return + } + if !wasOnline { + refreshAll() + } + } + func refreshAll() { + guard isOnline else { + statusMenuItem.title = "📡 Offline" + return + } + statusMenuItem.title = "Updating..." runBrew("update") { _ in