16 KiB
FuelBoard Backlog
FuelBoard — UK-wide petrol-station price widget + app.
GitHub mirror (aptonline/fuelboard-data, 2×/day 10:00+16:00) holds the
full-UK dump (~8,022 stations, all prices); app fetches it once per 12 h and
filters/sorts on-device, with a bundled snapshot as the no-network last resort.
LAN relay is dev-only (hidden fuelboard.relayFallback flag).
Priority guide: P0 = next build, P1 = soon, P2 = later, P3 = maybe/idea. Status: TODO / IN PROGRESS / DONE / BLOCKED.
P1 — Soon
-
Move relay + mirror off the Mac mini (DONE 2026-08-16) — dedicated Debian 13 LXC on Proxmox via community-scripts/ProxmoxVE
ct/debian.sh(CT 111, hostnamefuelboard-relay, 1 core/512 MB/2 GB, static IP 192.168.1.113, plain LXC — NO Docker layer). Scope: relay AND mirror both moved (full Mac independence; relays bind loopback only, GitHub is the only public surface). What landed: Python 3.13.5 (trixie) boots both relays (relay 8788 demo/CSV + relay-api 8789source: api, 8,024 stations, ~2 min first sync, 0 failures); source at/root/workspace/fuelboard-{relay,relay-api}(venvs + pip,.envchmod 600); deploy keyfuelboard_deploy+github-fuelboard-datassh config +fuelboard-dataclone at/root/workspace/fuelboard-data(origin = ssh alias, green + synced);mirror_push.py→/root/scripts/mirror_push.py(RELAY line already127.0.0.1:8789); systemdfuelboard-relay.service+fuelboard-relay-api.service(Restart=always,--host 127.0.0.1) +fuelboard-mirror.service(oneshot) +.timer(10:00 + 16:00, Persistent, armed — next Mon 10:00); first live push VERIFIED from outside (commit735a292, latest.jsondata_updated 16:50:53Z); Mac launchdcom.apt.fuelboard-{relay,relay-api,mirror}unloaded + plists parked in~/Library/LaunchAgents/fuelboard-migrated-to-ct/; editor 8790 + IPA 8765 stay on the Mac; price watchdogrelay_price_watchdog.pynow fetches via SSH (~/.ssh/fuelboard_ct→/root/scripts/watchdog_fetch.py). Logs viajournalctl -u fuelboard-*on the CT; SSH into CT:ssh -i ~/.ssh/fuelboard_ct root@192.168.1.113. Open: rootfs is 2 GB (1.1 GB free) — mirror retention ~940 MB will get tight; rootfs grown to 8 GB on pve 2026-08-16 (5.9 GB free). -
Siri: "Cheapest [fuel] near me" —
AppShortcutsProvider+CheapestFuelIntent(App Intents, iOS 16+), dialog + price-card snippet, cached full-UK dump first with freshness label ("as of 6am"), last-known-location fallback. Constraint: fresh-data path needs a reachable source off-LAN — cached answers work anywhere the app has previously fetched (the GitHub mirror serves this now). DONE — radius-scoped lookup037c241(real-data regression: GULF HISTON 100.9p 132 mi away), per-fuel literal-word shortcutse55078c, names = app wordsa3b6f37, plus Directions-to-cheapestcaadf18+ spoken hand-offaed99e3. -
Siri: "Closest [fuel] station" — same plumbing as cheapest, sort by distance instead of price. Free second phrase in the same
AppShortcutsProvider. -
Favourites Trends graph — price history chart for starred stations. Swift Charts (iOS 26 target) line chart in the Favourites tab; one series per favourite, fuel-scoped (station, fuel) pairs map 1:1 to the archive. Default = absolute price lines; toggle "vs cheapest favourite" re-baselines to 0 as signed pence (mirrors the Cheapest/Closest delta pattern users already know). Reuse the capsule fuel picker; range control 7/30/90 days; respects the price-display toggle; honest empty-state copy in Localizable.strings ("Prices are recorded from each refresh — check back in a few days"). No widget in v1 (one-kind rule). Depends on the P0 archive. DONE 2026-08-15, merged to main 2026-08-16:
Shared/FuelHistory.swift(FuelHistoryStore: day math, slim day decode w/ shared band, series + deltaSeries, app-group favourites-only cache, parallel per-day fetches) +FuelBoard/TrendsView.swift(chart, fuel capsule, 7/30/90, Price/vs cheapest, per-station legend, empty/loading/retry states) + toolbar entry in FavouritesView + strings. 83 tests incl. URL regression (appendingPathComponent — URL(string:relativeTo:) dropped /main, all fetches 404'd). Sim-verified: favourites rows + TOP/deltas; sheet controls + honest empty state w/ live first- snapshot date. Pending: lines render once ≥2 snapshots (archive warming daily, ~Aug 17).
P2 — Later
-
Shortcuts: station price lookup —
StationEntity+EntityQueryso Shortcuts/Siri pick a station by name;ReturnsValue<Double>enables user automations ("get cheapest diesel → notify"). -
Siri: favourite station price — "How much is my favourite unleaded?"
FavouriteFuelPriceIntent: TOP favourite per fuel (first in the manual Favourites order —SiriCheapestLookup.topFavourite, refresh-vs-dump with offline cached-snapshot fallback); no location required (distance shown only when the saved fix exists).DirectionsToFavouriteFuelIntent(Maps hand-off, station announced first) stays in code but carries no voice phrases — every favourite price card has the interactive Directions button (6d7254e), and the voice slots went to Directions-to-Cheapest (8a4c472). DONE — favourites already carried station snapshots, so noStationEntitydependency. -
Siri: on-screen awareness (View Annotations + StationEntity) — iOS 26/27 Siri prefers the visible screen over the App Shortcut when the app is frontmost; without view annotations it answers from the rendered screen (unstructured "screenshot" parse) — visible tab/sort/radius, not the full dataset or the intent's logic. Fix:
StationEntity(shared with the station lookup P2 above) +.appEntityIdentifieron station rows (collection annotation on the List) +NSUserActivityon the station detail/map + App Schema conformance (optionalIndexedEntityfor Spotlight semantic search). Verify annotations with AppIntentsTesting (P3). Off-screen/locked requests already route toCheapestFuelIntentcorrectly; this closes the frontmost gap. WWDC26: "Build intelligent Siri experiences with App Schemas"- "Explore advanced App Intents features".
-
Lock screen widgets — new
FuelPriceLockScreenkind (fuel-only config intent, reusesFuelPriceTimelineProvider):accessoryRectangular(cheapest station + price + distance),accessoryCircular(price ring),accessoryInline("⛽ 139.9p"). Provider +containerBackgroundalready exist; must branch the view for accessory families (currentelserendersstationList). Bonus: StandBy mode. ~1 session. -
CarPlay — widgets render but can't launch Maps; consider a proper CarPlay scene (CPListTemplate) for navigation deep-links.
-
Alert region tuning — make alert trigger radius + dedup window user-configurable (currently fixed 300 m trigger / 1 per station per hour).
-
Price-drop alerts: threshold control + Check now — keep the current cache-first / 12 h refresh policy, but add a per-alert sensitivity control for favourite price-drop alerts instead of a fetch-frequency slider. Scope:
Minimum changepicker (e.g. 0.5p / 1.0p / 2.0p), plus aCheck nowaction in the Alerts tab that forces a fetch and immediately re-evaluates the cheapest-favourite alert. Copy should set expectation honestly: standard mode checks when FuelBoard refreshes prices (up to twice daily). -
Price-drop alerts: optional frequent-check mode — if the app ever relaxes the current "twice daily max" data policy, expose this as a clearly separate mode rather than a freeform frequency slider. Example framing:
Standard(current behaviour) vsFrequent(best-effort extra checks), with conservative caps (e.g. every 4–6 h, not hourly) and copy that does not promise exact timing under iOS background scheduling. -
Pull-to-refresh spinner state — surface refresh in-flight state (currently
refreshablefires but no visible progress in the row list). -
Offline first-run — bundled REAL 8,022-station dump (
FuelBoardDump, refreshed byscripts/refresh_bundled_dump.sh) is the no-network last resort in the live chain (P0, 2026-08-16); first launch demos the full dataset offline. -
Localization — UK English only today; audit string catalog for l10n.
-
Dark mode polish — verify brand logos (white-circle clips) and RAG dot contrast under dark appearance on-device.
-
Trends: national average overlay + per-day min/max — the full dump is already held, so the UK/regional average line is computable locally; archive can store per-day min/max so the chart shows intraday spikes, not just the daily point.
P3 — Ideas / Research
-
Siri: create fuel alert — voice-create an alert for a station. Deferred: alert semantics (cheapest-within-radius + hourly dedup) are hard to express in a dialog; needs the alert engine surfaced as an intent.
-
App Intents testing — AppIntentsTesting (iOS 27) for intent coverage instead of XCUITest when the Siri features land.
-
Fuel-price prediction — "price tends up/down" arrow derived from the price-history archive (P0). Cheap once the archive + Trends graph exist; keep a separate phase. Backlog ancestry: this was the original "relay could keep a rolling archive" idea — the archive is now P0 and the Trends graph (P1) is its visible half.
-
Foresight Live Activity (heading-aware next station) — investigate a driving-time surface that predicts the likely next station ahead of the user from cached data only (no extra source-data refresh on motion). Best v1 shape: Live Activity option, not widget-first, because widget space/refresh budgets are tight and the lock-screen/CarPlay compact slot better matches the use case. Viable MVP: infer heading from recent motion/course, keep only stations in a forward cone, score candidates by forward distance + lateral offset, and show the next-ahead station with all three fuel prices or a compact rotating/focused fuel treatment if space allows. Key caveat: heading is not full route awareness, so avoid overpromising "the next station you will reach" on motorways/opposite carriageways; frame as "next ahead" / "likely next stop" unless route-quality heuristics improve. Must include hysteresis/speed thresholds to avoid flapping, and should be tested specifically against compact Live Activity/CarPlay layouts where space is extremely limited.
-
Widget sparkline — medium-widget mini sparkline of a favourite's 30-day trend, after the Trends UI lands (one-kind rule preserved).
-
Price alerts for a specific station — notify when a starred station's price drops below a user threshold.
-
Price-drop alerts: mode = Cheapest favourite / All favourites — keep
Cheapest favouriteas the default low-noise mode, but explore an optionalAll favouritesmode for one fuel. Guardrails if built: threshold required, per-station dedupe based on last-alerted price (not just time), and combine multiple drops from one refresh into a single summary notification rather than one alert per station. -
Share sheet — share "Cheapest Unleaded near me" as text/image card.
-
Watch app v1 — favourites + Check now — small Apple Watch companion, not a phone-app clone. Favourites-first home screen: fuel picker,
Cheapest favouritesummary card, top few favourites for that fuel, last-updated time, and a prominentCheck nowaction that forces a refresh + re-evaluates favourite prices. Keep the watch UX pull-based and glanceable; avoid implying high-frequency background refresh. Shared-store architecture preferred over a second watch-only data model. -
Watch complications / Smart Stack — complication/widget surfaces for the same watch concept: show the cheapest favourite price, fuel, and short station name at a glance. Keep copy minimal and honest about freshness.
-
Watch app v2 — favourite detail + handoff — tap a favourite row for a detail screen (station, brand, price, last updated) with actions like
DirectionsandOpen on iPhone. Reuse existing maps handoff patterns where possible instead of inventing watch-only navigation logic. -
App Clip — 10 MB budget for "find cheapest fuel nearby" without install.
-
Widget picker: dual-unit distance labels — the Edit-Widget Distance row label is cached by the system at sheet-render time and only re-resolves on picker interaction, so "mirror the app unit" goes stale. Fix: label options with both units ("5 miles (8 km)") so nothing can go stale; widget rows keep mirroring the live unit.
-
Localizable.strings: normalize keys to match copy — today 8 onboarding keys diverge from their values (key = old source literal, value = tightened copy, e.g. "…updated twice a day" key vs "…updated daily" value). Option to rename keys so key == value everywhere. Requires sweeping every Swift occurrence of the old literal (Text/NSLocalizedString) AND updating key + value together in one commit + rebuild — renaming the file entry alone silently reverts copy to the Swift literal at next build. Not recommended: keys are invisible anchors; value edits already change copy without code changes, and normalization reintroduces divergence on the next value edit.
Done (recent)
- GitHub price-history mirror + LIVE provider chain (P0, DONE 2026-08-16) —
push side (relay → public
aptonline/fuelboard-data, daily 10:00 launchd, SSH deploy key, 180-day retention) + history read side (Trends, P1) + live chain (feature/p0-live-chain):MirrorFuelProviderfetches CURRENT prices from raw.githubusercontent (latest.json → history/.json, app-group day-cache) → GitHub FIRST everywhere (alert checks + widget too — the relay is unreachable from any consumer phone and attempting it would fire the Local Network prompt); LAN relay joins ONLY behind the hidden dev flagfuelboard.relayFallback(Settings → Debug, off by default); bundled REAL dump (FuelBoardDumpdataset, refreshed byscripts/refresh_bundled_dump.sh) is the app's no-network last resort.FuelBeaconfire-and-forget X-Client app ping fires only with the dev flag on. Local Network permission removed from onboarding entirely — data page is informational, replay on cellular can no longer hang. 95 tests. - Switch dataset to govuk API data — official Fuel Finder OAuth API via
relay-api :8789 (source: api, ~8,012 stations); app baseURL → :8789; credentials
live in relay
.envonly, never in the IPA - Widget configuration — per-widget fuel/sort/distance via
WidgetConfigurationIntent(AppIntentConfiguration), distance options asWidgetDistanceAppEntity (26e724f); small widget gained Favourites (d0bd4e7) +.systemMedium(b94f769) - Unit test target for the app — FuelBoardShared XCTest target, 37 tests (title sanitizer, decode price guard, RAG thresholds, cheapest/closest baseline, miles↔km)
- Backlog hygiene — implemented items marked DONE, obsolete pruned
- Sync retry with exponential backoff — relay-api retries failed syncs
15s→15min cap;
/healthexposes retry state (6a58275) - England-wide full-UK dump, on-device filtering (relay
142061b, appcf9765b) - Cache-first, 12 h gate, pull-to-refresh only
- Distance filter 5/10/15 miles, local-only changes (no re-fetch)
- Closest mode = whole country, distance picker disabled
- Delta/TOP baseline = cheapest-within-radius in both modes
- Three-layer price sanitisation (relay band 50–500p / app decode guard / title-case)
- Title-case station names (chunk-aware sanitizer)
- Native large-title navigation (Mail-style collapse) —
313fe84 - E10/E5 bracket labels in picker + descriptions —
05f3ef0 - Gitea remotes created + pushed (
apt/fuelboard,apt/fuelboard-relay) - Brand logos: proper 2x/3x assets, 75% smaller (315 KB → 79 KB)
- Security audit (see SECURITY.md)
- App Store approval gotcha review (see APPSTORE.md)