Commit Graph
125 Commits
Author SHA1 Message Date
FuelBoard Contributor 050c9f13a3 Onboarding: show the app icon with iOS rounded corners
The welcome page's green-gradient circle + SF symbol is replaced by the
app icon itself (AppIconArtwork imageset mirroring AppIcon.png), clipped
to the standard iOS rounded-rect corner radius (21pt at 96pt — ~22%),
so onboarding matches the Home Screen icon.
2026-08-13 23:41:16 +01:00
FuelBoard Contributor fec139cf5a Icon: replace with Fuelboard-copy composer render (Vecteezy gas-station art)
New Icon Composer project artwork (gas-station vector, layered + background)
rendered via ictool at 1024pt: Default / Dark / TintedLight renditions
replacing all three asset-catalog slots.
2026-08-13 23:36:57 +01:00
FuelBoard Contributor 018e6eec2f Icon: use the Icon Composer render instead of the pre-baked appiconset PNG
The export zip contained both a pre-rendered appiconset AND the
Fuelboard.icon composer project; the pre-baked PNGs were a different
(flatter) render. Re-rendered the .icon bundle with ictool (Xcode 26
Icon Composer executable) — Default/Dark/TintedLight renditions at
1024pt — and replaced the three asset-catalog slots.
2026-08-13 22:00:14 +01:00
FuelBoard Contributor 3545239ad6 New app icon: green pump showing £1.00, with dark + tinted variants
Replaces the single 1024px icon with the three-variant set (universal,
dark-luminosity, tinted) generated for FuelBoard; Contents.json updated to
the iOS-only slots.
2026-08-13 21:37:36 +01:00
FuelBoard Contributor cd88e23601 Widget pump icon flexes to the chosen fuel's colour
The pump glyph in the widget header (both the small single-station face and
the medium list face) was hardcoded green; it now uses the app's fuel
colour wheel — green #30D158 unleaded, yellow #FFD60A premium, cyan
#64D2FF diesel — mirroring the per-fuel pumps in the app's segmented
picker. Palette duplicated in the widget target (Shared is Foundation-only
by design); cross-referenced to StationsView.swift's FuelType.tintColor.
2026-08-13 20:53:20 +01:00
FuelBoard Contributor dafc96f845 Fix small widget skeleton: use the proven medium intent; delete failing small intent
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).
2026-08-13 20:43:15 +01:00
FuelBoard Contributor 553730681f A/B test: from-scratch minimal small widget (proven medium intent)
Temporary FuelBoardTestSmallWidget kind: a small-face widget driven by
the PROVEN medium intent (fuel/sort/distance only — no favourite params,
no dependency, no custom entity query), registered first in the gallery.
Isolates small SIZE vs small INTENT: if it populates while
FuelPriceSmallWidget skeletons, the small intent's extra machinery is the
culprit; if it also skeletons, the failure is small-family/system-level.
Provider gains a beaconTag so the test widget's beacons are
distinguishable in the relay log and keychain slots. Removed once the
root cause is confirmed.
2026-08-13 20:27:03 +01:00
FuelBoard Contributor 8a9e77546f Small widget: remove @IntentParameterDependency from WidgetFavouriteQuery
Final structural difference from the working medium widget. The
dependent-picker machinery resolves the favourite parameter through the
hidden favouriteFuel parameter during fresh-widget default-config
resolution (default Sort = Cheapest hides favouriteFuel), and that
resolution fails on-device — the whole default config fails and the
small widget strands on the skeleton. Favourite picker now lists ALL
favourites, each row labelled with its fuel (the fuel-scoped filter was
the dependency's only purpose). Configured Favourites-mode widgets are
unaffected. Per-intent beacons remain in place to verify the outcome.
2026-08-13 20:23:12 +01:00
FuelBoard Contributor 3cf1d5c9af Widget diagnostics: per-intent beacons (keychain slots + relay query)
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).
2026-08-13 20:13:24 +01:00
FuelBoard Contributor 335a9751dc Widget diagnostics: beacon from makeEntry (keychain + relay) + app diagnostics section
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).
2026-08-13 19:52:12 +01:00
FuelBoard Contributor 4d35b08533 Widget: keychain-free fresh defaults — drop defaultResult(), auto-populate at entry time
The small-widget skeleton (fresh widget stranded on the placeholder while
the medium populated in the gallery) traced to the small intent's default
configuration resolution: FavouriteFuelQuery/WidgetFavouriteQuery
defaultResult() read keychain in the gallery-preview/fresh-add path,
failing the whole default config and leaving no timeline. Configured
widgets bypassed defaultResult (stored values → lenient entities) — the
exact favourites-works/cheapest-closest-doesn't split.

