Commit Graph
78 Commits
Author SHA1 Message Date
FuelBoard Contributor 01517ba687 onboarding: replay on a fresh/reinstalled install, not every launch
The keychain-first completed flag persists across reinstalls (same bundle id),
so a new install was treated as already-completed and onboarding silently
skipped while permissions still prompted. Add install identity:
- Store an install id in BOTH the app-own container defaults (wiped on
  reinstall, persists across launches) and keychain (survives reinstall).
- isFreshInstall() is true on first-ever launch or when the local id is
  missing/different from keychain (i.e. reinstalled); it seeds a fresh common
  id so the next launch of the same install is not 'fresh' again.
- shouldShowOnboarding() = not completed || fresh install -> onboarding now
  replays once per fresh/reinstall, never on ordinary launches.
Works on free SideStore (no app-group), using the app's own defaults domain
as the same-install signal.
2026-08-20 13:12:53 +01:00
FuelBoard Contributor d73a64023f road distance: trigger at launch + foreground, fall back to saved location
Road-distance routing previously only fired once a fresh GPS fix arrived via
the location hook/onChange. A returning user on a cold launch waited for that
fix, and if onboarding/flag issues kept tracking from starting, distances
stayed straight-line. Now:
- refreshRoadDistancesIfNeeded() uses the last saved location when the live
  fix isn't set yet, so it can run immediately.
- It's called explicitly on launch (already-onboarded branch), on foreground
  activation, and on the existing location paths.
2026-08-20 12:56:16 +01:00
FuelBoard Contributor 64ef260959 widget + live activity: prefer Apple-Maps road distance, computed by the app
The widget/Live Activity showed straight-line haversine distance (0.8 mi)
while Apple Maps routes 1.8 mi. Road routing is too heavy for the widget
execution + ~40-70/day refresh budget, so the APP now computes it:

- New RoadDistanceService (app target): for the nearest 12 stations each
  pass, calls MapKit MKDirections (free, no API key, matches Apple Maps)
  and caches metres keyed by station ID.
- Cache stored in KEYCHAIN (fuelboard.roadDistances) so the widget reads it
  even on free SideStore accounts with no app-group container; only valid
  within 600 m of the location it was built from.
- Throttled: recompute max every 10 min, or when the user moves > 400 m;
  wired into the location-update hook + location onChange.
- Widget face, app station rows + Live Activity show the cached road
  distance, falling back to straight-line when absent.
- 4 new cache tests (105 total).
2026-08-20 11:33:47 +01:00
FuelBoard Contributor 727ef6e58f banner: explicit opacity fade (transitions were unreliable)
SwiftUI's removal transition never actually played despite .id keying — the
tip banner vanished instantly on dismiss. Replace it with explicit opacity/
offset driven by withAnimation in reflectBanner (gated by a .task(id:
activeBanner) so presentation is reliable and prompt): a dismissal now
fades out + slides up over 0.35s, and a tip→network swap crossfades. Uses
the same state-driven pattern that fixed the HermesCall ticker fades.
2026-08-19 11:57:32 +01:00
FuelBoard Contributor 4c222cdf55 banner: key by value so tip fades out on dismiss
The banner sits in a single if-let slot; without an identity key, clearing a
tip that was covering a network/offline banner swapped content in place with
no transition (instantly vanishing). Keying the banner by its value (.id)
makes every change — tip→network or tip→nil — run the shared removal
transition, so the tip slides up and fades out when it auto-dismisses or is
tapped.
2026-08-19 11:10:29 +01:00
FuelBoard Contributor cfd435d12a banner: top-floating; fix offline-trigger cache; onboarding prompt timing
- Banner floats at the very top (over the nav/title area), never pushing the
  content below and staying clear of the list/pill.
- Offline banner now fires on refresh in airplane mode / no data: the mirror
  live chain bypassed the HTTP cache (+10s timeout) so an offline fetch really
  fails instead of silently re-serving a cached pointer+dump as a 'success'
  (which kept dataStatus .live and hid the banner).
- Onboarding permissions fire at the Continue/Allow tap BEFORE advancing, so
  the system prompt appears after the page is read and never covers the next
  page's animation (grant auto-advances).
