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.
- 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
- 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
- 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
- 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
Raw tab captures for the yuzu-hub App Store Screenshot Generator:
stations, favourites, alerts, settings. Captured via simulator launch
args (-skipOnboarding / -tab).
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.
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.
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.
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.
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)
- 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)
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.
- 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
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.
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.
- 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.