- Remove defaultResult() from both queries: fresh configs now resolve
  from the static @Parameter(default:) values through lenient
  entities(for:) with ZERO storage reads.
- WidgetFavouriteQuery.entities(for:) skips the keychain name lookup for
  id-only (empty station) values.
- Auto-populate moves to makeEntry: empty pinned favourite → first
  favourite of the fuel (same rendered result as the old sheet default).
- Sheet keeps the dependent picker via suggestedEntities() + dependency;
  fresh widgets show static defaults until picked (approved tradeoff).
2026-08-13 19:35:46 +01:00
FuelBoard Contributor 8646521176 Small widget: fuel-only relay fetch when no location, so default/Cheapest/Closest faces populate with real stations instead of sample data. Root cause of the persistent skeleton: favourites mode reads keychain (works on free accounts), but the non-favourites path needs stations — the app-group cache can be absent on free accounts and keychain can't hold the station dump, so with no location fix the widget fell through to SampleFuelProvider and never populated. Now when location is nil the widget fetches /api/v1/stations?fuel=X&limit=500 (fuel-only, cheapest-first, verified against the relay). 2026-08-13 18:51:41 +01:00
FuelBoard Contributor 38b47eea38 Widget gallery order + default-widget skeleton fix. (1) Register FuelPriceSmallWidget BEFORE FuelPriceWidget so the gallery lists small, then medium (bundle order = gallery order). (2) Revert entity resolution to LENIENT: the timeline request resolves the full configuration through the entity queries, and the b1765b5 strict entities(for:) (nil for the empty default favourite 'fuel|') failed resolution for default-config small widgets, stranding them on the iOS skeleton placeholder. entities(for:) now always returns a value; defaultResult() still auto-populates fresh widgets to the first fuel with a favourite / first favourite of that fuel. 2026-08-13 18:39:30 +01:00
FuelBoard Contributor b1765b5611 Widget favourites defaults: auto-populate Fuel and Favourite rows when Sort = Favourites. FavouriteFuelQuery gains defaultResult() (first fuel with a favourite, Unleaded→Diesel order) and strict entities(for:) — a stored fuel that no longer has favourites fails resolution and falls back to the default. WidgetFavouriteQuery gains defaultResult() (first favourite of the Fuel row's fuel, else first favourite overall) and strict entities(for:) — the empty static default ('fuel|') and removed favourites now resolve to nil so the sheet pre-selects a real favourite instead of showing a placeholder. Provider fallback (cheapest favourite of the fuel) unchanged. 2026-08-13 18:28:23 +01:00
FuelBoard Contributor 906f8a8429 Small widget: declare config params in display order for every summary branch. If iOS falls back to declaration order when re-rendering the sheet after a parameter change, the favourites branch previously re-ordered to Sort by first (sort was declared before favouriteFuel). Now favouriteFuel, fuel, sort, favourite, distance — Favourites mode reads Fuel · Sort by · Favourite under either ordering mechanism. No encoding change (config keys are identifiers, order-independent). 2026-08-13 18:21:03 +01:00
FuelBoard Contributor 926317236a Widget config sheets: unify row layout across both widgets — Fuel type first, then Sort by, then Favourite (Favourites sort only). Small widget's Favourites branch reordered from Sort/Fuel/Favourite to Fuel/Sort/Favourite, and the favourites-scoped fuel param retitled 'Fuel' so the row reads identically in every mode. Medium widget was already Fuel-first. No parameter type changes, so no stored-config reset beyond the normal reinstall. 2026-08-13 18:02:53 +01:00
FuelBoard Contributor 3e2d3775f2 Widget config sheet: revert WidgetSort to String-backed AppEnum. parameterSummary When(\.$sort, .equalTo, …) silently falls back to the otherwise branch for AppEntity parameters (iOS 17+ bug FB13263902), which hid the Favourite/Favourite Fuel rows on the small widget — the metadata compared against a literal 'WidgetSort.favourites' string instead of the enum case. String AppEnums match reliably (the d8506a0 form). Tradeoff: the Sort picker always offers Favourites; picking it with no favourites shows the empty state. FavouriteFuel entity + IntentParameterDependency filtering unchanged. 2026-08-13 17:48:33 +01:00
FuelBoard Contributor d0bd4e7a8e Small widget favourites config: fuel dictates the favourite list (@IntentParameterDependency filters picker options by the chosen fuel), fuel picker shows only fuels with favourites (FavouriteFuel entity), Favourites sort hidden when no favourites exist (WidgetSort → dynamic AppEntity), fuel tags removed from favourite rows 2026-08-13 17:14:14 +01:00
FuelBoard Contributor d8506a035f Small widget favourite picker: embed station name + multi-fuel flag in the WidgetFavourite entity so picker rows render with no storage read (config-sheet resolution can run without keychain/app-group access, which made every row show the 'Favourite' placeholder) 2026-08-13 17:00:17 +01:00
FuelBoard Contributor 7357eb98c3 Small widget favourites: fuel row returns to the config sheet (picks the default favourite); pinned favourite wins, fuel-tagged picker rows when favourites span >1 fuel; fallback = selected fuel's cheapest favourite 2026-08-13 16:51:21 +01:00
FuelBoard Contributor 1bdef59d78 Small widget: Favourites gains a picker to pin which favourite to show (separate FuelPriceWidgetSmall kind + AppEntity picker; medium keeps the list, no picker). Backlog: dual-unit widget picker labels 2026-08-13 15:49:18 +01:00
FuelBoard Contributor 26e724f48a Widget config: Distance picker as AppEntity so labels resolve dynamically from the app's units (AppEnum labels are static) 2026-08-13 15:33:24 +01:00
FuelBoard Contributor 64330f96a8 Widget config: Distance picker mirrors the app's units setting (5 mi = 8 km) via dynamic AppEnum displayRepresentation 2026-08-13 15:25:20 +01:00
FuelBoard Contributor 2409b71f2f Labels: pluralize miles (1 mile) and show true distances in the chosen unit (5 mi = 8 km) across Alerts/Stations pickers + captions 2026-08-13 15:10:23 +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 bb48995fac Onboarding copy: tighten values (daily refresh, widget line, alert line) 2026-08-13 14:22:12 +01:00
FuelBoard Contributor 61bb61d802 Backlog: add GitHub production-data mirror (P3); mark API switch, unit tests, backoff done 2026-08-13 14:04:58 +01:00
FuelBoard Contributor 2d070fa3ba Fuel colours: align with wheel palette — Unleaded #30D158, Premium #FFD60A, Diesel #64D2FF 2026-08-13 13:09:22 +01:00
FuelBoard Contributor 3fe2a7f03c Onboarding copy: drop premium fuel mention, 'updated daily' 2026-08-13 13:02:50 +01:00
FuelBoard Contributor 0d91c82bb1 Localize all app copy via Localizable.strings 2026-08-13 10:57:36 +01:00
FuelBoard Contributor bbe43a6149 Onboarding: Local Network confirmation matches other pages
Granted label now reads just 'Connected' with the green tick and uses
the same .subheadline confirmation styling as the Location and
Notifications pages.
2026-08-13 10:35:18 +01:00
FuelBoard Contributor a1fa0f9998 Onboarding: silent Local Network pre-flight on replay
Replaying onboarding showed a visible probe on the 4th page because
iOS exposes no status API for the Local Network permission — the
connection attempt IS the only detector. Location and Notifications
query their status at init, so their ticks are already known when the
page appears; Local Network could only be known by probing.

Now, when onboarding is re-opened (already completed), a pre-flight
probe runs silently at open: granted connects in ms, denied reports
.waiting/.localNetworkDenied immediately, and a plain .waiting
(permission undetermined after a reinstall) cancels so page 4's
Continue still fires the prompt in context. By the time the data page
appears its tick (or denied text) is already on screen, matching the
other permission pages. First run is unchanged.
2026-08-13 10:29:20 +01:00
FuelBoard Contributor ed243fae9e Onboarding: show green tick on Local Network page before advancing
The probe result landed while the Local Network prompt was still
dismissing, so the granted tick was skipped and the page jumped
straight to the final slide. Hold 'Connected — prices will load'
on screen for a beat before auto-advancing, matching the visible
tick flow of the Location and Notifications pages.
2026-08-13 10:11:57 +01:00
FuelBoard Contributor cb174f66fa Settings About: drop redundant 'FuelBoard x.y(z)' footer
Version already shown in the Version row; the footer just duplicated it.
2026-08-13 10:06:51 +01:00
FuelBoard Contributor c2a9943d4c Favourites tab: fuel-type selector gets the same coloured pump icons
- FuelTypeSegmentedPicker promoted to shared (was private in StationsView)
  and parameterised by fuel list so Favourites passes only fuels with favourites
- Favourites tab swaps native .segmented picker for the capsule control with
  green/purple/grey fuelpump icons, matching Stations
2026-08-12 23:03:35 +01:00
FuelBoard Contributor 0fef9870cb Favourites tab: remove the crown 'cheapest favourite' banner
The gold-crown header row is redundant with the list itself (rows are already
cheapest-first with a TOP badge on the first). The bottom caption about
alerts monitoring stays.
2026-08-12 23:00:16 +01:00
FuelBoard Contributor d864319593 Fuel-type segmented control: coloured pump icons per fuel
- Custom capsule segmented control (native .segmented can't tint per-segment)
- fuelpump.fill icon per segment: green=Unleaded, purple=Premium, grey=Diesel
- FuelType.tintColor: Premium blue -> purple (also updates the nav title icon)
- Selected segment keeps the raised-capsule look; save+widget reload unchanged
2026-08-12 22:52:03 +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 a721ccd187 Raise deployment target to iOS 26.0; enable CarPlay Live Activity
- IPHONEOS_DEPLOYMENT_TARGET 17.0 -> 26.0 (iPhone 11+, Apple's current floor)
- Widget CarPlay disfavor is now unconditional (no #available guard)
- Live Activity gains supplementalActivityFamilies([.small]) so it renders
  in the car's small slot on CarPlay Ultra (display-only: FuelBoard has no
  CarPlay entitlement, car taps can't launch anything)
2026-08-12 21:13:24 +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 d37aab04f1 Widget: family-aware row cap — large/extraLarge show 5 stations, medium 3
The 3-row cap was hardcoded for the medium widget height; large widgets
have ~2x the height and fit 5 compact rows. Timeline now carries up to
8 stations (was 4) so the large list never starves. Distance picker
still controls which stations are in range; the visible row count now
depends on widget size, not the distance setting.
2026-08-12 16:35:42 +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 a5a95cbb03 Point relay baseURL at API relay :8789 for live test
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.
2026-08-12 16:04:51 +01:00
FuelBoard Contributor 3988dea30c Onboarding: network page no longer loads data — bare TCP connect
- The Data page's probe was an HTTP GET /api/v1/stations (8s timeout)
  that read as 'loading data' and could stall the slide. Replaced with a
  bare TCP connect to the relay host:port — just enough to trigger the
  iOS Local Network permission prompt, zero payload transferred.
- Grant -> auto-advance to the final slide (existing onChange); denial or
  unreachable relay -> Continue re-enables and proceeds. 12s safety
  timeout so the page can never dead-end on a spinner.
- NWConnection held on the prompter so the connect survives until the
  prompt is answered. Status copy: 'Waiting for network permission…'.
  Full dataset still downloads AFTER onboarding via forced refresh.
  35 tests pass.
2026-08-12 15:19:00 +01:00
FuelBoard Contributor 55ab4537b5 Onboarding: Data page never dead-ends on Open Settings
- The Prices-ready page button stays 'Continue': tapping it fires the
  Local Network prompt + relay probe; on grant the page auto-advances to
  the final slide (existing onChange), on denial it still advances.
- Removed the Open Settings branch from the Data page entirely (kept for
  Location page, where the user can still be sent to Settings).
- Denied caption no longer suggests opening Settings. 35 tests pass.
2026-08-12 15:02:16 +01:00
FuelBoard Contributor 3303035c04 Alerts: trigger on approach — geofence radius = alert radius
- Region radius changed from fixed 300 m forecourt fence to the alert
  radius (the Alerts-tab slider), clamped to the device's
  maximumRegionMonitoringDistance.
- Notification now fires when you ENTER the winner's circle while
  approaching, not when you reach the forecourt.
- Cheapest-gate unchanged: still only fires when the entered station is
  the cheapest within the alert radius (fresh prices). Dedup still 1/
  station/hour. 35 tests pass.
2026-08-12 14:50:09 +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