# FuelBoard Backlog FuelBoard — England-wide petrol-station price widget + app. Relay mirrors the UK gov data (hourly CSV); app fetches the full-UK dump (~8,038 stations, all prices) once per 12 h and filters/sorts on-device. Priority guide: P0 = next build, P1 = soon, P2 = later, P3 = maybe/idea. Status: TODO / IN PROGRESS / DONE / BLOCKED. --- ## P0 — Next build - [ ] **Price history archive (start now — warm the data for Trends)** — relay-api appends one daily snapshot (digest-change-triggered, once per calendar day) using the existing `relay_snapshot.py` shape; prune to ~90 days (~18 MB). No UI dependency: accumulating TODAY means the Trends graph launches with weeks of real history instead of an empty chart. Optional first-weeks tweak: hourly snapshots to build history faster, then settle to daily. Later add `/api/v1/history?station=&fuel=&days=` (keyless, X-Client-tagged like `/api/v1/stations`). ## P1 — Soon - [x] **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 (see P3 data mirror). Interim: the relay on Tailscale (100.120.217.98:8789) is reachable off-LAN for testing; relay will move to a permanent box or the GitHub mirror long-term. *DONE — radius-scoped lookup `037c241` (real-data regression: GULF HISTON 100.9p 132 mi away), per-fuel literal-word shortcuts `e55078c`, names = app words `a3b6f37`, plus Directions-to-cheapest `caadf18` + spoken hand-off `aed99e3`.* - [ ] **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. ## P2 — Later - [ ] **Shortcuts: station price lookup** — `StationEntity` + `EntityQuery` so Shortcuts/Siri pick a station by name; `ReturnsValue` enables user automations ("get cheapest diesel → notify"). - [x] **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 no `StationEntity` dependency.* - [ ] **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) + `.appEntityIdentifier` on station rows (collection annotation on the List) + `NSUserActivity` on the station detail/map + App Schema conformance (optional `IndexedEntity` for Spotlight semantic search). Verify annotations with AppIntentsTesting (P3). Off-screen/locked requests already route to `CheapestFuelIntent` correctly; this closes the frontmost gap. WWDC26: "Build intelligent Siri experiences with App Schemas" + "Explore advanced App Intents features". - [ ] **Lock screen widgets** — new `FuelPriceLockScreen` kind (fuel-only config intent, reuses `FuelPriceTimelineProvider`): `accessoryRectangular` (cheapest station + price + distance), `accessoryCircular` (price ring), `accessoryInline` ("⛽ 139.9p"). Provider + `containerBackground` already exist; must branch the view for accessory families (current `else` renders `stationList`). 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). - [ ] **Pull-to-refresh spinner state** — surface refresh in-flight state (currently `refreshable` fires but no visible progress in the row list). - [ ] **Offline first-run** — first launch without network currently shows empty states; bundle a sample dump so the app demos 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. - [ ] **Production data mirror (GitHub)** — relay pushes the synced dump (`stations.json` + `latest.json` version pointer) to a PUBLIC GitHub repo; the app fetches `raw.githubusercontent.com` with the LAN relay as fallback source. Public repo required for App Review (reviewer must fetch the data themselves); HTTPS/ATS fine; plain JSON data is not "remote code" (2.5.2-safe); bundle a small fallback dump so the app never demos empty. Needs GitHub PAT (aptonline account). - [ ] **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. - [ ] **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. - [ ] **Share sheet** — share "Cheapest Unleaded near me" as text/image card. - [ ] **Watch app** — glanceable cheapest-price complication (WatchKit + shared app-group store; watch target would need to be added to the project). - [ ] **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) - [x] 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 `.env` only, never in the IPA - [x] Widget configuration — per-widget fuel/sort/distance via `WidgetConfigurationIntent` (`AppIntentConfiguration`), distance options as `WidgetDistance` AppEntity (`26e724f`); small widget gained Favourites (`d0bd4e7`) + `.systemMedium` (`b94f769`) - [x] Unit test target for the app — FuelBoardShared XCTest target, 37 tests (title sanitizer, decode price guard, RAG thresholds, cheapest/closest baseline, miles↔km) - [x] Backlog hygiene — implemented items marked DONE, obsolete pruned - [x] Sync retry with exponential backoff — relay-api retries failed syncs 15s→15min cap; `/health` exposes retry state (`6a58275`) - [x] England-wide full-UK dump, on-device filtering (relay `142061b`, app `cf9765b`) - [x] Cache-first, 12 h gate, pull-to-refresh only - [x] Distance filter 5/10/15 miles, local-only changes (no re-fetch) - [x] Closest mode = whole country, distance picker disabled - [x] Delta/TOP baseline = cheapest-within-radius in both modes - [x] Three-layer price sanitisation (relay band 50–500p / app decode guard / title-case) - [x] Title-case station names (chunk-aware sanitizer) - [x] Native large-title navigation (Mail-style collapse) — `313fe84` - [x] E10/E5 bracket labels in picker + descriptions — `05f3ef0` - [x] Gitea remotes created + pushed (`apt/fuelboard`, `apt/fuelboard-relay`) - [x] Brand logos: proper 2x/3x assets, 75% smaller (315 KB → 79 KB) - [x] Security audit (see SECURITY.md) - [x] App Store approval gotcha review (see APPSTORE.md)