skip refreshes while offline, catch up when the connection returns

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 <noreply@anthropic.com>
This commit is contained in:
maxsoch 2026-07-09 19:27:41 +02:00
parent 3f130af56a
commit c6372ece05

View file

@ -1,4 +1,5 @@
import Cocoa import Cocoa
import Network
import UserNotifications import UserNotifications
@main @main
@ -43,6 +44,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
var cachedOutdated: OutdatedPackages = .none var cachedOutdated: OutdatedPackages = .none
var lastNotifiedOutdated: Set<String> = [] var lastNotifiedOutdated: Set<String> = []
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 // MARK: - Menu
func applicationDidFinishLaunching(_: Notification) { func applicationDidFinishLaunching(_: Notification) {
@ -91,7 +97,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
statusItem.menu = menu 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() refreshBrewVersion()
refreshTimer = Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { _ in 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 offlineonline
/// 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() { func refreshAll() {
guard isOnline else {
statusMenuItem.title = "📡 Offline"
return
}
statusMenuItem.title = "Updating..." statusMenuItem.title = "Updating..."
runBrew("update") { _ in runBrew("update") { _ in