Files

249 lines
16 KiB
Markdown
Raw Permalink 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 — 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
- [x] **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).**
- [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 (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`.
- [x] **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").
- [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).
- [ ] **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 46 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).
- [x] **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)
- [x] **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/<day>.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 flag
`fuelboard.relayFallback` (Settings → Debug, off by default); bundled REAL
dump (`FuelBoardDump` dataset, refreshed by `scripts/refresh_bundled_dump.sh`)
is the app's no-network last resort. `FuelBeacon` fire-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.
- [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)