Trends: price-history fetch failure with no data raises the connection banner
A history fetch that comes back empty BECAUSE the mirror is unreachable
(pointer probe failed) was only an in-sheet retry card — no global signal.
Now it also raises the red connection banner ('Check your internet
connection / Tap to try again'), so a network problem is visible on every
tab, not just inside the Trends sheet. A successful load clears the
banner (only when the banner is the connection banner — never clobbers
the offline-dump banner).
- TrendsView: onHistoryUnavailable/onHistoryRecovered closures fired from
load() (loadFailed -> unavailable; hasAnyData -> recovered);
-forceHistoryFailure QA hook (forces the unreachable state, skips
auto-refresh like the other force-* hooks)
- FavouritesView: closures threaded through the Trends sheet init
- ContentView: wires them to dataStatus (.live -> .connectionProblem on
failure; recovered clears only .connectionProblem)
- fuelboard-development skill: hook + wiring documented
This commit is contained in:
@@ -284,7 +284,7 @@ struct ContentView: View {
|
|||||||
updateLiveActivity()
|
updateLiveActivity()
|
||||||
// Refresh only when the cache is stale (twice-a-day policy).
|
// Refresh only when the cache is stale (twice-a-day policy).
|
||||||
// Skipped under the force-* hooks so the banner stays up.
|
// 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() }
|
Task { await refresh() }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -526,7 +526,9 @@ struct ContentView: View {
|
|||||||
distanceUnit: distanceUnit,
|
distanceUnit: distanceUnit,
|
||||||
priceDisplayStyle: priceDisplayStyle,
|
priceDisplayStyle: priceDisplayStyle,
|
||||||
onToggleFavourite: toggleFavourite,
|
onToggleFavourite: toggleFavourite,
|
||||||
onReorder: reorderFavourites
|
onReorder: reorderFavourites,
|
||||||
|
onHistoryUnavailable: { if dataStatus == .live { dataStatus = .connectionProblem } },
|
||||||
|
onHistoryRecovered: { if dataStatus == .connectionProblem { dataStatus = .live } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ struct FavouritesView: View {
|
|||||||
var onToggleFavourite: (FuelStation, FuelType) -> Void = { _, _ in }
|
var onToggleFavourite: (FuelStation, FuelType) -> Void = { _, _ in }
|
||||||
/// Persists a reordered favourites array (after drag-and-drop).
|
/// Persists a reordered favourites array (after drag-and-drop).
|
||||||
var onReorder: ([FavouriteEntry]) -> Void = { _ in }
|
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
|
/// Fuel types that currently have at least one favourite — these are the
|
||||||
/// only tabs shown (a fuel with no favourites gets no tab).
|
/// only tabs shown (a fuel with no favourites gets no tab).
|
||||||
@@ -62,7 +66,9 @@ struct FavouritesView: View {
|
|||||||
distanceUnit: DistanceUnit,
|
distanceUnit: DistanceUnit,
|
||||||
priceDisplayStyle: PriceDisplayStyle,
|
priceDisplayStyle: PriceDisplayStyle,
|
||||||
onToggleFavourite: @escaping (FuelStation, FuelType) -> Void = { _, _ in },
|
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.favourites = favourites
|
||||||
self.selectedFuel = selectedFuel
|
self.selectedFuel = selectedFuel
|
||||||
self.location = location
|
self.location = location
|
||||||
@@ -70,6 +76,8 @@ struct FavouritesView: View {
|
|||||||
self.priceDisplayStyle = priceDisplayStyle
|
self.priceDisplayStyle = priceDisplayStyle
|
||||||
self.onToggleFavourite = onToggleFavourite
|
self.onToggleFavourite = onToggleFavourite
|
||||||
self.onReorder = onReorder
|
self.onReorder = onReorder
|
||||||
|
self.onHistoryUnavailable = onHistoryUnavailable
|
||||||
|
self.onHistoryRecovered = onHistoryRecovered
|
||||||
_fuel = State(initialValue: selectedFuel)
|
_fuel = State(initialValue: selectedFuel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +166,9 @@ struct FavouritesView: View {
|
|||||||
TrendsView(
|
TrendsView(
|
||||||
favourites: favourites,
|
favourites: favourites,
|
||||||
selectedFuel: activeFuel,
|
selectedFuel: activeFuel,
|
||||||
priceDisplayStyle: priceDisplayStyle
|
priceDisplayStyle: priceDisplayStyle,
|
||||||
|
onHistoryUnavailable: onHistoryUnavailable,
|
||||||
|
onHistoryRecovered: onHistoryRecovered
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ struct TrendsView: View {
|
|||||||
let selectedFuel: FuelType
|
let selectedFuel: FuelType
|
||||||
let priceDisplayStyle: PriceDisplayStyle
|
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
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@State private var fuel: FuelType = .e10
|
@State private var fuel: FuelType = .e10
|
||||||
@@ -80,6 +86,15 @@ struct TrendsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func load() async {
|
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
|
isLoading = true
|
||||||
loadFailed = false
|
loadFailed = false
|
||||||
defer { isLoading = false }
|
defer { isLoading = false }
|
||||||
@@ -98,6 +113,14 @@ struct TrendsView: View {
|
|||||||
loadFailed = firstSnapshot == nil
|
loadFailed = firstSnapshot == nil
|
||||||
}
|
}
|
||||||
series = fetched
|
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 {
|
private func yLabel(_ pence: Double) -> String {
|
||||||
|
|||||||
Reference in New Issue
Block a user