diff --git a/FuelBoard/ContentView.swift b/FuelBoard/ContentView.swift index 1934340..08f5711 100644 --- a/FuelBoard/ContentView.swift +++ b/FuelBoard/ContentView.swift @@ -284,7 +284,7 @@ struct ContentView: View { updateLiveActivity() // Refresh only when the cache is stale (twice-a-day policy). // Skipped under the force-* hooks so the banner stays up. - if !args.contains("-forceOfflineDump") && !args.contains("-forceConnectionProblem") { + if !args.contains("-forceOfflineDump") && !args.contains("-forceConnectionProblem") && !args.contains("-forceHistoryFailure") { Task { await refresh() } } } else { @@ -526,7 +526,9 @@ struct ContentView: View { distanceUnit: distanceUnit, priceDisplayStyle: priceDisplayStyle, onToggleFavourite: toggleFavourite, - onReorder: reorderFavourites + onReorder: reorderFavourites, + onHistoryUnavailable: { if dataStatus == .live { dataStatus = .connectionProblem } }, + onHistoryRecovered: { if dataStatus == .connectionProblem { dataStatus = .live } } ) } diff --git a/FuelBoard/FavouritesView.swift b/FuelBoard/FavouritesView.swift index b814cdc..dbc9463 100644 --- a/FuelBoard/FavouritesView.swift +++ b/FuelBoard/FavouritesView.swift @@ -17,6 +17,10 @@ struct FavouritesView: View { var onToggleFavourite: (FuelStation, FuelType) -> Void = { _, _ in } /// Persists a reordered favourites array (after drag-and-drop). var onReorder: ([FavouriteEntry]) -> Void = { _ in } + /// Propagated from ContentView — Trends' history failure with no data + /// raises the global connection banner; recovery clears it. + var onHistoryUnavailable: (() -> Void)? = nil + var onHistoryRecovered: (() -> Void)? = nil /// Fuel types that currently have at least one favourite — these are the /// only tabs shown (a fuel with no favourites gets no tab). @@ -62,7 +66,9 @@ struct FavouritesView: View { distanceUnit: DistanceUnit, priceDisplayStyle: PriceDisplayStyle, onToggleFavourite: @escaping (FuelStation, FuelType) -> Void = { _, _ in }, - onReorder: @escaping ([FavouriteEntry]) -> Void = { _ in }) { + onReorder: @escaping ([FavouriteEntry]) -> Void = { _ in }, + onHistoryUnavailable: (() -> Void)? = nil, + onHistoryRecovered: (() -> Void)? = nil) { self.favourites = favourites self.selectedFuel = selectedFuel self.location = location @@ -70,6 +76,8 @@ struct FavouritesView: View { self.priceDisplayStyle = priceDisplayStyle self.onToggleFavourite = onToggleFavourite self.onReorder = onReorder + self.onHistoryUnavailable = onHistoryUnavailable + self.onHistoryRecovered = onHistoryRecovered _fuel = State(initialValue: selectedFuel) } @@ -158,7 +166,9 @@ struct FavouritesView: View { TrendsView( favourites: favourites, selectedFuel: activeFuel, - priceDisplayStyle: priceDisplayStyle + priceDisplayStyle: priceDisplayStyle, + onHistoryUnavailable: onHistoryUnavailable, + onHistoryRecovered: onHistoryRecovered ) } .onAppear { diff --git a/FuelBoard/TrendsView.swift b/FuelBoard/TrendsView.swift index 6d00adb..dfef426 100644 --- a/FuelBoard/TrendsView.swift +++ b/FuelBoard/TrendsView.swift @@ -16,6 +16,12 @@ struct TrendsView: View { let selectedFuel: FuelType let priceDisplayStyle: PriceDisplayStyle + /// Propagated up to ContentView so a price-history fetch that fails with + /// NO data raises the global connection banner (same red banner as the + /// stations fetch). `onHistoryRecovered` fires once data loads again. + var onHistoryUnavailable: (() -> Void)? = nil + var onHistoryRecovered: (() -> Void)? = nil + @Environment(\.dismiss) private var dismiss @State private var fuel: FuelType = .e10 @@ -80,6 +86,15 @@ struct TrendsView: View { } private func load() async { + // QA hook: force the unreachable state for screenshots (same pattern + // as -showTrends / -forceConnectionProblem). Runs before the fetch so + // the retry state renders immediately with no spinner flash. + if ProcessInfo.processInfo.arguments.contains("-forceHistoryFailure") { + series = [] + loadFailed = true + onHistoryUnavailable?() + return + } isLoading = true loadFailed = false defer { isLoading = false } @@ -98,6 +113,14 @@ struct TrendsView: View { loadFailed = firstSnapshot == nil } series = fetched + // A failure with no data IS a connection problem — raise the global + // banner so the user isn't stuck with a silent retry state. Success + // clears it (only if the banner is the connection banner). + if loadFailed { + onHistoryUnavailable?() + } else if hasAnyData { + onHistoryRecovered?() + } } private func yLabel(_ pence: Double) -> String {