STRICT radius enforcement: never fall back to out-of-radius stations in list, cheapest reference, or widget

This commit is contained in:
FuelBoard Contributor
2026-08-11 18:36:08 +01:00
parent e0a5bb3d5d
commit 6eae42ff3d
2 changed files with 12 additions and 13 deletions
+9 -11
View File
@@ -23,15 +23,14 @@ struct ContentView: View {
/// Stations within the CHOSEN search radius (miles) of the current location /// Stations within the CHOSEN search radius (miles) of the current location
/// the same pool the list shows, so "cheapest" (RAG, TOP, deltas) matches /// the same pool the list shows, so "cheapest" (RAG, TOP, deltas) matches
/// exactly what's on screen. Falls back to the full fetch when location is /// exactly what's on screen. STRICT: no fallback to out-of-radius stations
/// unknown or the radius pool is empty. /// (a cached fetch around another location must never leak far results in).
private var radiusScopedStations: [FuelStation] { private var radiusScopedStations: [FuelStation] {
guard let location else { return stations } guard let location else { return [] }
let radiusKM = Double(stationLimit) * 1.60934 // chosen miles km let radiusKM = Double(stationLimit) * 1.60934 // chosen miles km
let within = stations.filter { return stations.filter {
$0.distanceKM(to: location.lat, lng2: location.lng) <= radiusKM $0.distanceKM(to: location.lat, lng2: location.lng) <= radiusKM
} }
return within.isEmpty ? stations : within
} }
private var cheapestPrice: Double? { private var cheapestPrice: Double? {
@@ -45,18 +44,17 @@ struct ContentView: View {
} }
private var sortedStations: [FuelStation] { private var sortedStations: [FuelStation] {
// Stations selling the selected fuel, scoped to the chosen miles radius // Stations selling the selected fuel, scoped STRICTLY to the chosen
// from the current location. Local enforcement matters: the cache can // miles radius from the current location. Local enforcement matters:
// hold stations from a previous, larger radius, and the relay filter // the cache can hold stations from a previous, larger radius (or a
// only applies at fetch time. // different area), and the relay filter only applies at fetch time.
let selling = stations.filter { $0.prices[selectedFuel] != nil } let selling = stations.filter { $0.prices[selectedFuel] != nil }
let available: [FuelStation] let available: [FuelStation]
if let location { if let location {
let radiusKM = Double(stationLimit) * 1.60934 // miles km let radiusKM = Double(stationLimit) * 1.60934 // miles km
let within = selling.filter { available = selling.filter {
$0.distanceKM(to: location.lat, lng2: location.lng) <= radiusKM $0.distanceKM(to: location.lat, lng2: location.lng) <= radiusKM
} }
available = within.isEmpty ? selling : within
} else { } else {
available = selling available = selling
} }
+3 -2
View File
@@ -91,10 +91,11 @@ struct FuelPriceTimelineProvider: TimelineProvider {
if stations.isEmpty { stations = SampleFuelProvider.sampleStations } if stations.isEmpty { stations = SampleFuelProvider.sampleStations }
let radiusKM = Double(FuelStore.loadStationLimit()) * 1.60934 // miles km let radiusKM = Double(FuelStore.loadStationLimit()) * 1.60934 // miles km
if let location { if let location {
let within = stations.filter { // STRICT: cached data fetched around another location must never
// leak out-of-radius stations into the widget.
stations = stations.filter {
$0.distanceKM(to: location.lat, lng2: location.lng) <= radiusKM $0.distanceKM(to: location.lat, lng2: location.lng) <= radiusKM
} }
if !within.isEmpty { stations = within }
} }
let filtered = stations.filter { $0.prices[fuel] != nil } let filtered = stations.filter { $0.prices[fuel] != nil }
let sorted: [FuelStation] let sorted: [FuelStation]