P0: live provider chain — GitHub mirror primary, relay fallback, bundled dump last resort + app telemetry beacon

- MirrorFuelProvider: fetches CURRENT prices from raw.githubusercontent
  (latest.json pointer → history/<day>.json full dump), app-group day-cache
  so the ~2.8 MB dump is re-downloaded only when the mirror pushes a new day
- LiveChainProvider: full path (app refresh) GitHub → relay; focused path
  (background alert checks) relay-first so alerts never pull the full dump
  over mobile data; records which leg served for About + telemetry
- FuelBeacon: fire-and-forget X-Client app ping to the relay's existing
  widget-diag route (zero relay changes) — attribution + cadence on-LAN,
  silent skip off-LAN is the reachability datum; /stats app-hit spike =
  GitHub path failing
- Bundled dump: FuelBoardDump dataset (real 2026-08-15 snapshot, 8,022
  stations) + BundledDumpProvider + scripts/refresh_bundled_dump.sh —
  no-network last resort replaces the demo sample set
- ContentView: About meta from the chain; catch falls back cached → bundled
  → sample
- 89 tests (6 new: chain order x4, cache decision, beacon URL); Release
  build green; beacon route verified against the live relay (204 + logged)
This commit is contained in:
FuelBoard Contributor
2026-08-15 13:00:24 +01:00
parent 54aca14fd4
commit 366e21a1b4
9 changed files with 368 additions and 8 deletions
@@ -0,0 +1,12 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"data" : [
{
"filename" : "fuelboard_dump.json",
"idiom" : "universal"
}
]
}
File diff suppressed because one or more lines are too long
+18
View File
@@ -0,0 +1,18 @@
// BundledDumpProvider.swift no-network last resort (P0 live chain).
//
// A REAL snapshot of the mirror dump is bundled into the app (FuelBoardDump
// dataset, refreshed before builds by scripts/refresh_bundled_dump.sh) so the
// app never demos empty and App Review always sees real data even fully
// offline. Same relay envelope, so the shared decode applies unchanged.
import UIKit
enum BundledDumpProvider {
/// The bundled real snapshot, decoded with the shared relay decode
/// nil only if the asset is missing or corrupt (callers then fall back
/// to the England-wide sample set).
static var stations: [FuelStation]? {
guard let asset = NSDataAsset(name: "FuelBoardDump") else { return nil }
return try? FuelPriceProvider.decodeStations(from: asset.data)
}
}
+12 -5
View File
@@ -454,9 +454,10 @@ struct ContentView: View {
stations = fetched
FuelStore.saveStations(fetched)
FuelStore.saveLastRefresh()
// Persist relay envelope metadata (source, station count, GOV.UK
// dataset update time) for the Settings About section.
if let meta = RelayFuelProvider.latestMeta {
// Persist envelope metadata (source, station count, GOV.UK
// dataset update time) for the Settings About section the
// live chain records whichever leg served the fetch.
if let meta = LiveChainProvider.latestMeta {
FuelStore.saveRelayMeta(meta)
}
// Keep the keychain favourites fresh with the new prices the
@@ -467,8 +468,14 @@ struct ContentView: View {
WidgetCenter.shared.reloadAllTimelines()
statusMessage = "Loaded \(fetched.count) stations · \(Date().formatted(date: .omitted, time: .shortened))"
} catch {
statusMessage = "Live fetch failed: \(error.localizedDescription). Showing cached/sample data."
stations = FuelStore.loadStations().isEmpty ? SampleFuelProvider.sampleStations : FuelStore.loadStations()
statusMessage = "Live fetch failed: \(error.localizedDescription). Showing cached data."
if FuelStore.loadStations().isEmpty {
// No cached prices last resort is the bundled REAL dump
// (stale but genuine), then the demo sample set.
stations = BundledDumpProvider.stations ?? SampleFuelProvider.sampleStations
} else {
stations = FuelStore.loadStations()
}
}
// Keep monitor geofences in sync with the freshest data.
monitor.update(stations: stations, favourites: refreshedFavourites,