- 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)
- Shared/FuelHistory.swift: GitHub mirror read path (latest.json pointer +
history/YYYY-MM-DD.json day files); day math (UTC noon keys), slim day decode
with the shared price band, series + deltaSeries (vs-cheapest rebase),
favourites-only app-group cache (90-day prune), parallel per-day fetches,
404/network = graph gap never error.
- FuelPriceProvider: shared priceBand + mapGrades (single source for live +
history decoding).
- FuelBoard/TrendsView.swift: line chart in the Favourites tab via toolbar
button + sheet; fuel capsule (fuels with favourites only), 7/30/90 range,
Price/vs-cheapest toggle, per-station legend, price-display style on y-axis,
empty/loading/retry states with honest copy; no widget in v1.
- Localizable.strings: Trends keys.
- 83 tests (13 history + URL regression): day math, band guard, series gaps,
delta rebasing, prune, and the appendingPathComponent regression (relative
URL resolution dropped /main — all fetches 404'd).
- Sim-verified: favourites rows + TOP/deltas; sheet controls + building-up
state with live first-snapshot date (archive has 1 day; lines render once
>=2 snapshots).
- Backlog: P1 Trends DONE (unmerged); P0 REMAINING = live provider chain +
telemetry beacon.
The single-slot beacon couldn't distinguish small vs medium (last writer
wins). Beacon keychain service now embeds the intent type name so small
and medium never overwrite each other; the relay GET now carries the
intent type + entry state as query params so the access log shows exactly
which widget kind ran makeEntry, when, and with what. Diagnostics screen
shows SMALL and MEDIUM beacons side by side — a missing SMALL beacon
after adding the small widget proves its timeline never reaches the
provider (system-level config resolution failure).
The small-widget skeleton survived four resolution fixes; instrument the
widget instead of guessing. Every makeEntry now writes its entry state
(intent type, location source, station count, fuel, sort, favourites flag,
first station) to a keychain beacon (app-readable) and fire-and-forgets a
GET to the relay's /api/v1/widget-diag so its access log records whether a
widget timeline actually runs in the extension and what it produced.
Settings → Debug gains a Widget Diagnostics section: the keychain beacon +
the installed-widget inventory from WidgetCenter. Relay gains the
/api/v1/widget-diag route (204, log-only).
- Alerts section restyled to mirror the Live Activity section: one section
with toggle + Fuel picker + Radius picker (was segmented fuel + slider)
- Alert radius options expanded to 1/2/3/5/8/10/15/20 (display unit, snapped
to nearest option; stored km unchanged; validation widened to 33 km)
- Live Activity section moved from Settings into the Alerts tab so both
'notify while driving' surfaces sit together
- SettingsView loses the Live Activity bindings/section
- Settings → Live Activity now has Fuel and Distance pickers (5/10/15,
unit-aware) stored under fuelboard.liveActivityFuel / liveActivityRadiusMiles
- LiveActivityManager uses these instead of the Stations-tab selectedFuel /
stationLimit; Stations changes no longer re-target the pill
- Fuel moved from Activity attributes into ContentState so changing fuel in
Settings updates the RUNNING activity in place (no end+restart needed)
- Stations tab stays a pure data-interrogation + favourites surface
- 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.
The API relay (fuelboard-relay-api, official Fuel Finder source) passed
contract verification and live health (source=api, 8010 stations).
Switching the app's single relay URL to :8789; original CSV relay stays
running on :8788 if we need to roll back.
- Debug section now shows the last device fix (lat/lng to 5dp) and its age.
- In-range indicator uses the SAME criteria as live alerts: stations selling
the monitored fuel within the alert radius of the fix. Green dot + count
when in range, red when none, grey while waiting for a fix.
- Shows the cheapest in-range station (name, distance, price).
- ProximityMonitor recomputes the snapshot on every location/stations
change and on Debug-section appear; ContentView passes the live status.
- SettingsView extracted to its own property to keep TabView body within
the compiler type-check budget. 35 tests pass.
- Test-alert buttons moved out of the Alerts section into a Debug section.
- Debug section only renders when the debug flag is on; completely hidden
otherwise (no user-facing toggle).
- Hidden toggle: tap Settings → About → Version five times to flip the
flag (keychain + app-group storage, survives reinstall).
- About section added with version/build number.
- 35 tests pass.
- The widget now uses the native maps://?daddr= scheme for taps in both
sizes. In CarPlay there is no containing app to relay through, but since
Apple Maps IS a CarPlay app, the system may route the tap straight to
Maps on the car display (WWDC25: widgets can launch apps in CarPlay).
- iOS keeps working via onOpenURL: the app now forwards maps:// URLs (if
the Home Screen delivers them to the app), plus legacy fuelboard:// and
http://maps.apple.com links from older cached timelines.
- No public API exists for a widget to detect CarPlay context, so the
single maps:// link serves both; the app-side forward is the iOS safety
net. 35 tests pass.
- WidgetKit widgets can only open their containing app — the Link URL is
delivered to FuelBoard via onOpenURL, never opened directly in Maps. The
widget now links to a registered fuelboard://directions?lat=&lng= URL,
and FuelBoardApp forwards it to native maps://?daddr= directions.
(http://maps.apple.com and raw maps:// both silently dropped in-app.)
- Widget rows (small + medium) now show the full station name (station.name)
instead of just the brand.
- Onboarding network page: removed the 'Check again' secondary screen after
the Local Network prompt — Open Settings remains the only retry path.
35 tests pass.
- Widget tap-to-directions now uses the native maps:// scheme instead of
http://maps.apple.com. The universal-link http URL failed to open from a
widget and fell back to launching the containing app; maps:// opens
Apple Maps directly for both small (whole widget) and medium (per row).
- Onboarding: removed the Skip button (mandatory flow), and system prompts
(location, notifications, local network) now fire only when the user taps
Continue/Allow on each page — not when the page appears. Pages
auto-advance once a permission is granted.
- After 'Start Using FuelBoard' the first data fetch is forced, so a
reinstall that leaves a recent lastRefresh in app-group defaults can no
longer skip the fetch and leave the station list empty. 35 tests pass.
When several stations share the cheapest price within the radius (within
0.01p), the alert registers a dynamic UNNotificationCategory with up to
4 action buttons (brand + distance, nearest first) and taps open
directions to the chosen station. Shared FuelStore.tiedStations helper
used by both the live geofence path and the Settings real-data test;
35 tests pass.
The proximity monitor was tied to the Stations-tab fuel selection, so
switching the list to another fuel silently re-targeted geofences. Now the
Alerts tab has its own 'Fuel to monitor' picker (persisted as
fuelboard.alertsFuel):
- Monitor reads loadAlertsFuel() on init/background relaunch
- Changing the alerts fuel re-registers geofences around stations selling
that fuel only
- Favourites priority slots stay scoped to the monitored fuel
- Tests: alerts fuel round-trip + default (33/33 passing)
Fixing the bug where favouriting an Unleaded entry also created a Diesel
favourite. Favourites are now [FavouriteEntry] = (station, fuel) pairs:
- Starring a row pins it only for the fuel being viewed
- Favourites tabs show only fuels that actually have favourites
- Geofence monitor gives priority slots only to favourites matching the
monitored fuel
- Tests updated + new fuel-scoping test (31/31 passing)
- FuelType labels: 'Unleaded (E10)'/'Premium (E5)' -> 'Unleaded'/'Premium'
- New DistanceUnit (miles/km) preference; all app + widget distances,
radii and alert text convert/display in the chosen unit
- Settings tab (4th tab): segmented miles/km picker, 'Show introduction'
replay button (onboarding moved out of Alerts tab), StoreKit tip £4.99
- ProximityMonitor + widget notifications use the chosen unit
- Tests: label expectations updated, DistanceUnit conversion + format tests