2026-08-19 10:17:24 +01:00
FuelBoard Contributor 8936643e0e banner: float below nav bar (no glass pass-through) + slide-up dismiss
Position the floating banner just below the nav bar so it no longer overlays the iOS 26 Liquid Glass back/toolbar chrome behind it — tapping retry can no longer also select the control beneath. Banner now slides up + fades on dismiss/timeout (one style for all notifications); full-rect contentShape for a solid tap target.
2026-08-19 08:43:35 +01:00
FuelBoard Contributor ba83f9e1c0 banner: unify network+tip into ONE floating overlay (never layout-shifting)
Extract AppBanner; render offline/connection/tip through the single statusBannerCard chrome as a ZStack top overlay so it floats over content instead of shifting layout. Tip (auto-dismiss ~3.5s) takes precedence over network state.
2026-08-19 08:17:50 +01:00
FuelBoard Contributor 57dffb6c5e tip: show outcome in the shared status banner (auto-dismiss)
Tip purchase results now ride the SAME banner chrome as the network/offline
strip above the tabs instead of a hard-to-see Settings footnote. TipStore is
lifted to ContentView (the banner owner) and shared down to Settings. Each
outcome maps to an icon/tint; auto-dismisses after ~3.5 s; tap to dismiss.
Renderer refactored into a shared statusBannerCard(icon:tint:title:subtitle:
trailingIcon:action:) used by both the network strip (retry) and tips (x).
2026-08-19 07:48:10 +01:00
FuelBoard Contributor 8945190da5 fix: keep live activity in sync with motion 2026-08-18 17:59:03 +01:00
FuelBoard Contributor 2e23dcb9ae ui: keep station brand badges white 2026-08-18 12:56:49 +01:00
FuelBoard Contributor db916cafcb ui: fix fallback station badge visibility 2026-08-18 12:47:49 +01:00
FuelBoard Contributor f1c5923c88 Add favourite price-drop Check now action 2026-08-17 19:59:45 +01:00
FuelBoard Contributor 6f0fc43085 Add cheapest-favourite price-drop alerts 2026-08-17 16:33:46 +01:00
FuelBoard Contributor 63084322f6 Refine trends and debug harness; clean up warnings 2026-08-17 14:06:26 +01:00
FuelBoard Contributor e0e5b36503 Trends: price-history fetch failure with no data raises the connection banner
A history fetch that comes back empty BECAUSE the mirror is unreachable
(pointer probe failed) was only an in-sheet retry card — no global signal.
Now it also raises the red connection banner ('Check your internet
connection / Tap to try again'), so a network problem is visible on every
tab, not just inside the Trends sheet. A successful load clears the
banner (only when the banner is the connection banner — never clobbers
the offline-dump banner).

- TrendsView: onHistoryUnavailable/onHistoryRecovered closures fired from
  load() (loadFailed -> unavailable; hasAnyData -> recovered);
  -forceHistoryFailure QA hook (forces the unreachable state, skips
  auto-refresh like the other force-* hooks)
- FavouritesView: closures threaded through the Trends sheet init
- ContentView: wires them to dataStatus (.live -> .connectionProblem on
  failure; recovered clears only .connectionProblem)
- fuelboard-development skill: hook + wiring documented
2026-08-16 12:31:35 +01:00
FuelBoard Contributor a6dbe8c46e Banners: mention Stations-tab refresh; add connection-problem banner
Fix 1 — the offline banner's subtitle claimed 'pull to refresh' while that
gesture only exists on the Stations tab, yet the banner pins across ALL
tabs. Copy now says where the gesture lives: 'Pull to refresh on the
Stations tab'. The banner itself stays tappable everywhere (tap = retry),
so the retry path is never trapped on one tab.

Fix 2 — fetch-failed-with-cache case showed stale saved prices with NO
feedback (looked like a working live app). New second banner state
'Check your internet connection / Tap to try again' (red wifi.exclamation
mark, tap = retry) now appears whenever the fetch fails but a saved cache
is on screen. It clears on the next successful fetch.

- ContentView: offlineDataDate:String? -> dataStatus enum (.live /
  .offlineDump(date) / .connectionProblem) driving a shared statusBanner
  (icon/tint/title/subtitle); -forceConnectionProblem QA launch hook;
  auto-refresh skipped under either force-* hook
- Localizable.strings: subtitle key updated, +5 new keys (incl. date-less
  'Offline data' fallback + a11y labels)
- Tests unchanged (101 green) — pure view-layer change
2026-08-16 12:14:13 +01:00
FuelBoard Contributor bfeb494c03 Offline banner: label the bundled-dump fallback with its snapshot date
The no-network last resort (bundled real dump) now shows a tappable banner
above the tabs — 'Offline data from 15 Aug / Pull to refresh to update
prices' — so stale snapshot data is never presented as live. Banner date
comes from the dump's own data_updated stamp (FuelStore.offlineDataLabel,
ISO 8601 with/without fractional seconds). Cleared on any successful fetch.

