Files
fuelboard/BACKLOG.md
FuelBoard Contributor 6cbb083208 Retire the Best Value proximity alert option; move Key sheet copy into a tested model
- Alerts: drop the alert-type picker and the Best Value alert path, so
  proximity alerts always follow the cheapest station. A stored `bestValue`
  selection now falls back to cheapest when loaded.
- Key sheet: the price-rating, savings, station-label and favourite wording
  comes from `KeySheetContent` in Shared/FuelSavings.swift — a Foundation-only
  model with tests that pin the exact meaning of each indicator — and the
  sheet is built from that model rather than inline strings.
- BACKLOG: record the plan to restore Best Value proximity alerts behind one
  shared evaluator (user-location baseline, a single disclosed qualification
  rule, and real/Test alert parity).
- Bump CURRENT_PROJECT_VERSION to 29.
2026-10-18 17:16:12 +01:00

18 KiB
Raw Permalink Blame History

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 fallback has been removed from the app binary; the public GitHub mirror is the only live network data source.

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, hostname fuelboard-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 8789 source: api, 8,024 stations, ~2 min first sync, 0 failures); source at /root/workspace/fuelboard-{relay,relay-api} (venvs + pip, .env chmod 600); deploy key fuelboard_deploy + github-fuelboard-data ssh config + fuelboard-data clone at /root/workspace/fuelboard-data (origin = ssh alias, green + synced); mirror_push.py → /root/scripts/mirror_push.py (RELAY line already 127.0.0.1:8789); systemd fuelboard-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 (commit 735a292, latest.json data_updated 16:50:53Z); Mac launchd com.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 watchdog relay_price_watchdog.py now fetches via SSH (~/.ssh/fuelboard_ct → /root/scripts/watchdog_fetch.py). Logs via journalctl -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 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. 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 + EntityQuery so 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 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).

  • Restore Best Value proximity alerts after fixing evaluation parity — the option is intentionally removed from the Alerts tab and legacy selections fall back to Cheapest station until this is implemented. Root cause: the live geofence path calculates journeys from the entered station, making that station its own zero-saving baseline and therefore unable to clear any positive minimum saving, while Test alert calculates from the user's location and can pass. Future implementation must obtain a fresh user fix on region entry; fetch, radius-filter, choose the closest eligible baseline and calculate every journey from that user location; then notify only when the entered station is the best qualifying candidate. Real and Test alerts must call one shared pure evaluator. Apply one disclosed qualification rule everywhere (real alert, Test alert and BEST VALUE badge): remove the live path's hidden extra £0.50 estimate margin, compare the same penny-rounded amount shown to the user while retaining raw precision for ranking, and pass the current threshold explicitly into station rows instead of reading persistence per row. Regression coverage: entered station can qualify, a better nearby candidate blocks it, real/Test parity, exact and display-rounding boundaries, estimated/confirmed parity, immediate threshold changes, and below-threshold rejection.

  • 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 change picker (e.g. 0.5p / 1.0p / 2.0p), plus a Check now action 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) vs Frequent (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 refreshable fires but no visible progress in the row list).

  • Offline first-run — bundled REAL 8,022-station dump (FuelBoardDump, refreshed by scripts/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. Prefer ONE FuelBoard Live Activity with a mode selector (e.g. Nearby cheapest vs Foresight / next ahead) rather than trying to run two simultaneous FuelBoard activities; iOS can support multiple activities, but FuelBoard's current implementation is single-activity and the compact surfaces are too constrained for two competing driving modes anyway. 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 favourite as the default low-noise mode, but explore an optional All favourites mode 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 favourite summary card, top few favourites for that fuel, last-updated time, and a prominent Check now action 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 Directions and Open 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): MirrorFuelProvider fetches CURRENT prices from raw.githubusercontent (latest.json → history/.json, app-group day-cache) → GitHub mirror only everywhere (alert checks + widget too); no LAN relay fallback, private IP, Local Network permission, or diagnostic network beacon ships in the app binary. Bundled REAL dump (FuelBoardDump dataset, refreshed by scripts/refresh_bundled_dump.sh) is the app's no-network last resort. Local Network permission removed entirely — data page is informational, replay on cellular can no longer hang. 107+ 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 .env only, never in the IPA
  • Widget configuration — per-widget fuel/sort/distance via WidgetConfigurationIntent (AppIntentConfiguration), distance options as WidgetDistance AppEntity (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; /health exposes retry state (6a58275)
  • England-wide full-UK dump, on-device filtering (relay 142061b, app cf9765b)
  • 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)