Commit Graph
63 Commits
Author SHA1 Message Date
FuelBoard Contributor 1d62204a7a live activity: explicit badge (dark circle + bright pump); fix alignment look
fuelpump.circle.fill's first hierarchical style hits the PUMP, not the
circle, so the earlier darkened colour landed on the pump and the circle
stayed white (user: 'I didn't want the fuel pump darker I wanted the
white circle darker'). Rebuild the brand glyph from two explicit shapes:
a light-grey (.86) Circle badge + a bright fuel-tinted fuelpump.fill, so
circle vs pump colours are unambiguous and the badge centres cleanly with
the two-line text block.

Add a DEBUG-only -qaLiveActivity <fuel> launch hook that starts a Live
Activity with a long station name so the Lock Screen can be rendered in
the Simulator and verified by pixels (pump bright green 43,193,81; badge
grey 202,201,202).
2026-08-19 22:46:57 +01:00
FuelBoard Contributor bdfbbc605f live activity: centre content vertically + darken pump circle
A GeometryReader hands its content the full proposed frame and pins it
top-left, so the Lock Screen card sat high instead of vertically centred
when the activity container offered extra height. Wrap the branch in
.frame(maxWidth:.infinity, maxHeight:.infinity, alignment:.center).

Also darken the pump circle to 82% of the raw fuel tint (mix 18% black) so
the white pump symbol keeps contrast against bright Lock Screen/card, esp.
for the light yellow premium / cyan diesel palette colours.
2026-08-19 22:26:37 +01:00
FuelBoard Contributor 2c6e63f6b1 live activity: tint pump glyph by monitored fuel
Move FuelType.tintColor (green=unleaded #30D158, yellow=premium #FFD60A,
cyan=diesel #64D2FF) into Shared/FuelStore.swift so the app, widget, and
Live Activity all read one definition (was app-only in StationsView, so the
widget couldn't see it). Tint the Live Activity rich card pump icon and the
Dynamic Island compactLeading pump by context.state.fuel.tintColor.
2026-08-19 22:18:56 +01:00
FuelBoard Contributor a6620d36f7 live activity: select rich vs compact by actual GeometryReader width
ViewThatFits measures each child's IDEAL (untruncated) width to decide
fit, and a flexible .frame(maxWidth:.infinity) middle column does not cap
that ideal — so a long station name (e.g. 5-mi Phoenix Filling Stations)
inflated richBody's ideal past the iPhone Lock Screen width and fell back
to the compact strip. Replace the ViewThatFits branch with a
GeometryReader that reads the ACTUAL proposed width and shows the full
three-column card at/above 280pt, compact below. Content-independent, so a
long name can no longer collapse the iPhone card.
2026-08-19 22:08:07 +01:00
FuelBoard Contributor 227cce4aa6 live activity: fix compact strip string interpolation (double backslash)
The compact CarPlay/Watch strip's second line had escaped interpolation
(\\( instead of \\() so it rendered the literal placeholder text
"context.state.stationName · context.state.distanceText" instead of the
actual station name and distance. Normalize to single-backslash interpolation.
2026-08-19 20:01:14 +01:00
FuelBoard Contributor 01bb3ba905 live activity: compact CarPlay strip shows fuel + price + station distance
Restore .supplementalActivityFamilies([.small]) + a ViewThatFits compact
fallback so CarPlay's small slot renders our shared compact strip (fuel type,
bold price, station · distance) instead of falling back to the Dynamic Island
compact closure, which showed the app name and omitted the distance the user
wants. Full iPhone/iPad card is preserved via the richMinWidth gate on
richBody plus its flexible, truncating middle column (long names can no longer
collapse it).
2026-08-19 18:31:36 +01:00
FuelBoard Contributor 5d26509034 live activity: always render full card (drop compact fallback)
Long station names inflated richBody's IDEAL width, so ViewThatFits (which
measures ideal, untruncated text) decided 'rich doesn't fit' and fell back to
the compact strip — that's why 5-mi / long-named activities rendered small
while 10-15-mi / short names stayed full. With .supplementalActivityFamilies
already removed, the body is only ever Lock-Screen width, so drop ViewThatFits
and compactBody entirely and always render the full three-column card; the
station caption line-limits + scale-downs + tail-truncates in place.
2026-08-19 16:39:49 +01:00
FuelBoard Contributor 221b96097d live activity: drop .small family so iPhone/iPad show full-width card
The active activity was rendering the narrow .small form on the iPhone Lock
Screen (squeezed, small text) because .supplementalActivityFamilies([.small])
made iOS eligible to present it that way. Remove it so iPhone/iPad always get
the full three-column card; CarPlay's small form still comes from the Dynamic
Island compact closures. Keep compactBody as a narrow-width safety fallback.
2026-08-19 16:09:02 +01:00
FuelBoard Contributor b012232012 live activity: robust full-vs-compact split; scale long station names
The adaptive layout picks the full three-column view when there's Lock
Screen width and a compact price strip in the small CarPlay/Watch slot.
Tighten the discriminant (rich ≥290pt, compact ≤220pt) so iPhone/iPad
always get the full view and only genuinely small slots get the compact
strip. Long station names used to clip at inconsistent points across
the rich/compact/Dynamic Island views; both bodies now scale + tail-
truncate so the caption stays consistent regardless of station name.
2026-08-19 13:58:54 +01:00
FuelBoard Contributor dcd5517825 Live Activity: add distance to compact CarPlay layout 2026-08-18 10:00:19 +01:00
FuelBoard Contributor 63084322f6 Refine trends and debug harness; clean up warnings 2026-08-17 14:06:26 +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 535a504160 Consumer chain: GitHub-only — relay removed from app+widget, Local Network permission gone
The LAN relay is unreachable from any phone outside the developer's LAN, so
for App Store users a relay fallback was dead weight AND harmful: attempting
the private IP fires the iOS Local Network prompt on a stranger's phone and
adds a timeout before the bundled dump. GitHub raw is reachable anywhere the
relay would be, and more reliably.

- LiveChainProvider: GitHub mirror first EVERYWHERE (focused alert path no
  longer relay-first); LAN relay only joins behind the hidden dev flag
  fuelboard.relayFallback (Settings → Debug, keychain-first, off by default)
- MirrorFuelProvider: focused() projection replaces the relay's server-side
  radius endpoint (fuel filter + within-radius + nearest-first + limit 500)
  — the alert path re-checks radius/fuel itself, so behaviour is identical
- Widget: fetchFocused/fetchFuelOnly now GitHub-first via the chain (day-cache
  shared through the app group); relay + widget-diag GET gated on the same
  dev flag — off-LAN widgets no longer attempt local network at all
- FuelBeacon: fires only when the dev flag is on — consumers make zero
  local-network attempts; /stats app-hit spike = GitHub down + dev flag on
- Onboarding: Local Network machinery removed entirely (no probe, no prompt,
  no 12 s stall) — data page is now informational: prices download from the
  internet, no permissions needed. Replay on cellular can no longer hang.
- Info.plist: NSLocalNetworkUsageDescription copy → optional dev-only wording
- 95 tests (6 new: relay-skip-when-disabled x2, focused prefer-mirror, mirror
  focused projection x3, beacon dev-flag gate)
2026-08-15 13:33:02 +01:00
FuelBoard Contributor 75506b48b7 Telemetry: tag relay fetches with X-Client (app/widget) — app via RelayFuelProvider, widget focused/fuel-only fetches + diag beacon; Siri untouched (cached-only). Purely observational for relay admin /stats. 2026-08-14 23:23:06 +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 3dd567a5cc Widgets: merge small+medium into one kind (both sizes, one gallery entry) 2026-08-14 09:52:50 +01:00
FuelBoard Contributor b94f769750 Small widget: support .systemMedium so the icon menu offers both sizes
The iOS 18+ long-press app-icon menu (turn icon into a widget) shows
the supported SIZES of the FIRST-registered widget kind only — it
does not list widget kinds and is not keyed by intent type (two
distinct intents in 6375af4 changed nothing on-device). The bundle's
first kind (FuelPriceSmallWidget, small-first since 38b47ee) therefore
decided the menu offered only the small tile.

The early build worked because FuelPriceWidget was registered first
back then. Reordering to medium-first would fix the menu but flip the
gallery order (38b47ee). Instead: this first kind now supports BOTH
.systemSmall and .systemMedium, so the icon menu offers both size
tiles (ChatGPT/Safari behave the same way — their first kind supports
several families). The gallery still lists both widget kinds in bundle
order; the Small entry just gains a medium variant. The shared view
already branches on family, so medium renders the station list.
2026-08-14 02:53:52 +01:00
FuelBoard Contributor 6375af4c05 Small widget: restore its own intent TYPE (minimal params) for the icon menu
iOS keys the long-press app-icon contextual menu by configuration-intent
TYPE, so sharing one intent across kinds collapsed the menu to the
first-registered kind (small) — the medium 'FuelBoard Prices' vanished
from the icon menu while the + gallery (kind-keyed) still listed both.
dafc96f's skeleton fix was never the problem; the extra favourite
AppEntity params were.

FIX: FuelBoardSmallWidgetConfigurationIntent — a distinct intent TYPE
with the IDENTICAL minimal parameter set (fuel/sort/distance only, no
AppEntity params). Fresh-widget default-config resolution stays intact;
the appex metadata now registers two WidgetConfiguration actions.

Verified: build green, 45/45 tests, extract.actionsdata lists both
intents, fresh IPA packaged + signed per component.
2026-08-14 02:28:43 +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 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 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 3e226ece57 Widget links maps:// directly so CarPlay can route to Apple Maps
- 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.
2026-08-12 13:07:44 +01:00
FuelBoard Contributor 1ff14607c5 Widget tap relay via fuelboard:// scheme; full station names; no check-again
- 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.
2026-08-12 12:54:08 +01:00