Commit Graph
100 Commits
Author SHA1 Message Date
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 28bb245c93 live activity: fix vertical centering on lock screen
GeometryReader as the layout root pinned content top-left, and forcing
maxHeight:.infinity over-claimed the whole proposed height, centring the
content below true vertical centre (bigger gap above) on the Lock Screen.

Demote GeometryReader to a background side-channel that only measures the
slot width for the rich-vs-compact branch; the content stays intrinsic-sized
so the system vertically centres it itself.
2026-08-20 10:16:21 +01:00
FuelBoard Contributor d8412c132a live activity: remove the white/grey background circle behind the pump
User: 'Remove the white background circle from the live activity'. Drop
the explicit Circle badge and render the fuel-tinted fuelpump.fill on its
own (sized up 19 -> 28 since it no longer sits inside a disc).
2026-08-19 22:52:50 +01:00
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 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 2205a585f1 onboarding: animated feature icons 2026-08-19 07:09:54 +01:00
FuelBoard Contributor f9b70dbb9d anim: consistent bold crossfade in fuel-type picker 2026-08-19 07:09:54 +01:00
FuelBoard Contributor 68d970720a backlog: CT 111 rootfs grown to 8 GB (pve); open item retired 2026-08-16 21:19:39 +01:00
FuelBoard Contributor 9952a096a4 docs: CT 111 is source of truth; Mac clone read-only mirror 2026-08-16 21:04:44 +01:00
FuelBoard Contributor 49503bef99 backlog: relay+mirror moved to Proxmox CT 111 (fuelboard-relay), live push verified 2026-08-16 18:55:07 +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 430006be94 Trends: chart key shows average price per station + descriptor footer
Legend rows now show each station's average over the days shown
in brackets (pence; signed +X.Xp in vs-cheapest mode, matching the
list-row deltas). Footer under the key explains the figure per mode.
averagePence helper (nil for empty series) + 3 tests; -showTrends
becomes a permanent QA hook like -showKeySheet.
2026-08-16 10:39:49 +01:00
FuelBoard Contributor 9aea478317 Trends: explicit series so each favourite draws its own line
LineMark lacked a series: value, so Swift Charts merged every
station's points into one polyline — only the first station's
line was recognisable. Adding series: per station splits the
chart into N coloured lines (legend colours now match).
2026-08-16 10:19:51 +01:00
FuelBoard Contributor 3416ad563e Key sheet: taller detent + opaque background
Sheet grows from .medium to .fraction(0.6) so the footer ('Colours
match each station's rating on the list') clears the bottom edge with
comfortable margin (verified: footer clearance +33pt). Sheet background
switched from the translucent default to a solid
systemGroupedBackground via .presentationBackground so the stations
list no longer shows through.
2026-08-16 09:57:58 +01:00
FuelBoard Contributor 87d66eae94 Key sheet: padding on TOP + star cards
The two bottom cards (TOP, star) previously had no padding — their
background hugged the text. Add .padding(.horizontal, 14).padding(.vertical, 12),
matching (slightly exceeding) the RAG cards' vertical padding so the
info cards read as proper cards.
2026-08-16 09:48:33 +01:00
FuelBoard Contributor 86385d53a2 Key sheet: card redesign matching the tip section
Price-rating legend now renders as five accent cards (tip-section style):
Best value (green, checkmark) / Okay (orange, equal) / Pricey (red,
exclamation) each with a RAG dot + threshold (≤1.5p / ≤4p / >4p) on the
right in the row's accent colour; TOP (blue capsule) and star (yellow)
as accent cards below a divider. Footer: 'Colours match each station's
rating on the list'. Copy split into title/blurb keys in Localizable.
Sheet chrome (medium detent, Done, info button) unchanged.
2026-08-16 09:18:27 +01:00
FuelBoard Contributor 9fdcc7370e Tip section: card design per user mockup
Three stacked rounded cards, one per tier, each with an accent-tinted
fuel-pump icon and price (Splash & Dash #64D2FF blue, Half a Tank
#30D158 green, Fill 'Er Up #FFD60A amber — the app's fuel palette),
name + blurb on the left, bold accent price on the right. Cards sit on
clear list rows with their own rounded fill + subtle accent tint.
Purchase flow, per-tier live prices, disabled state and footer copy
unchanged.
2026-08-16 08:57:40 +01:00
FuelBoard Contributor 5060aa9df5 APPSTORE: tip jar checklist — three consumables + IAP capability
Adds the three tip IAPs (tip099/tip299/tip499 at £0.99/£2.99/£4.99) to
the USER-owner ASC tasks, and the In-App Purchase capability to the
App ID blocker alongside the App Group.
2026-08-16 08:37:33 +01:00
FuelBoard Contributor 41e4c913fb Tip jar: three tiers with names and blurbs
Splash & Dash (£0.99), Half a Tank (£2.99), Fill 'Er Up (£4.99) —
each a consumable (com.apt.fuelboard.tip099/tip299/tip499) with its own
blurb row. TipStore now loads all three products, prices fall back to
the intended storefront values until ASC products exist, and the
Transaction.updates listener acknowledges any of the three tiers.
Footer copy: 'A tip helps keep this app free and supports development.
Thank you'.
2026-08-16 08:34:27 +01:00
FuelBoard Contributor 2ffa90d879 Tip jar: finish transactions + Transaction.updates listener
The £4.99 consumable was never finished — StoreKit kept re-delivering
it through Transaction.updates on every launch. Verified purchases now
call transaction.finish() (direct path), and a Transaction.updates
listener finishes + acknowledges purchases that complete outside the
direct call (Ask to Buy approvals, other-device purchases on the same
Apple ID). Untrusted (unverified) transactions are never finished or
delivered; only our own product ID is handled.
2026-08-16 08:23:18 +01:00
FuelBoard Contributor a490bcf7b5 backlog: plan relay+mirror move to dedicated Debian LXC (2026-08-16) 2026-08-15 23:26:56 +01:00
FuelBoard Contributor 29bf2f98d6 Widget shots: use the shipping app icon (light + dark variants)
- AppIconPreview now byte-identical to the real AppIcon.png (the preview
  had been pointing at an older Design/ export); added AppIconPreviewDark
  from AppIcon-dark.png so the mock shows the correct icon per appearance
- Re-captured 9-widgets-dark + 10-widgets-light
- 95/95 tests green; Release IPA rebuilt + re-served
2026-08-15 16:53:31 +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 6369eef249 Widget preview: real small-tile proportions + iOS-default-style wallpaper
- Small widget card sized to actual system-small content (158×158pt + padding)
  instead of filling available width; medium keeps 338×158pt
- Wallpaper now mimics the iOS default (dark navy→black in dark mode,
  soft blue-grey in light) via colorScheme-adaptive gradient
- Added 10-widgets-light.png; re-captured 9-widgets-dark.png with data
- 95/95 tests green; Release IPA rebuilt + re-served
2026-08-15 16:32:45 +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 24b6ca9bb2 Copy: England → UK-wide, include Northern Ireland; migrate user-facing copy into Localizable.strings
- Onboarding feature: 'England-wide prices' → 'UK-wide prices — 8,000+ stations, updated twice a day' (key + value unified)
- Stations empty state: 'covers England, Scotland and Wales' → 'covers the whole UK' (dataset includes Northern Ireland)
- Settings Data source footer: source wording updated (CSV mirror or API), OGL attribution retained
- Empty state, browse button and OGL footer now keys in Localizable.strings so the web copy editor covers them
- APPSTORE.md + BACKLOG.md + code comments: England-wide → UK-wide, NI included
2026-08-15 16:04:56 +01:00
FuelBoard Contributor 4797886a1d Add raw App Store screenshot captures (6.9-inch, 1320x2868)
Raw tab captures for the yuzu-hub App Store Screenshot Generator:
stations, favourites, alerts, settings. Captured via simulator launch
args (-skipOnboarding / -tab).
2026-08-15 15:55:19 +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 06dd8a919c APPSTORE.md: submission plan — P0 resolved old blockers, owner/timeline checklist 2026-08-15 14:57:20 +01:00
FuelBoard Contributor 44a8e7f42a Onboarding: prompts fire on forward-leave of their page, not arrival
b634c06 fired on page arrival, so swiping stacked Location + Notifications
before a screen was read. Now each permission fires only when the user
ADVANCES PAST its page (swipe forward or Continue) — the screen has been
read by then. Forward-only (swiping back never fires), idempotent
(already-determined → status refresh), so replay and auto-advance stay
clean. Continue buttons are now pure navigation; denied paths unchanged.
2026-08-15 14:44:18 +01:00
FuelBoard Contributor b634c0686d Onboarding: permission prompts fire on page arrival, not just Continue
Swiping past the Location/Notifications pages never triggered the system
prompts — only tapping Continue did. The prompt now fires the moment its
page comes into view (onChange(of: page)): swipe, auto-advance after a
grant, or Continue all land on the page and request the permission. Both
request methods are idempotent (already-determined → status refresh, no
duplicate alert), so replay and the auto-advance path stay clean.
2026-08-15 14:34:24 +01:00
FuelBoard Contributor 485ef782b6 Stations tab: explainer styled like the Favourites cheapest callout
The N/TOTAL stations-updated explainer now sits in a plain caption Section
cell at the bottom of the tab (font .caption, secondary) — matching the
Favourites tab's bottom callout instead of rendering as a section footer.
2026-08-15 14:22:43 +01:00
FuelBoard Contributor b072b62208 Stations tab: explainer moved below the list with N/TOTAL station count
The header section was removed entirely — the tab now opens with Fuel type.
The explainer lives as a section footer at the bottom of the tab:
- Cheapest: 'Cheapest Unleaded within 5 miles — 46/8022 stations updated ·
  tap a station for directions.'
- Closest: 'Closest Unleaded stations — nearest first, best value within
  5 miles · 46/8022 stations updated'
- no-location variant keeps mode + tap-for-directions hint
Numerator = stations in the current list pool (as before); denominator =
full UK station total from the last chain fetch (loadStationCount, 8,022
mirror snapshot) — ratio hidden until the first fetch lands.
2026-08-15 13:57:08 +01:00
FuelBoard Contributor 10ab1cebef Backlog: P0 Done entry updated for GitHub-only consumer chain 2026-08-15 13:33:53 +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 c447372d40 Backlog: P0 live chain DONE (moved to Done, P0 section cleared) 2026-08-15 13:01:37 +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 54aca14fd4 Trends: anchor empty/loading/error states to the top of the chart slot
The no-data states previously floated or centred vertically (Spacer
sandwich), so the sheet rendered them halfway down the page while the
chart-with-data anchors to the top. All chartArea states now share one
top-aligned container that fills the space below the pickers — the
empty state sits exactly where the chart will render, and the layout no
longer jumps between states. 83 tests, Release build green.
2026-08-15 12:30:25 +01:00
FuelBoard Contributor d1fbac3f05 New app icon (drip: £ fuel drop, green border, gradient bg) + Alerts radius guidance labels
- AppIcon / AppIcon-dark / AppIcon-tinted swapped for the drip design
  (1024px, dark + tinted variants), onboarding artwork matched
- Alerts tab: live explainer under the Radius picker — situational
  guidance per option (Follow search = set-and-forget mirror of the
  Stations distance; 1 = town driving; 2 = city/suburb commute;
  3 = mixed; 5 = A-roads/motorway exits; 8 = long-distance) — plus the
  trade-off line in the section footer
- 83 tests, Release build green
2026-08-15 12:18:56 +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 d0d25811c7 chore: untrack build-sim artifacts (ignored now; were committed before the rule) 2026-08-15 10:12:46 +01:00
FuelBoard Contributor aab2d02ef5 P1: Favourites Trends graph — FuelHistoryStore + TrendsView (Swift Charts)
- Shared/FuelHistory.swift: GitHub mirror read path (latest.json pointer +
  history/YYYY-MM-DD.json day files); day math (UTC noon keys), slim day decode
  with the shared price band, series + deltaSeries (vs-cheapest rebase),
  favourites-only app-group cache (90-day prune), parallel per-day fetches,
  404/network = graph gap never error.
- FuelPriceProvider: shared priceBand + mapGrades (single source for live +
  history decoding).
- FuelBoard/TrendsView.swift: line chart in the Favourites tab via toolbar
  button + sheet; fuel capsule (fuels with favourites only), 7/30/90 range,
  Price/vs-cheapest toggle, per-station legend, price-display style on y-axis,
  empty/loading/retry states with honest copy; no widget in v1.
- Localizable.strings: Trends keys.
- 83 tests (13 history + URL regression): day math, band guard, series gaps,
  delta rebasing, prune, and the appendingPathComponent regression (relative
  URL resolution dropped /main — all fetches 404'd).
- Sim-verified: favourites rows + TOP/deltas; sheet controls + building-up
  state with live first-snapshot date (archive has 1 day; lines render once
  >=2 snapshots).
- Backlog: P1 Trends DONE (unmerged); P0 REMAINING = live provider chain +
  telemetry beacon.
2026-08-15 10:12:23 +01:00
FuelBoard Contributor 5b83d9f510 Backlog: P0 mirror push side DONE (repo live, deploy key, launchd daily job); size math corrected to full-dump 2.58MB/day @180d retention (Option 2) 2026-08-15 09:24:33 +01:00
FuelBoard Contributor 0545659c1b Backlog: P0 GitHub mirror — capture telemetry implications (widget beacon survives, opportunistic app beacon, /stats canary + fallback-outage signal) 2026-08-15 08:43:59 +01:00
FuelBoard Contributor 817a829107 Backlog: P0 archive reshaped to GitHub price-history mirror (relay pushes daily latest.json + history/YYYY-MM-DD.json to public fuelboard-data repo; app reads raw.githubusercontent); P3 production-mirror item absorbed into P0 2026-08-15 05:49:18 +01:00
FuelBoard Contributor 16bf9ba9dd Backlog: price-history direction — P0 relay archive (start now) → P1 Favourites Trends graph → P2 avg/min-max overlay → P3 prediction arrow + widget sparkline 2026-08-15 05:39:51 +01:00
FuelBoard Contributor bb6ee33254 Stations: move price-rating Key (legend) into a modal sheet opened by an info button in the nav header; list stays focused on stations. Sheet = medium detent, inline nav title 'Key', Done button; legend rows unchanged. Adds -showKeySheet launch arg (QA hook for sim). 2026-08-14 23:53: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 d91652aedc Onboarding: space the welcome feature rows out vertically (per-row padding 5pt → 10pt) 2026-08-14 22:47:48 +01:00
FuelBoard Contributor 9571e0c876 Onboarding: enlarge welcome-page feature icons 16pt → 20pt (semibold, wider alignment frame) for readability 2026-08-14 22:43: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 8a589a35c1 Settings: drop parenthesized example figures from Price display segmented buttons — 'Station sign' / 'Pounds & pence' (cleaner, roomier labels) 2026-08-14 20:56:01 +01:00
FuelBoard Contributor e317a1e451 Settings: rename 'Show introduction' row to 'Replay onboarding' with round-arrow icon (arrow.clockwise.circle); key stays the address, value changed in Localizable.strings 2026-08-14 19:34:11 +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 819d2e6152 BACKLOG: favourite-directions phrase drop + Directions-to-Cheapest allocation 2026-08-14 14:50:53 +01:00
FuelBoard Contributor 8a4c472ab3 Siri: allocation swap B — Directions-to-Cheapest voice phrases restored (per-fuel), favourite-directions phrases dropped (card button covers it) 2026-08-14 14:50:31 +01:00
FuelBoard Contributor b46c09109e Siri: full-width Directions row on price card — fixes squashed 2-line button in narrow snippet 2026-08-14 14:45:51 +01:00
FuelBoard Contributor 6d7254ebba Siri: interactive price-card snippet (iOS 26 SnippetIntent) with Directions button → Apple Maps; zero AppShortcut slots 2026-08-14 14:38:14 +01:00
FuelBoard Contributor b458d92972 Siri: favourite price + favourite directions (top favourite per fuel); trim to 10 AppShortcuts (Apple cap) 2026-08-14 14:24:20 +01:00
FuelBoard Contributor aed99e3f32 Siri directions: speak the station before Maps hand-off (.result(opensIntent:dialog:)) 2026-08-14 14:05:42 +01:00
FuelBoard Contributor caadf184f2 Siri: Directions to Cheapest [fuel] AppShortcuts — OpenURLIntent hands off to Apple Maps (universal link) 2026-08-14 13:55:03 +01:00
FuelBoard Contributor a3b6f37843 Siri: shortcut names match app (Unleaded/Premium/Diesel); phrases cover UK speech variants 2026-08-14 13:23:52 +01:00
FuelBoard Contributor e55078c5e2 Siri: per-fuel App Shortcuts with literal fuel words (unleaded/petrol/premium/diesel) + UK case labels 2026-08-14 13:06:26 +01:00
FuelBoard Contributor 037c2418af Siri: scope cheapest-near-me to the saved search radius (5/10/15 mi) 2026-08-14 12:49:30 +01:00
FuelBoard Contributor be781d80f5 Backlog: Siri on-screen awareness (View Annotations + StationEntity) P2 2026-08-14 12:42:27 +01:00
FuelBoard Contributor 29d4a0472b Siri: cheapest [fuel] near me intent + App Shortcuts (cached-first, freshness label) 2026-08-14 12:32:31 +01:00
FuelBoard Contributor b5fe21c44b Backlog: Siri cheapest entry notes Tailscale interim + IN PROGRESS status 2026-08-14 12:29:09 +01:00
FuelBoard Contributor f85c74a7a8 Backlog: Localizable.strings key-normalization idea (P3, no action) 2026-08-14 12:02:26 +01:00
FuelBoard Contributor b66a9161a3 Copy sync: Localizable.strings matches current UI (114 keys; favourites reorder note, geofence debug section, alerts footer; dead keys pruned) 2026-08-14 11:07:15 +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 edf376d378 Settings: move Data source section behind debug flag (debug-only) 2026-08-14 10:01:36 +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 e62177ae2e Backlog: add lock screen widgets (P2, Option B) 2026-08-14 05:02:43 +01:00
FuelBoard Contributor e044a1552a Backlog: add Siri/Shortcuts features (P1-P3); mark widget config done 2026-08-14 04:59:26 +01:00
FuelBoard Contributor f5a5d3325f Swap app icon to water-drop pound design (drip export) 2026-08-14 04:53:31 +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 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