Harden App Store build for review
This commit is contained in:
@@ -19,14 +19,15 @@ protocol FuelPriceProviding {
|
||||
}
|
||||
|
||||
enum FuelPriceProvider {
|
||||
/// Default: the live chain — GitHub mirror (primary, off-LAN) → LAN
|
||||
/// relay (fallback) → bundled dump (last resort in the app target).
|
||||
/// Default: the live chain — public GitHub mirror (primary, off-LAN) →
|
||||
/// bundled dump/cache fallback in callers. No private LAN fallback ships in
|
||||
/// the app binary.
|
||||
static let active: FuelPriceProviding = LiveChainProvider()
|
||||
|
||||
/// Decodes a relay payload into stations, applying the defensive price
|
||||
/// Decodes a mirror payload into stations, applying the defensive price
|
||||
/// band. Internal so the unit-test target can exercise the guard.
|
||||
static func decodeStations(from data: Data) throws -> [FuelStation] {
|
||||
let payload = try JSONDecoder().decode(RelayResponse.self, from: data)
|
||||
let payload = try JSONDecoder().decode(MirrorResponse.self, from: data)
|
||||
return payload.stations.map { relay in
|
||||
FuelStation(
|
||||
id: relay.id ?? relay.name ?? UUID().uuidString,
|
||||
@@ -42,13 +43,13 @@ enum FuelPriceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// Price sanity band (pence/litre) — anything outside is relay regression
|
||||
/// Price sanity band (pence/litre) — anything outside is mirror regression
|
||||
/// garbage and must never reach calculations (shared by the live decode
|
||||
/// and the history mirror decoder).
|
||||
static let priceBand: ClosedRange<Double> = 50...500
|
||||
|
||||
/// Maps relay grade keys (E5/E10/DIESEL…) to FuelType with the defensive
|
||||
/// band guard. Shared by the relay decode and the history mirror parser so
|
||||
/// Maps mirror grade keys (E5/E10/DIESEL…) to FuelType with the defensive
|
||||
/// band guard. Shared by the mirror decode and the history mirror parser so
|
||||
/// both surfaces apply identical sanitisation.
|
||||
static func mapGrades(_ prices: [String: Double]) -> [FuelType: Double] {
|
||||
var result: [FuelType: Double] = [:]
|
||||
@@ -64,12 +65,12 @@ enum FuelPriceProvider {
|
||||
return result
|
||||
}
|
||||
|
||||
/// Decodes the relay envelope metadata (source, dataset update time) — the
|
||||
/// Decodes mirror envelope metadata (source, dataset update time) — the
|
||||
/// About section shows these so the user can see which data source is
|
||||
/// live and how fresh the GOV.UK data itself is. The fields are additive
|
||||
/// on the relay, so older relays simply return nil values.
|
||||
/// on the mirror, so older snapshots simply return nil values.
|
||||
static func decodeRelayMeta(from data: Data) -> RelayMeta? {
|
||||
guard let payload = try? JSONDecoder().decode(RelayResponse.self, from: data) else {
|
||||
guard let payload = try? JSONDecoder().decode(MirrorResponse.self, from: data) else {
|
||||
return nil
|
||||
}
|
||||
return RelayMeta(
|
||||
@@ -80,64 +81,11 @@ enum FuelPriceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Relay provider (default)
|
||||
// MARK: - Mirror payload envelope
|
||||
|
||||
/// Talks to the FuelBoard Relay (keyless proxy). The relay serves the FULL-UK
|
||||
/// Fuel Finder dataset — every station in England, Wales, Scotland, NI — and
|
||||
/// filters/sorts server-side. No credentials in the app.
|
||||
struct RelayFuelProvider: FuelPriceProviding {
|
||||
var baseURL = URL(string: "http://192.168.1.131:8789")!
|
||||
|
||||
/// Envelope metadata from the most recent successful relay fetch —
|
||||
/// consumed by Settings → About (source api/csv, station count, GOV.UK
|
||||
/// dataset update time). Written by fetchStations, read by the view.
|
||||
static var latestMeta: RelayMeta?
|
||||
|
||||
/// Admin telemetry: the relay tags inbound connections by client kind
|
||||
/// (app vs widget) so the developer can monitor the poll model. Siri
|
||||
/// never fetches (cached-data-first). Purely observational — the relay
|
||||
/// ignores the header for responses.
|
||||
static let clientTagHeader = "X-Client"
|
||||
|
||||
static func relayRequest(_ url: URL, client: String, timeout: TimeInterval = 30) -> URLRequest {
|
||||
var request = URLRequest(url: url)
|
||||
request.timeoutInterval = timeout
|
||||
request.setValue(client, forHTTPHeaderField: clientTagHeader)
|
||||
return request
|
||||
}
|
||||
|
||||
func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType, radiusKM: Double?) async throws -> [FuelStation] {
|
||||
var components = URLComponents(url: baseURL.appendingPathComponent("api/v1/stations"), resolvingAgainstBaseURL: false)!
|
||||
var query: [URLQueryItem] = [URLQueryItem(name: "fuel", value: fuel.rawValue)]
|
||||
if let lat, let lng {
|
||||
query.append(URLQueryItem(name: "lat", value: String(lat)))
|
||||
query.append(URLQueryItem(name: "lng", value: String(lng)))
|
||||
}
|
||||
if let radiusKM {
|
||||
// Focused fetch (alert path) — small response, server-side radius.
|
||||
query.append(URLQueryItem(name: "radius", value: String(radiusKM)))
|
||||
query.append(URLQueryItem(name: "limit", value: "500"))
|
||||
} else {
|
||||
// Full-UK dump: NO radius — the device filters locally
|
||||
// (5/10/15-mile radius, fuel, sort). 10,000 covers the dataset.
|
||||
query.append(URLQueryItem(name: "limit", value: "10000"))
|
||||
}
|
||||
components.queryItems = query
|
||||
|
||||
let request = RelayFuelProvider.relayRequest(components.url!, client: "app")
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
|
||||
throw FuelProviderError.relayUnavailable
|
||||
}
|
||||
let stations = try FuelPriceProvider.decodeStations(from: data)
|
||||
Self.latestMeta = FuelPriceProvider.decodeRelayMeta(from: data)
|
||||
return stations
|
||||
}
|
||||
}
|
||||
|
||||
private struct RelayResponse: Codable {
|
||||
private struct MirrorResponse: Codable {
|
||||
/// Envelope metadata surfaced in Settings → About so the user can see
|
||||
/// which relay source is live and how fresh the GOV.UK data is.
|
||||
/// which mirror source is live and how fresh the GOV.UK data is.
|
||||
let source: String?
|
||||
let stationsCount: Int?
|
||||
let dataUpdated: String?
|
||||
@@ -148,7 +96,7 @@ private struct RelayResponse: Codable {
|
||||
case dataUpdated = "data_updated"
|
||||
}
|
||||
|
||||
struct RelayStation: Codable {
|
||||
struct MirrorStation: Codable {
|
||||
let id: String?
|
||||
let name: String?
|
||||
let brand: String?
|
||||
@@ -159,24 +107,24 @@ private struct RelayResponse: Codable {
|
||||
let price: Double?
|
||||
let prices: [String: Double]?
|
||||
|
||||
/// Map relay grade keys (E5/E10/DIESEL) to FuelType for ALL fuels the
|
||||
/// Map mirror grade keys (E5/E10/DIESEL) to FuelType for ALL fuels the
|
||||
/// station sells — one fetch populates every fuel tab. Defensive guard:
|
||||
/// any grade outside the 50–500 pence/litre band is dropped, so a relay
|
||||
/// regression (e.g. the band being removed server-side) can't re-poison
|
||||
/// any grade outside the 50–500 pence/litre band is dropped, so a mirror
|
||||
/// regression (e.g. the band being removed upstream) can't re-poison
|
||||
/// the nationwide cheapest reference with 1.3p / 1589p garbage.
|
||||
var allPrices: [FuelType: Double] {
|
||||
var result = FuelPriceProvider.mapGrades(prices ?? [:])
|
||||
// Backwards-compat: relay versions without `prices` still send `price`.
|
||||
// Backwards-compat: mirror versions without `prices` still send `price`.
|
||||
if result.isEmpty, let price, FuelPriceProvider.priceBand.contains(price) {
|
||||
result[.e10] = price
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
let stations: [RelayStation]
|
||||
let stations: [MirrorStation]
|
||||
}
|
||||
|
||||
/// Envelope metadata from the relay response — which data source is live
|
||||
/// Envelope metadata from the mirror response — which data source is live
|
||||
/// (api/csv) and the freshest price-update timestamp the GOV.UK server
|
||||
/// itself reports for the dataset. Displayed in Settings → About.
|
||||
struct RelayMeta: Codable, Equatable {
|
||||
@@ -189,7 +137,7 @@ struct RelayMeta: Codable, Equatable {
|
||||
|
||||
/// Representative stations spread across ENGLAND (one cluster per region) so
|
||||
/// the fallback works anywhere in the country — it is NOT tied to one town.
|
||||
/// Real UK-wide data comes from the relay (full-UK Fuel Finder CSV/API).
|
||||
/// Real UK-wide data comes from the public mirror (full-UK Fuel Finder CSV/API).
|
||||
/// Prices in pence/litre.
|
||||
struct SampleFuelProvider: FuelPriceProviding {
|
||||
func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType, radiusKM: Double?) async throws -> [FuelStation] {
|
||||
@@ -302,7 +250,7 @@ enum FuelProviderError: LocalizedError {
|
||||
case .notImplemented:
|
||||
return "Live Fuel Finder API not wired yet — activate with GOV.UK One Login credentials."
|
||||
case .relayUnavailable:
|
||||
return "FuelBoard Relay unreachable — showing cached/sample data."
|
||||
return "FuelBoard live data unavailable — showing cached/sample data."
|
||||
case .mirrorUnavailable:
|
||||
return "GitHub price mirror unreachable."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user