# FuelBoard Relay API Keyless proxy for the **official UK Fuel Finder API** (the OAuth-protected GOV.UK service). Sibling of `fuelboard-relay` (which reads the public CSV mirror): this one talks to the real API, joins the station-info and fuel-price endpoints on `node_id`, and normalises to the **same output shape** so the FuelBoard iOS app and widget work against either relay unchanged. Holds the GOV.UK OAuth 2.0 client credentials server-side so the app never embeds a secret (secrets inside an IPA are extractable by anyone). ## Architecture ``` FuelBoard app / widget │ GET http://192.168.1.131:8789/api/v1/stations?fuel=e10&lat=..&lng=.. ▼ FuelBoard Relay API (this repo, runs on the Mac Mini) │ POST /api/v1/oauth/generate_access_token (token cached ~1h) │ GET /api/v1/pfs?batch-number=N (station info, 500/page) │ GET /api/v1/pfs/fuel-prices?batch-number=N (prices, 500/page) ▼ UK Fuel Finder API (official gov.uk open data) ``` - Token refreshed automatically before expiry (60s buffer); reused across the two batched loops (docs: reuse tokens until near expiry). - Both endpoints paginated 500 records/page via `batch-number` until a short page is returned; pages are spaced `FUEL_API_BATCH_SLEEP` (default 4s) apart to stay inside the documented 30 rpm sequential-only rate limit. - Prices arrive as **decimal strings in pence** (`"0120.0000"` = 120.0p); some stations report pounds (values < 2.0 are multiplied by 100). Out-of-band values are dropped (50–500p sane band) so a pounds-denominated column can't poison the nationwide cheapest reference. - Fuel types mapped: `E5`/`E10` → same, `B7`/`B7_STANDARD`/`B7S`/`B7P`/ `B7_PREMIUM`/`B10` → `DIESEL`. Others (`HVO`, `SDV`, …) are ignored — the app tracks exactly E5/E10/DIESEL. - Station snapshot cached `FUEL_CACHE_TTL` (default 300s); a stale cache is refreshed in the background on the next request and stale data is served in the meantime, so a multi-minute sync never blocks the app's 5s timeout. - Base URL is `https://www.fuel-finder.service.gov.uk` (NOT `api.fuelfinder...` — that host doesn't resolve). Overridable per-endpoint via env. - No credentials are exposed to clients — the app calls the relay keyless. ## Setup ```bash python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt pip install pytest # only needed for the test suite cp .env.example .env # then fill in FUEL_API_CLIENT_ID / FUEL_API_CLIENT_SECRET ./scripts/run.sh # serves on FUEL_RELAY_PORT (default 8789) ``` Without credentials, `FUEL_SOURCE=auto` falls back to the public hourly CSV mirror (same parser as `fuelboard-relay`), so the full relay→app path is testable end-to-end before GOV.UK One Login creds exist. `/health` reports the active `source` (`csv` vs `api`). ## Endpoints | Endpoint | Description | |---|---| | `GET /health` | Cache status, active source, last sync, failure counters | | `GET /api/v1/stations` | All stations with prices (keyless) | | `GET /api/v1/stations?fuel=e10&lat=53.72&lng=-1.85&radius=10&limit=10` | Filter by fuel, radius around a point, sorted nearest-first | | `GET /api/v1/stations?fuel=diesel` (no lat/lng) | Sorted cheapest-first | Fuel grades: `e10`, `e5`, `diesel`. Output contract identical to `fuelboard-relay` — the app can switch relays by changing the base URL only. ## Getting credentials 1. Sign in at with GOV.UK One Login. 2. Register an application → get `client_id` + `client_secret`. 3. Put them in `.env` (never in git, never in the app). ## Fair use notes (from the Fuel Finder developer guideline) - Refresh at least every 5 minutes — we do (cron-style refresh on request + TTL cache). - Serve data unmodified — this relay only filters/sorts, never alters prices. - Keep timestamps — `price_updated` is passed through untouched. - Rate limits: ~100 rpm documented — batched loops issue ~2 requests per page (info + prices), comfortably inside the limit on a 5-minute cadence. ## Tests ```bash python -m pytest tests/ -q ``` Covers price-string parsing (pence, pounds, out-of-band), the node_id join, fuel-type mapping, and CSV-fallback parsing — against the documented API shapes, no network required. ## Switching the app Point `Shared/FuelPriceProvider.swift`'s `RelayFuelProvider.baseURL` at `http://192.168.1.131:8789` (API relay) instead of `:8788` (CSV relay). No other app change needed — the station JSON is identical in shape.