From 6eae42ff3dd95ffeee649de3943d1417acf70399 Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Tue, 11 Aug 2026 18:36:08 +0100 Subject: [PATCH] STRICT radius enforcement: never fall back to out-of-radius stations in list, cheapest reference, or widget --- FuelBoard/ContentView.swift | 20 +++++++++----------- FuelBoardWidgets/FuelPriceWidget.swift | 5 +++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/FuelBoard/ContentView.swift b/FuelBoard/ContentView.swift index 46cdf60..8de8399 100644 --- a/FuelBoard/ContentView.swift +++ b/FuelBoard/ContentView.swift @@ -23,15 +23,14 @@ struct ContentView: View { /// Stations within the CHOSEN search radius (miles) of the current location /// — 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 - /// unknown or the radius pool is empty. + /// exactly what's on screen. STRICT: no fallback to out-of-radius stations + /// (a cached fetch around another location must never leak far results in). 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 within = stations.filter { + return stations.filter { $0.distanceKM(to: location.lat, lng2: location.lng) <= radiusKM } - return within.isEmpty ? stations : within } private var cheapestPrice: Double? { @@ -45,18 +44,17 @@ struct ContentView: View { } private var sortedStations: [FuelStation] { - // Stations selling the selected fuel, scoped to the chosen miles radius - // from the current location. Local enforcement matters: the cache can - // hold stations from a previous, larger radius, and the relay filter - // only applies at fetch time. + // Stations selling the selected fuel, scoped STRICTLY to the chosen + // miles radius from the current location. Local enforcement matters: + // the cache can hold stations from a previous, larger radius (or a + // different area), and the relay filter only applies at fetch time. let selling = stations.filter { $0.prices[selectedFuel] != nil } let available: [FuelStation] if let location { let radiusKM = Double(stationLimit) * 1.60934 // miles → km - let within = selling.filter { + available = selling.filter { $0.distanceKM(to: location.lat, lng2: location.lng) <= radiusKM } - available = within.isEmpty ? selling : within } else { available = selling } diff --git a/FuelBoardWidgets/FuelPriceWidget.swift b/FuelBoardWidgets/FuelPriceWidget.swift index 61ee2b5..900ea77 100644 --- a/FuelBoardWidgets/FuelPriceWidget.swift +++ b/FuelBoardWidgets/FuelPriceWidget.swift @@ -91,10 +91,11 @@ struct FuelPriceTimelineProvider: TimelineProvider { if stations.isEmpty { stations = SampleFuelProvider.sampleStations } let radiusKM = Double(FuelStore.loadStationLimit()) * 1.60934 // miles → km 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 } - if !within.isEmpty { stations = within } } let filtered = stations.filter { $0.prices[fuel] != nil } let sorted: [FuelStation]