ROOT CAUSE (proven by A/B test on-device): the small widget's custom
intent (FuelBoardSmallWidgetConfigurationIntent with favouriteFuel +
favourite AppEntity params) fails fresh-widget default-config resolution
at the system level — the whole default config fails, no timeline is
ever requested, makeEntry never runs, widget stays on the skeleton.
A minimal small widget sharing the MEDIUM intent (fuel/sort/distance
only) populated immediately in the same extension on the same device.
Neither removing defaultResult() (4d35b08) nor the
@IntentParameterDependency (8a9e775) helped — the extra entity params
themselves break resolution.
FIX: FuelPriceSmallWidget now uses FuelBoardWidgetConfigurationIntent
exactly like the working test widget. FuelBoardSmallWidgetConfiguration
Intent, FavouriteFuel(+Query), WidgetFavourite(+Query) deleted; the
pinned-favourite picker is gone (approved tradeoff). Favourites sort
still works on both faces via the list branch — the small face renders
the cheapest favourite of the chosen fuel. Beacon tag distinguishes the
small widget in diagnostics; test widget removed, gallery order
restored (small, medium, live activity).
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
Widget distance menu: the Distance picker only makes sense for
Cheapest ordering. It was hidden for Favourites but Closest still
showed it (regressed when the per-widget Distance picker was added).
parameterSummary now nests When clauses: Distance visible for
Cheapest only; Closest and Favourites hide it.
Notifications: three compounding defects kept real geofence alerts
from ever firing while the app was suspended:
1. Always permission was never properly requested. requestPermissions
fired WhenInUse and Always back-to-back; iOS ignores the second
call while the first prompt is pending, leaving the app stuck on
WhenInUse — and region entries are never delivered in the
background. Added locationManagerDidChangeAuthorization to
ProximityMonitor: escalate WhenInUse -> Always, and re-register
geofences on grant (regions registered under WhenInUse-only won't
deliver in the background).
2. The 18-region window was frozen in the background. Re-registration
lived in SwiftUI .onChange(of: locationManager.current), which
never runs while suspended. Added a delegate hook
(LocationManager.onLocationUpdate) fired from didUpdateLocations on
every fix including background significant-change wake-ups; the app
wires it to re-register geofences around the new position.
3. Region-registration failures were invisible. monitoringDidFailFor
was never implemented, so a failed startMonitoring (region budget,
auth, radius) silently stopped alerts. Now surfaced as
monitor.lastRegionError and shown in Settings -> Debug; cleared on
the next successful registration.
1. CarPlay widget taps could never launch Maps — FuelBoard is not a
CarPlay app, and Apple only lets a widget launch its own app in
CarPlay when that app is CarPlay-enabled. Marked the widget as a
disfavored CarPlay location (iOS 26+; no-op below): read-only in
the car, no dead tap. Header comment updated to match reality.
2. Widgets now track the user's location: timeline refresh 15m -> 5m,
and the app's location manager reloads all widget timelines after
every significant move (250m filter, throttled to 60s), so the
widget re-orders around the new position while driving instead of
waiting for the next tick.
3. Debug 'Cheapest in range' no longer shows the ALERT prediction
(alertsFuel + alert radius — a different pool by design). It now
mirrors the app list exactly: the TOP-badge station for the
selected fuel within the stationLimit radius (new DebugAppCheapest
computed in ContentView, passed to Settings). Alert-prediction
cheapest kept as fallback when the app view hasn't resolved.
- 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.
- 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.
- Plain test notification now picks the nearest station selling the
monitored fuel and carries its real name, price, distance and
coordinates (was a static no-station notification)
- addAlertRequest reports whether the map snapshot attached or why it
failed, appended to the Settings result line instead of silently
delivering a notification without the embedded map
- Settings routes both test buttons through the live monitor
The first Settings test button no longer sends a hardcoded placeholder —
it runs the same criteria as a live alert (monitored fuel, alert radius,
current location) against the loaded stations and sends the REAL station
name, brand and price. Result line shown under the button; graceful
message when no stations or no sellers within radius.
- New 'Send plain test notification' button fires with zero criteria checks
(no geofence, no cheapest check, no permission gate) to isolate delivery
and tap behaviour
- ProximityMonitor is now the UNUserNotificationCenter delegate: banners
show even in the foreground (system suppresses them otherwise), and taps
complete the 'open FuelBoard' action
Fires a local notification shaped exactly like a real price alert (uses the
currently configured alerts fuel + radius), with permission-gated handling:
prompts on first use, guides to Settings when denied. Tapping it opens
FuelBoard, same as a real alert.
- 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