Live-test fixes: CarPlay disfavor, widget location refresh, debug cheapest parity

1. CarPlay widget taps could never launch Maps — FuelBoard is not a
   CarPlay app, and Apple only lets a widget launch its own app in
   CarPlay when that app is CarPlay-enabled. Marked the widget as a
   disfavored CarPlay location (iOS 26+; no-op below): read-only in
   the car, no dead tap. Header comment updated to match reality.

2. Widgets now track the user's location: timeline refresh 15m -> 5m,
   and the app's location manager reloads all widget timelines after
   every significant move (250m filter, throttled to 60s), so the
   widget re-orders around the new position while driving instead of
   waiting for the next tick.

3. Debug 'Cheapest in range' no longer shows the ALERT prediction
   (alertsFuel + alert radius — a different pool by design). It now
   mirrors the app list exactly: the TOP-badge station for the
   selected fuel within the stationLimit radius (new DebugAppCheapest
   computed in ContentView, passed to Settings). Alert-prediction
   cheapest kept as fallback when the app view hasn't resolved.
This commit is contained in:
FuelBoard Contributor
2026-08-12 18:01:29 +01:00
parent d37aab04f1
commit f822bd2636
4 changed files with 95 additions and 13 deletions
+31 -1
View File
@@ -67,6 +67,24 @@ struct ContentView: View {
return displayedStations.first { $0.prices[selectedFuel] == baselinePrice }?.id
}
/// The app's own "cheapest in range" the TOP-badge station mirrored
/// into the Settings Debug section so its "Cheapest in range" row
/// matches the app list (and the widget when the widget's fuel + distance
/// match the app's). The debug section previously showed the ALERT
/// prediction (alertsFuel + alert radius), which is a different pool by
/// design that mismatch is what this fixes.
private var appCheapestStatus: DebugAppCheapest? {
guard let topID = topStationID,
let station = stations.first(where: { $0.id == topID }),
let location else { return nil }
return DebugAppCheapest(
station: station,
fuel: selectedFuel,
radiusKM: distanceUnit.toKM(Double(stationLimit)),
distanceKM: station.distanceKM(to: location.lat, lng2: location.lng)
)
}
private var displayedStations: [FuelStation] {
sortedStations
}
@@ -258,7 +276,8 @@ struct ContentView: View {
onPlainTestAlert: { monitor.sendPlainTestNotification() },
onRefreshDebugStatus: { monitor.refreshDebugStatus() },
onShowOnboarding: { showOnboarding = true },
debugStatus: monitor.debugStatus
debugStatus: monitor.debugStatus,
appCheapest: appCheapestStatus
)
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
}
@@ -494,8 +513,19 @@ final class LocationManager: NSObject, ObservableObject, @preconcurrency CLLocat
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let loc = locations.last else { return }
current = Coordinate(lat: loc.coordinate.latitude, lng: loc.coordinate.longitude)
// Drive the widget: every significant move re-orders the widget list
// around the new position immediately, instead of waiting for the
// widget's own 5-minute timeline tick. Throttled WidgetKit ignores
// reload spam and the relay has a rate limit, so 60 s is plenty.
let now = Date()
if now.timeIntervalSince(lastWidgetReload) >= 60 {
lastWidgetReload = now
WidgetCenter.shared.reloadAllTimelines()
}
}
private var lastWidgetReload = Date.distantPast
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
// Ignore the app still works price-sorted without location.
}