Files
fuelboard/BACKLOG.md
T

171 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
- [ ] **GitHub price-history mirror (start now — warm the data for Trends)** — the
relay does the GOV.UK heavy lifting (OAuth token stays in relay `.env`) and
pushes a daily snapshot to a PUBLIC repo; the app reads from GitHub, so this
doubles as the production data source (App Review can't reach the LAN relay) AND
the history archive the Trends graph (P1) reads. Shape: relay writes
`latest.json` (tiny pointer: date, station_count, data_updated, available range)
+ `history/YYYY-MM-DD.json` (full ~200 KB dump) and pushes once/day; the app
fetches `raw.githubusercontent.com/aptonline/fuelboard-data/main/latest.json`,
then only the missing day files for starred (station, fuel) pairs (deterministic
URLs — 404 = no data that day, leave a graph gap, never error). LAN relay stays
the fallback source. Setup: create the PUBLIC repo `aptonline/fuelboard-data`
(gh installed); GitHub PAT (aptonline) lives in relay `.env` only — never in the
repo or IPA. App Review: public repo required (reviewer fetches the data
themselves); HTTPS/ATS fine; plain JSON is not "remote code" (2.5.2-safe); bundle
a small fallback dump so the app never demos empty. Growth ~200 KB/day (~73 MB/yr
— no pruning for years); git history doubles as an audit trail. Optional
first-weeks tweak: hourly pushes to build history faster, then settle to daily.
Accumulating TODAY means the Trends graph launches with weeks of real history
instead of an empty chart.
## 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<Double>` 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.
- [ ] **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 50500p / 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)