renovate-bot 4cea16a974
Build and Push Docker Images / test-frontend (push) Successful in 7m54s
Build and Push Docker Images / test-backend (push) Successful in 12m58s
Build and Push Docker Images / build-backend (push) Successful in 20m12s
Build and Push Docker Images / e2e-frontend (push) Failing after 6m56s
Build and Push Docker Images / build-frontend (push) Successful in 39m59s
Merge pull request 'chore(deps): update dependency @playwright/test to v1.64.1' (#331) from renovate/playwright-(package-+-ci-image) into master
2026-10-16 11:04:38 +11:00
2024-10-26 22:41:43 +11:00
2026-10-07 19:59:11 +11:00
2025-08-12 14:35:34 +10:00

Fuel Wizard

This is the source code mono-repository for the Fuel Wizard front end and server/API applications.

What is Fuel Wizard?

Fuel Wizard (https://fuelwizard.au) is a free, open source web application, which provides simple, useful fuel price analytics to everyone. We currently plan to integrate with all of Australia's state government-provided fuel price data sources. Currently, we integrate with NSW, ACT, TAS, VIC and WA; more will be added in the future. If you are interested in working on an integration for a particular state, please reach out!

Why do this?

Fuel Wizard was a response to limitations with state-government provided fuel price applications. Whilst these have been a positive step for empowering consumers with open data, these apps have a long way to go in terms of functionality. Some third-parties have created fuel price applications, but we hope to provide an application which is modern, fast, simple to use, and contains intelligent price analytics, which can help our users to find better deals on fuel.

Features

  • Interactive map of fuel prices, with user-selectable map styles (rendered from self-hosted OpenStreetMap vector tiles)
  • Fuel price history charts and trends
  • Station search and listings
  • Route directions between locations

Architecture

Front End

The front end application is a Next.js 16, TypeScript web app, which uses the Mantine UI library and utilities to display the application data. react-map-gl with MapLibre GL is used for the map views, rendering self-hosted OpenStreetMap vector tiles served from tile.fuelwizard.au (styles are loaded from the server's styles.json and are user-selectable, with "Basic" as the default — see frontend/utils/mapStyles.ts). Hey API (@hey-api/openapi-ts) automatically generates types, an SDK, and TanStack Query helpers for requests to the API, and TanStack Query manages front end data fetching. OSRM (served at osrm.fuelwizard.au) provides the routing API, with osrm-text-instructions generating turn-by-turn directions.

Back End

The back end/API server is an ASP.NET Core (.NET 10), C# web app, which serves a REST API for consumption by the front end, and by third-parties. OpenAPI is used to automatically create API definitions, with a Scalar web interface for viewing and trying out the spec (served at /api-docs in development). Hangfire is used to schedule recurring jobs within the application code, such as fetching the latest NSW fuel prices every 20 minutes. EF Core (with the Npgsql provider) handles data access.

Database

A TimescaleDB instance (PostgreSQL with the PostGIS extension) is used for persistence, and more complex queries of time-series data. The schema is model-first: EF Core migrations are applied automatically on application startup, and the price table is stored as a TimescaleDB hypertable.

Local Development

Environment

cp .env.sample .env

The back end auto-loads the root .env when running in Development — it carries the (public, sandbox) NSW credentials, the optional Victoria consumer ID, and the Western Australia feed switches. Fill in VIC_FUEL_API_CONSUMER_ID= if you have a key (VIC jobs otherwise log a warning and skip). The front end needs no env setup for local development.

Database

A TimescaleDB instance is required to run a local development instance of the application. This can be set up quickly through Docker, using the compose config found at backend/Database/Deployment/compose.yaml:

docker compose up -d

Run the command from the backend/Database/Deployment directory. The container listens on host port 5432 with the default credentials postgres/password, which match the development configuration, so no further setup is needed. You can reference the Timescale docs for more on running the database with Docker here: https://docs.timescale.com/self-hosted/latest/install/installation-docker/ .

The image tag is pinned exactly (in this compose file, the production compose.yaml, and the CI Testcontainers fixture — all three must stay in lockstep) because the TimescaleDB extension lives inside the data directory and never upgrades itself: if the image floats ahead of the volume, Postgres keeps running the old extension library, and continuous-aggregate migrations then fail with only immutable functions supported in continuous aggregate view. If you change the pin and already have a database volume, upgrade the extension in it after pulling:

docker compose pull && docker compose up -d
docker exec -i timescaledb psql -U postgres -d <your-db> -c "ALTER EXTENSION timescaledb UPDATE"
docker restart timescaledb

The ALTER must be the first command of a fresh psql session — the Timescale loader rejects it mid-session.

Back End

You will need the .NET SDK 10 installed on your machine: https://dotnet.microsoft.com/download/dotnet/10.0 .

Build and run the API from the repository root:

dotnet run --project backend/

The API is served at http://localhost:5161. From there you can visit:

  • http://localhost:5161/api-docs — the Scalar interface for browsing the API spec and trying out requests (served in all environments; the machine-readable spec is at /api/openapi/v1.json)
  • http://localhost:5161/hangfire — the Hangfire dashboard, for manually running scheduled jobs (fetching new prices, seeding reference data, etc.)

The Hangfire dashboard is development-only.

Front End

Whilst you should be able to get away with npm or yarn, we strongly recommend using pnpm. We have found this to be the best performing, and most reliable package manager for React projects, and so we tailor these instructions to it.

Install pnpm, as they recommend: https://pnpm.io/installation .

At the root of the front end project (fuel-wizard/frontend), run the install command:

pnpm i

This will download and install all of the required packages, which could take a while, so sit tight until that finishes.

After this has completed, run:

pnpm dev

This starts the Next.js development server, with hot-reload, etc. Navigate to http://localhost:3000 to see the web application.

To review the dev app from another machine on the LAN (phone, laptop), use:

pnpm dev:lan            # auto-detects the LAN IP; or: node scripts/dev-lan.mjs 10.0.2.132

and point the backend at all interfaces so the browser can reach it:

ALLOWED_ORIGINS="http://<lan-ip>:3000" dotnet run --project backend/ --urls http://0.0.0.0:5161

Plain pnpm dev opened via a LAN IP instead shows a half-dead page (nav links render, but hydrated-gated controls like the header fuel type select are missing): Next 16 blocks dev resources for unknown origins, which silently kills hydration. dev:lan sets allowedDevOrigins and the API base URLs so this can't recur.

Other command aliases which will be useful for you are:

Command Description
pnpm dev:lan Dev server bound to 0.0.0.0 with allowedDevOrigins + LAN API base URLs set (see above)
pnpm build Creates a production-optimised build of the Next.js app in the .next directory
pnpm start Starts the production Next.js server
pnpm lint / pnpm lint:fix Runs oxlint on the front end codebase / fixes lint errors
pnpm format / pnpm format:check Formats the codebase with oxfmt / checks formatting
pnpm test / pnpm test:watch / pnpm test:coverage Runs the Vitest test suite (in watch mode / with coverage)
pnpm typecheck Runs the TypeScript compiler to check for type errors
pnpm generate Regenerates the API client in the generated directory (requires the API server running on port 5161)

Tests

The back end test suite (xUnit) lives in backend/Tests/, and can be run from the repository root:

dotnet test backend/Tests/FuelWizardApi.Tests.csproj

The integration tests spin up a TimescaleDB container via Testcontainers, so they require Docker.

The front end also has end-to-end (Playwright) tests in frontend/e2e/. They boot a seeded mock API and the Next.js server automatically — no database, back end build, or Docker required:

cd frontend
pnpm e2e         # functional suites (map, stations, charts, hydration, …)
pnpm e2e:visual  # visual regression baselines

Production Deployment

The stack runs via the root compose.yaml (back end + TimescaleDB + front end on a pinned Docker network):

cp .env.sample .env   # fill in real NSW credentials, DB_PASSWORD, domain URLs, VIC consumer ID
docker compose up -d --build

Deploy with stop/start (EF Core migrations run at startup against the new schema). The app refuses to start without the NSW credential variables.

Upgrading the database image

This section is for minor bumps (same pgXX major, newer -ts or patch — the data volume is reused). For a major bump (new pgXX) the data volume cannot be reused at all — see "Major database version upgrades" below.

The TimescaleDB image tag is pinned exactly in compose.yaml — never float it. Pulling a newer image upgrades the server, but the TimescaleDB extension lives in the data volume and stays on the old version until you upgrade it; Postgres then keeps running the old extension library, and continuous-aggregate migrations fail at startup with only immutable functions supported in continuous aggregate view (this crash-looped production on 2026-10-06). The correct sequence when the pin changes:

docker compose pull && docker compose up -d timescaledb-prod
docker exec -i timescaledb-prod psql -U "$DB_USER" -d <database> -c "ALTER EXTENSION timescaledb UPDATE"
docker restart timescaledb-prod
docker restart backend-prod

The ALTER must be the first command of a fresh psql session — the Timescale loader refuses it mid-session. Check what version a database actually runs with:

docker exec -i timescaledb-prod psql -U "$DB_USER" -d <database> -c "SELECT default_version, installed_version FROM pg_available_extensions WHERE name = 'timescaledb'"

If the two columns disagree, the volume is behind. As a backstop, the back end logs the extension version on every boot and refuses to start with an actionable message if a pending migration needs a newer extension than the database has.

Reverse proxy & HTTPS

In production the stack is normally placed behind a TLS reverse proxy (Caddy, nginx, …) that terminates HTTPS and forwards requests to the back end (:5000) and front end (:3000). Two things depend on telling the app about that proxy:

  • Real client IPs (rate limiting). The API enforces 100 requests/60s per client IP. If ASP.NET Core doesn't know which peer is your proxy, every request looks like it came from the proxy itself, so either the proxy gets throttled on behalf of everyone or — with the default rate-limit exemption covering the proxy's own network — nobody is limited at all.
  • HTTPS scheme detection. Without the proxy's X-Forwarded-Proto header the app thinks it is being served over plain HTTP, which breaks HTTPS redirects and secure-cookie decisions.

Configure this in .env with the two variables from .env.sample:

# proxy running on the same host as the stack (e.g. Caddy on the compose host):
FORWARDING_KNOWN_PROXIES=127.0.0.1
# ...or, if the proxy is a separate box, its address or subnet instead:
# FORWARDING_KNOWN_NETWORKS=10.0.2.50/32

Only peers listed there may assert client IPs/schemes via forwarded headers; anything else sending those headers is ignored. Leave both empty only if you are not running behind a proxy (local dev, or direct HTTP hosting).

Your proxy must forward the standard headers — for Caddy reverse proxy this is the default (X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host are set automatically).

Rate-limit exemptions

The back end also exempts matching peer IPs from rate limiting via RATE_LIMIT_EXEMPT_CIDRS (comma-separated CIDRs; compose.yaml defaults it to the pinned internal Docker network so the Next.js server's own server-side calls are never throttled). Exemptions match the direct connection IP only — never X-Forwarded-For — so keep this list to networks you control.

Major database version upgrades (PG 17 → 18)

Status: done — production runs PG 18.6 / TS 2.30.2 since 2026-10-07. This section and scripts/pg18-upgrade/ are the record of how, and the tooling for the next major (PG 19 once Timescale ships images for it): cut a pgNN-upgrade copy of these scripts, update the two pinned image defaults in cutover.sh and restore-and-verify.sh, and rehearse from rung 1 again.

A PostgreSQL major version bump can't reuse the existing data directory, so the upgrade is dump → restore into a fresh PG18 volume → swap. The tooling lives in scripts/pg18-upgrade/ and the full design (why dump/restore, the V1–V8 verification checks, the rehearsal ladder) is in docs/superpowers/specs/2026-10-06-pg18-major-upgrade-design.md. The three scripts run in this order — each one finishes before the next starts:

# 1. Dump the live database and freeze a manifest of facts to verify against
#    (run where the live DB container runs; the dump is zstd-compressed;
#    the defaults target the dev container — for the production database use
#    --container timescaledb-prod, and export DB_USER if the database's
#    superuser is not `postgres`):
scripts/pg18-upgrade/dump.sh --container timescaledb-prod \
                             --outdir /path/to/dumps \
                             --second-copy /path/to/other/location
# → pre-cutover-<UTC-stamp>.dump + .dump.sha256 + .manifest + .manifest.sha256.
#   ALWAYS pass --second-copy: the dump and its sha256 sidecar land in two
#   locations, and `sha256sum -c <stem>.dump.sha256` proves both are intact
#   before anything touches the new database. The script refuses to run if
#   free disk where the dump lands is below ~3× the live DB size + 10 GiB.

# 2. Rehearsal / verification gate: restore that dump into a throwaway PG18
#    container and re-check every frozen fact (row counts, chunks,
#    continuous aggregates, geography checksums, EF history, and — with
#    --live-api — the four public read paths served by two live back end
#    instances, old DB vs new). Ends with `GATE ALL PASS wall_seconds=N`.
#    (V7 reaches the old DB in the `timescaledb` dev container on port 5433
#    by default; for a production rehearsal set --old-db-port (e.g. 5432) and
#    the V7B_OLD_DB_CONTAINER / V7B_OLD_DB_NAME environment variables):
scripts/pg18-upgrade/restore-and-verify.sh /path/to/dumps/pre-cutover-<stamp> --fresh --live-api

# 3. Production cutover. Check the plan first — it prints the full step list
#    and refuses loudly on any unmet precondition without touching anything
#    (the consent flag is required even for --dry-run; --old-image is only
#    needed if the live compose floats its tag — with an exact pin like the
#    current one, the script reads the old image from the compose itself):
scripts/pg18-upgrade/cutover.sh --prod-compose-dir /path/to/prod/compose \
                                --dump /path/to/dumps/pre-cutover-<stamp> \
                                --dump-second-copy /path/to/other/location \
                                --i-understand-this-touches-prod --dry-run
#    …then, in the upgrade window, the same command with --dry-run removed. It fences writes (stops the back end),
#    takes one final dump, restores it into a NEW volume
#    (volumes/timescaledb-prod-pg18/), re-runs the verification gates, swaps
#    compose to the PG18 image + new volume, and brings the stack back up.

The old PG17 volume is never modified — cutover.sh only ever writes to the new -pg18 volume, so rolling back is a compose revert:

docker compose stop timescaledb-prod && revert compose to ${OLD_IMAGE} + old volume && docker compose up -d timescaledb-prod

(cutover.sh prints the exact one-liner, with the real image pinned in, before it does anything else. Keep the old volume until the new stack has run cleanly for a while.)

Environment variables the scripts read: FW_BACKEND_HEALTH_URL is a legitimate operator override (where the back-end health endpoint listens, if not http://127.0.0.1:5161/api/fuel-types/all — note the standard production compose maps the back end on host port 5000, so a real cutover will likely need FW_BACKEND_HEALTH_URL=http://127.0.0.1:5000/api/fuel-types/all, adjusted to whatever port mapping the live compose uses). Anything named FW_TEST_* (e.g. FW_TEST_DB_SIZE_BYTES) is a test seam for the automated suites only — never set one for a real run; the scripts always size the disk gate from the live database instead.

Attribution

Thank you to these fantastic projects, and their respective communities, without which this application would be much worse!

Back End

Front End

Data & Services

Roadmap (in rough order of priority)

  • Email notifications (WIP)
  • Generalise pagination across back end code (pass in simple Pagination objects to methods, rather than the large signature we have currently)
  • SA, QLD fuel price data (tough to get access to)
  • Trend predictions (we have historical trends; predictions need more data to work with before we start)
  • Trip logging and cost vs. distance tracking (routing is done; saving and analysing trips is not)
  • Request validation via FluentValidation (currently a single hand-rolled page-size validator)
  • Sorting and filtering on the paginated API endpoints (client-side sorting only applies to the loaded page)
  • And more...

License

This program is free software: you can redistribute it and/or modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.

S
Description
Source code for the Fuel Wizard ASP .NET Web API, and Next.js front end.
https://fuelwizard.au
Readme
33 MiB
2 Stars 2 Watchers 0 Forks
Languages
C# 40.6%
TypeScript 26.6%
JavaScript 23.2%
Shell 9.2%
CSS 0.3%
Other 0.1%