- BundledDumpProvider.dataUpdatedStamp (JSONSerialization, app target)
- ContentView: offlineDataDate state, offlineBanner view (tip-card styling,
  orange wifi.slash + arrow.clockwise, tap = force refresh), refresh()
  clears/sets it, -forceOfflineDump QA launch hook (serves the dump and
  skips auto-refresh so the banner stays up for captures)
- Localizable.strings: 3 keys (banner title/subtitle/a11y)
- +3 tests: OfflineDataLabelTests (101 total)
2026-08-16 11:52:19 +01:00
FuelBoard Contributor 015368faaf Real iPhone wallpaper + populated Favourites captures
- PreviewWallpaper image set: the actual iPhone wallpaper extracted from the
  iOS 26.3 sim runtime (ActionButtonSelector Display.ca, 1320x2870 native
  res, JPEG ~180 KB) — replaces the gradient backdrop in the widget mock
- -seedFavourite <substring> launch hook: pins the first matching station
  for Unleaded via the normal keychain+app-group save path, so the
  Favourites tab can be captured populated (Ring Road Service Station,
  129.9, 59.1 mi, TOP badge)
- Re-captured 2-favourites (light), 6-favourites-dark, 9-widgets-dark,
  10-widgets-light with real data
- 95/95 tests green; Release IPA rebuilt + re-served
2026-08-15 16:49:09 +01:00
FuelBoard Contributor 57121d5213 Widget preview screen + shared widget rendering; dark-mode screenshots with real data
- Move widget entry view into Shared/FuelPriceWidgetViews.swift (FuelPriceWidgetContent,
  FuelPriceWidgetEntryData) so the app can render the REAL widget faces; widget
  extension now wraps it (FuelPriceEntry.data) — one source of truth for the face
- WidgetMockScreen (-widgets launch arg): app icon + small + medium widget previews
  for App Store captures; AppIconPreview image set (1024 drip master)
- AppStoreScreenshots: 5-8 dark-mode tab captures with real Halifax data, 9 widgets
- 95/95 tests green; Release IPA rebuilt + re-served
2026-08-15 16:25:10 +01:00
FuelBoard Contributor b5a49fc68d App Store prep: privacy manifests, 1.0.1 (2), OGL attribution, UK empty state, screenshot harness
- PrivacyInfo.xcprivacy in app + widget targets (UserDefaults 1C8F.1;
  app declares Location linked + Diagnostics not-linked, no tracking)
- Version bump 1.0.1 (2) in App + Widget plists
- Settings → Data source footer: OGL attribution + 'refresh up to twice a day'
- Stations empty state: friendly UK-only explainer + 'Browse all UK stations'
- ContentView launch-arg harness (pattern of -showKeySheet): -skipOnboarding
  and -tab stations|favourites|alerts|settings for screenshot capture
- APPSTORE.md updated (blockers struck, tasks done, yuzu-hub workflow)
- BACKLOG.md hygiene: GitHub-first header, Trends merged note, offline-first-run DONE
2026-08-15 15:50:26 +01:00
FuelBoard Contributor 366e21a1b4 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)
2026-08-15 13:00:24 +01:00
FuelBoard Contributor 6cbf1dfc8f Debug: 100 m fence delivery test + test-button trace logging
Settings → Debug → 'Debug fence (delivery test)': registers a 100 m
CLCircularRegion at the current fix that exists solely to prove iOS
delivers region events on this install. Entering it ticks 'Region events
this session', logs 'debug fence entered' in the Alert trace, and fires a
plain notification — zero alert gates. Cleared via button; survives the
station re-registration loop (update() now skips debug-fence regions);
cleared when alerts are disabled.

