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
+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,