Settings About: data source section (connection, station count, GOV.UK update time)

- Relay envelope now carries source (api|csv), stations_count, and
  data_updated (freshest price_last_updated the GOV.UK server reports,
  independent of relay sync time).
- App decodes the new envelope metadata (RelayMeta), persists it after
  each full fetch, and shows it in Settings → About → Data source:
  Connection (API/CSV), Stations count, Data updated (local-formatted).
- Older relays without the fields decode cleanly (nil meta → em-dash).
- 2 new tests for meta decode + absent-meta tolerance; 37/37 pass.
This commit is contained in:
FuelBoard Contributor
2026-08-12 16:23:41 +01:00
parent a5a95cbb03
commit fccd0c07ef
5 changed files with 162 additions and 1 deletions
+44 -1
View File
@@ -41,6 +41,21 @@ enum FuelPriceProvider {
)
}
}
/// Decodes the relay 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.
static func decodeRelayMeta(from data: Data) -> RelayMeta? {
guard let payload = try? JSONDecoder().decode(RelayResponse.self, from: data) else {
return nil
}
return RelayMeta(
source: payload.source,
stationCount: payload.stationsCount,
dataUpdated: payload.dataUpdated
)
}
}
// MARK: - Relay provider (default)
@@ -51,6 +66,11 @@ enum FuelPriceProvider {
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?
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)]
@@ -73,11 +93,25 @@ struct RelayFuelProvider: FuelPriceProviding {
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
throw FuelProviderError.relayUnavailable
}
return try FuelPriceProvider.decodeStations(from: data)
let stations = try FuelPriceProvider.decodeStations(from: data)
Self.latestMeta = FuelPriceProvider.decodeRelayMeta(from: data)
return stations
}
}
private struct RelayResponse: 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.
let source: String?
let stationsCount: Int?
let dataUpdated: String?
enum CodingKeys: String, CodingKey {
case source, stations
case stationsCount = "stations_count"
case dataUpdated = "data_updated"
}
struct RelayStation: Codable {
let id: String?
let name: String?
@@ -117,6 +151,15 @@ private struct RelayResponse: Codable {
let stations: [RelayStation]
}
/// Envelope metadata from the relay 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 {
let source: String?
let stationCount: Int?
let dataUpdated: String?
}
// MARK: - Sample provider (offline fallback only)
/// Representative stations spread across ENGLAND (one cluster per region) so
+31
View File
@@ -264,6 +264,9 @@ struct FuelStore {
static let debugModeKey = "fuelboard.debugMode" // Bool hidden dev flag
static let onboardingCompletedKey = "fuelboard.onboardingCompleted" // Bool
static let lastRefreshKey = "fuelboard.lastRefresh" // TimeInterval (seconds since 1970)
static let relaySourceKey = "fuelboard.relaySource" // String "api" | "csv"
static let stationCountKey = "fuelboard.stationCount" // String station count
static let dataUpdatedKey = "fuelboard.dataUpdated" // String govUK dataset update time
// MARK: Stations
// The full-UK dataset (~2.9 MB) lives in app-group UserDefaults only
@@ -503,6 +506,34 @@ struct FuelStore {
saveString(String(date.timeIntervalSince1970), service: lastRefreshKey)
}
// MARK: Relay metadata shown in Settings About. Written after each
// successful full fetch so the About section reflects the live source.
static func saveRelayMeta(_ meta: RelayMeta) {
if let source = meta.source {
saveString(source, service: relaySourceKey)
}
if let count = meta.stationCount {
saveString(String(count), service: stationCountKey)
}
if let updated = meta.dataUpdated {
saveString(updated, service: dataUpdatedKey)
}
}
static func loadRelaySource() -> String? {
loadString(service: relaySourceKey)
}
static func loadStationCount() -> Int? {
guard let raw = loadString(service: stationCountKey) else { return nil }
return Int(raw)
}
static func loadDataUpdated() -> String? {
loadString(service: dataUpdatedKey)
}
/// True when the cached data is fresh enough that a scheduled auto-refresh
/// should be skipped (twice-a-day policy).
static var isCacheFresh: Bool {