The Test alert / Plain test buttons now also write to the Alert trace
('test alert scheduled — …'), so the trace reflects every probe, not just
live geofence entries. 83 tests, Release build green.
2026-08-15 11:46:00 +01:00
FuelBoard Contributor 494412ba99 Debug: live alert trace — surface every gate decision from the alert chain
ProximityMonitor now records each stage the live chain reaches (Settings →
Debug → Alert trace, in-memory, newest-first, capped at 8):
- didEnterRegion arrival (regionEventCount + entry line) — distinguishes
  'geofence never fires' from 'gate blocked the alert'
- gate misses: station not in loaded list, hourly dedup (with minutes ago),
  no fuel sellers within radius after fresh fetch, entered-not-cheapest
  (with the cheaper station + price)
- the previously-silent fresh-fetch catch now logs the error
- 'alert scheduled' once UNUserNotificationCenter.add is called — if that
  line appears but no banner shows, the failure is delivery/permission,
  not alert logic

SettingsView shows the trace (symbol per kind) + region-event count; the
existing Test-alert button already probes notification permission and
reports denial inline. 83 tests, Release build green.
2026-08-15 11:10:10 +01:00
FuelBoard Contributor bfb59d498a Live Activity: price now respects the Price display style — style carried in ContentState (captured at push time, extension renders deterministically instead of re-reading storage), setting change pushes an in-place content update via explicit override (no save-ordering race) 2026-08-14 21:06:04 +01:00
FuelBoard Contributor 80113456ae Price display: station sign drops the £ (134.9) — matches roadside signs; pounds & pence uses forecourt superscript style £1.29⁹/L (small superscript third digit, small /L); attributed Text on list/widgets/Live Activity, Unicode superscript in Siri cards 2026-08-14 18:14:10 +01:00
FuelBoard Contributor fd7fa5f9c9 Settings: price display toggle — station sign (£129.9) vs pounds & pence (£1.299); display-only, all surfaces (list, widgets, Live Activity, Siri cards); Siri dialog stays speech-safe pounds 2026-08-14 15:33:26 +01:00
FuelBoard Contributor fbf8c79feb Favourites: drag-and-drop reorder per fuel; stored order drives single-widget favourite 2026-08-14 10:48:53 +01:00
FuelBoard Contributor aa95d214e8 Alerts: move geofence monitoring status to Settings debug; plain-language footer 2026-08-14 10:34:45 +01:00
FuelBoard Contributor 490c343fd5 Alerts: mile-friendly radius 1/2/3/5/8 + Follow search (capped 8 mi); Live Activity Follow search; legacy radii clamped 2026-08-13 15:00:53 +01:00
FuelBoard Contributor 0d91c82bb1 Localize all app copy via Localizable.strings 2026-08-13 10:57:36 +01:00
FuelBoard Contributor 7f7c0fff03 Alerts tab: picker-style config matching Live Activity; move Live Activity there
- 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
2026-08-12 22:30:54 +01:00
FuelBoard Contributor fcb83f1eed Live Activity: own fuel + distance config, independent of Stations tab
- 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
2026-08-12 20:58:05 +01:00
FuelBoard Contributor 04b233bf9a Live Activity: cheapest-in-radius Lock Screen/Dynamic Island pill
- Shared FuelBoardLiveActivityAttributes/ContentState (app + widget targets)
- FuelBoardLiveActivity Widget: ActivityConfiguration rendered by the widget
  extension - Lock Screen banner + Dynamic Island (leading/trailing/bottom,
  compact, minimal), tap anywhere -> maps:// directions to the best station
- LiveActivityManager (app side): request/update/end; mirrors the app list's
  TOP badge (selected fuel + chosen distance radius, price-then-distance
  ties); local start-date tracking for the 8h cap (this SDK's Activity has no
  startDate) - auto-restarts at 7h59m; silent no-op when disabled/denied
- Wiring: 250m/60s location hook (incl. background significant-change
  wake-ups), refresh completion, fuel/radius/unit changes, toggle flip
- Settings toggle + FuelStore persistence (keychain-backed, app-group)
- App-Info.plist: NSSupportsLiveActivities
2026-08-12 20:11:42 +01:00
FuelBoard Contributor 1dcd47c6cd Fix widget distance menu + geofence notification chain
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.
2026-08-12 18:18:24 +01:00
FuelBoard Contributor f822bd2636 Live-test fixes: CarPlay disfavor, widget location refresh, debug cheapest parity
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.
2026-08-12 18:01:29 +01:00
FuelBoard Contributor fccd0c07ef 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.
2026-08-12 16:23:41 +01:00
FuelBoard Contributor 126fdbde62 Settings Debug: device coordinates + in-range station indicator
- 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.
2026-08-12 14:33:52 +01:00
FuelBoard Contributor 5246f41c48 Widget Favourites filter: pinned stations cheapest-first, no radius
- New Sort by value 'Favourites' in Edit Widget (per-widget config).
- ParameterSummary hides the Distance picker when Favourites is selected
  (favourites are not radius-bound).
- Favourites mode reads keychain favourites (shared access group works on
  SideStore free even without App Groups), filters to the widget's fuel,
  refreshes prices from a focused relay fetch when in range, and ranks
  cheapest-first with distance tiebreak. Not radius-filtered.
- App now re-saves refreshed favourites to keychain after every fetch so
  the widget shows current prices instead of star-time snapshots.
- Empty state + headings adapt to favourites mode. 35 tests pass.
2026-08-12 13:45:42 +01:00
FuelBoard Contributor 9ce0c83d66 Widget opens Maps via maps://; onboarding prompts after Continue; forced first fetch
- 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.
2026-08-12 12:41:19 +01:00
FuelBoard Contributor 20589f41fe Onboarding: prompt permissions in context + data page
Location and network fetches no longer fire at launch before onboarding
reaches the relevant step: foreground tracking and the first refresh are
gated behind onboarding completion (scenePhase no longer starts tracking
while the cover is up, and onAppear defers its fetch until after
finish). A new Data page sits just before 'You're all set': it probes
the FuelBoard Relay, which surfaces the iOS Local Network permission
prompt in context (NSLocalNetworkUsageDescription added), verifies
connectivity, and offers Open Settings/Check again on denial. Onboarding
is now 5 pages: Welcome, Location, Notifications, Data, Done. 35 tests
pass.
2026-08-12 11:59:07 +01:00
FuelBoard Contributor 9032c4a551 Both test buttons carry real station coords; map status surfaced
- 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
2026-08-12 10:06:48 +01:00
FuelBoard Contributor a964a45d49 Notification tap opens directions to the station
- Alerts (live + real-data test) carry the station name + coordinates in
  userInfo and attach a map snapshot with a red pin, so the expanded
  notification shows where the station is
- Tapping a notification opens Apple Maps driving directions to the
  station from the user's current location (maps:// daddr)
- If Apple Maps hand-off fails (LiveContainer), an in-app map sheet
  (StationMapView) pins the station + user location with a Directions
  button
2026-08-12 09:57:42 +01:00
FuelBoard Contributor 950844878c Test alert uses real data: actual cheapest station within radius
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.
2026-08-12 09:34:17 +01:00
FuelBoard Contributor d6550409df Settings: test alert notification button
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.
2026-08-12 09:15:04 +01:00
FuelBoard Contributor 46d5d45c93 Alerts choose their own fuel, independent of the Stations tab
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)
2026-08-12 08:51:31 +01:00
FuelBoard Contributor 35e0adaf0b Favourites are fuel-scoped: starring pins the station for one fuel only
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)
2026-08-12 08:31:08 +01:00
FuelBoard Contributor 44c6c38536 Add Settings tab: distance units toggle, onboarding replay, £4.99 tip; drop E10/E5 from labels
- 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
2026-08-12 07:46:08 +01:00
FuelBoard Contributor fb79cf0788 Add first-launch onboarding with location + notification permission prompts
- OnboardingView: 4-page flow (welcome + features, location, notifications, done)
- Triggers system prompts in-context via OnboardingPermissionPrompter
- Shown at initial launch only (FuelStore.onboardingCompleted flag, app-group defaults)
- Test hook: 'Show onboarding (testing)' button at bottom of Alerts tab
2026-08-12 07:10:54 +01:00
FuelBoard Contributor 7be98c51ad Closest mode: TOP + deltas vs the CHEAPEST within radius (not the nearest); list stays nearest-first 2026-08-11 20:16:32 +01:00