From c3169cd15e98b412070d733be28e08563d7ce106 Mon Sep 17 00:00:00 2001 From: AKolenda <91154044+AKolenda@users.noreply.github.com> Date: Sun, 27 Sep 2026 15:13:13 -0600 Subject: [PATCH] Link every download to the latest GitHub release The website, handbook and README link the Android APK and the source archive at github.com/AKolenda/openfuel/releases/latest/download/, and the site no longer bundles them under /downloads. A new release-assets command collects the APK, its checksum and the source archive in dist/release for a GitHub release; the build still packages the source archive for the checks. --- .env.example | 2 +- README.md | 4 +-- apps/docs/pages.js | 2 +- apps/docs/pages.json | 10 +++--- apps/web/_headers | 5 --- apps/web/designs/variant-b/index.html | 4 +-- apps/web/index.html | 6 ++-- apps/web/preview/index.html | 4 +-- docs/BUILD_STATUS.md | 3 +- docs/HOSTING.md | 22 ++++++++++---- docs/RUNNING_COSTS.md | 5 +-- tests/browser_checks.py | 9 +++--- tests/test_foundation.py | 2 +- tests/test_repository.py | 2 +- tools/project.py | 44 +++++++++++++-------------- tools/public_config.py | 3 +- 16 files changed, 67 insertions(+), 60 deletions(-) diff --git a/.env.example b/.env.example index 864cc9f..86b8c71 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ # The builder reads only the keys in this file; never store admin credentials here. OPENFUEL_PUBLIC_ENV=development OPENFUEL_PUBLIC_API_BASE_URL=/api/v1 -OPENFUEL_PUBLIC_SOURCE_URL=/downloads/openfuel-source.zip +OPENFUEL_PUBLIC_SOURCE_URL=https://github.com/AKolenda/openfuel/releases/latest/download/openfuel-source.zip # A built static site does NOT read its host's environment after deployment. # Changing these values requires a fresh site build. # Optional donation page (HTTPS) for the database and map costs. Without it no donate diff --git a/README.md b/README.md index d67c746..382cdf0 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ SwiftUI app still needs an Apple SDK build and simulator verification on a Mac. - [Public repository](https://github.com/AKolenda/openfuel) - [Website](https://openfuel.ca/) and [station map](https://openfuel.ca/preview/) -- [Android APK](https://openfuel.ca/downloads/openfuel-android.apk) — development build, Android 8+ -- [API health](https://openfuel.ca/api/v1/health) and [source download](https://openfuel.ca/downloads/openfuel-source.zip) +- [Android APK](https://github.com/AKolenda/openfuel/releases/latest/download/openfuel-android.apk) — development build, Android 8+ +- [API health](https://openfuel.ca/api/v1/health) and [source download](https://github.com/AKolenda/openfuel/releases/latest/download/openfuel-source.zip) Allow location when you enter the app to find nearby stations. If permission is unavailable, search a Canadian city or choose an area on the map. The app uses diff --git a/apps/docs/pages.js b/apps/docs/pages.js index fdad839..801077c 100644 --- a/apps/docs/pages.js +++ b/apps/docs/pages.js @@ -1,2 +1,2 @@ /* SPDX-License-Identifier: AGPL-3.0-only; generated from pages.json. */ -window.OPENFUEL_PAGES=[{"id":"start","title":"Get started","group":"Start here","description":"Open the real station map, install Android, or run the Expo project.","body":"

Try OpenFuel

OpenFuel is an open-source fuel map with a working web app, native Android app, React Native Expo project and SwiftUI source. The shared Cloudflare database contains real OpenStreetMap station locations.

Open the app Download Android APK

Real stations, community pump prices

Allow location to find stations near you, or search a Canadian city. Prices are blank until someone reports what they actually saw at the pump. Community reports are public and unverified. Station and price coverage may be incomplete.

Run locally

npm install\nnpm run dev

The local development database stays on your computer. Follow the root README for station data import and mobile build commands.

Explore the project

"},{"id":"repo","title":"Repository layout","group":"Start here","description":"One monorepo for web, Android, Expo, iOS and the Cloudflare API.","body":"

The source tree

apps/web/          Website + real station web app\napps/docs/         Handbook UI and content\napps/android/      Kotlin / Jetpack Compose\napps/expo/         React Native / Expo Go\napps/ios/          SwiftUI + Foundation core + XcodeGen\nservices/live/     Current Cloudflare Worker + D1\nservices/edge/     Earlier Worker references\nservices/api/      Earlier FastAPI / SQLite reference\nservices/registry/ Review model and PostgreSQL draft\npackages/         Shared contracts, data and design assets\ntools/            Build, import and verification commands\n.github/workflows/ CI and native build jobs

One source for each application

Each platform keeps its own build system and shares the public station API. One change can update the API, website and mobile clients together. Do not create nested Git repositories in the apps.

The earlier synthetic fixtures and interface studies are retained as historical design and test resources. The live web route reads real station data and rejects a sample API response.

"},{"id":"website","title":"Website & documentation","group":"Build","description":"A real Leaflet map, browser location and Canadian city search.","body":"

Edit the source

The landing page is in apps/web/. The working map is in apps/web/preview/. Documentation content is apps/docs/pages.json; the build generates pages.js.

Build the site

npm run build\nnpm run dev

The web app and API share an origin. A plain static server can render the shell but needs the API for city search and station lookup. Website /, map /preview/ and handbook /docs/ ship together, with the station snapshot as static files under /data/stations/ for the Worker.

To show donate links, set OPENFUEL_PUBLIC_DONATE_URL (HTTPS) in .env before building. Without it no donate link appears.

Use the map

On entry, the browser asks for location. The map shows the returned device coordinates and accuracy. If location is denied or unavailable, search a Canadian city, enter latitude and longitude, or move the map and choose Search this area. The app never substitutes a fictional current location.

Leaflet 1.9.4 and MapLibre GL JS 5.24.0 are bundled locally; MapLibre loads when the base map starts. The base map is OpenFreeMap vector tiles drawn in OpenFuel’s style from packages/map-style. Without WebGL, or if OpenFreeMap refuses its tiles, OpenStreetMap raster tiles are used instead. Tiles load for the viewed area with visible attribution and normal browser caching. No bulk download or offline tile feature is provided.

If OpenFuel’s database reaches its free daily limit, the map keeps saved prices on screen under a notice until live prices return after midnight UTC.

"},{"id":"android","title":"Android development","group":"Build","description":"Install the native APK or develop with React Native and Expo Go.","body":"

Install Android

Download Android APK

This is a development APK, not a Play Store release. Android may ask you to allow installation from your browser. Allow location in the app, or use a manual area. Real station coordinates are shared with the web app; missing prices stay unknown until reported.

Use Expo Go

The apps/expo/ project provides a React Native implementation for Expo Go. Follow its README to install dependencies, start Metro and open the project on your phone. Your phone must be able to reach the development server. Expo source and the compiled native Android APK are separate deliverables.

Native development

The Kotlin / Jetpack Compose project lives in apps/android/. Use its Gradle wrapper and README for build and device commands. The public API URL is safe to ship in the app; account credentials and signing secrets are not.

Verification

Check the root README and evidence files for the exact APK build and emulator runs. An emulator test does not replace testing location and navigation on your own phone.

"},{"id":"ios","title":"iOS development","group":"Build","description":"SwiftUI, MapKit and the current live station API; a Mac is required for Apple SDK and device verification.","body":"

Live SwiftUI source

The active app uses MapKit, foreground CoreLocation, Canadian city search and the current Cloudflare API. Stations without reported prices remain visible. Lists and map pins use direct remote brand logos with initials as a fallback. A compact control row and fully hideable results sheet preserve map space.

OpenFuelCore validates live station responses, report receipts and cached areas. The last approximate area loads before a fresh location or network request completes. Earlier sample fixtures remain separate historical tests.

swift test --package-path apps/ios/OpenFuelCore\n# On a Mac with Xcode + XcodeGen:\npython3 tools/project.py ios-build

Build on a Mac

XcodeGen reads apps/ios/project.yml, including public xcconfig inputs and the UI-test target. No signing credentials are embedded. Follow apps/ios/README.md when Xcode is available.

Verified here

The portable Swift tests and read-only live API checks pass on Linux. SwiftUI files passed syntax parsing. Apple SDK compilation, MapKit rendering, permission dialogs, iPhone layout and a signed IPA still require macOS/Xcode; these have not been verified.

Current build status · Xcode configuration

"},{"id":"api","title":"API & contracts","group":"Build","description":"Nearby real stations, Canadian city search and public unverified reports.","body":"

Shared station API

EndpointUse
GET /api/v1/healthService and database health
GET /api/v1/stations?lat=53.54&lon=-113.49&radius=10000&fuel=regularNearby real stations; radius is metres
GET /api/v1/geocode?q=EdmontonCanadian city search
GET /api/v1/regionsSnapshot coverage and attribution
POST /api/v1/reportsA price actually observed at a station

Live responses identify mode: live and is_demo: false. Station IDs have the form osm-node-123 or osm-way-123. Prices are nullable integer thousandths of CAD per litre: a value of 1499 means $1.499/L, displayed as 149.9 ¢/L. This is a unit example, not a reported pump price.

Station locations, city search and regions come from the bundled snapshot; D1 holds current prices and reports. Station answers carry Cache-Control: private, max-age=15, city search public, max-age=86400, regions public, max-age=3600 and health no-store. Prices can be about 15 seconds behind a new report on other Worker instances.

Price reporting

A request contains station_id, fuel_type, price_milli, a random client_id and a unique request_id. Reuse the same request ID when retrying an unconfirmed submission. Only show a report as accepted after the service confirms its ID, station, fuel and price. Rate limits and validation apply.

Daily database limit

When OpenFuel’s daily database budget is spent, the API answers HTTP 503 with {\"error\":\"spending_cap\",\"reason\":…,\"scope\":\"all\"|\"reports\",\"resets_at\":…,\"message\":…,\"donate_url\":…} until midnight UTC. scope: all pauses database reads: searches still answer for areas whose prices the Worker already holds. scope: reports pauses new reports only. When Cloudflare’s own daily request limit is reached, Cloudflare answers HTTP 429 with error 1027: an HTML page, or JSON with error_code 1027 and error_name workers_daily_limit when the client sends Accept: application/json. Treat either like scope: \"all\" until midnight UTC and keep saved stations on screen. Other Cloudflare 429s, such as 1015 rate limiting, are not the daily limit.

Reports are public, anonymous and unverified. Only submit prices actually checked at that station. Run integration tests against local D1; never seed made-up pump prices into the public database.

Earlier architecture

The FastAPI/SQLite service and PostgreSQL/PostGIS registry drafts remain as earlier reference work. They are separate from the deployed Cloudflare API.

Current live API contract · OpenAPI JSON

"},{"id":"parity","title":"Native visual parity","group":"Build","description":"Historical design references and honest native verification boundaries.","body":"

Platform behavior and design references

The current web app, Kotlin Android app and Expo project use real station coordinates. The earlier six-station fixture and illustrated map remain historical design/test resources. SwiftUI source now uses the live API and MapKit; Apple SDK compilation and iPhone verification remain outstanding.

Use evidence from each actual app

Browser screenshots verify browser rendering. Native screenshots must come from the Android app or iOS app being evaluated. JavaScript export proves bundling, not Expo device behavior. Do not claim pixel parity or an iPhone build from shared source alone.

Review map interaction, location permission denial, missing-price stations, reporting, cache age, keyboard behavior and large text on the intended device.

Current build status · Historical visual acceptance requirements

"},{"id":"state","title":"Station lifecycle","group":"Data & community","description":"Additions, corrections and closures stay separate from a raw vote count.","body":"
Current prototype

The working app uses a Cloudflare Worker, static station files and D1 for real station locations and unverified community prices. PostgreSQL/PostGIS and Supabase material below describes the retained registry foundation, not a prerequisite for this prototype.

What the included policy does

The tested model lives in services/registry/policy.py. Three proposal-scoped attestations raise triage priority; they do not publish a station. New records need checked evidence and review. Permanent closure requires two distinct reviewer inputs.

Reviewer strings are not authentication.

The production service still needs actual authorization, source verification, coordinated-abuse resistance and prevention of self-review. The model is not a deployed moderation API.

\n

Preserve station identity

A status change or reopening retains history and version checks. Old prices do not mean a station is closed. Nearby records are duplicate candidates, not automatic merges. Reviewed changes append public history; private reviewer/attester details are not exported.

PostgreSQL is the next data layer

The included schema is an unexecuted PostgreSQL/PostGIS draft. It is not the database currently running under the reference API. Test the schema, transaction paths and permissions before wiring either native app to it.

Read docs/reference/DATABASE_AND_STATION_LIFECYCLE.md in the source for the detailed original proposal. Imported OSM data, brand artwork and source code have separate licence obligations.

"},{"id":"maps","title":"Map, assets & navigation","group":"Data & community","description":"Real OpenStreetMap stations on an OpenFreeMap base map, with coordinate-based navigation.","body":"

Geography

OpenFuel imports Canadian fuel-station locations from OpenStreetMap. Records can have missing addresses, names or amenities. Their provenance and import date are recorded with the dataset. OpenStreetMap data is available under ODbL and city names from GeoNames under CC BY 4.0.

Interactive maps

The website and Android app draw OpenFreeMap vector tiles with MapLibre inside a Leaflet map, in OpenFuel’s own style: a fork of OpenFreeMap Liberty recoloured after CARTO Voyager. No map key or account is needed. Without WebGL, or if OpenFreeMap refuses its tiles, the map uses OpenStreetMap’s standard raster tiles. Expo uses those raster tiles, and the SwiftUI app uses MapKit.

On the website the credit reads “OpenFreeMap © OpenMapTiles · Style after CARTO Voyager · Data © OpenStreetMap contributors” and stays visible on desktop and mobile. The browser sends its normal user agent and an origin referrer, respects HTTP caching, and requests only tiles for the viewed map. There is no area-download feature.

OpenFreeMap · OpenStreetMap tile policy · OSM licence · GeoNames

Directions

Google Maps and Apple Maps are only used for directions; their links contain the actual selected station coordinates. The destination app handles origin location and route calculation. Straight-line distances in OpenFuel are not road distances or journey times.

Earlier design assets

The repository retains fictional map artwork and sample fixture sets for the earlier design study. They are not used as live station locations or current prices. Third-party brand logos are not bundled into the new web map.

"},{"id":"testing","title":"Build evidence","group":"Operate","description":"Executed checks, explicit unexecuted platform gates, and logs—not inferred release readiness.","body":"

Root checks

python3 tools/project.py check --native-cores --web\nnpm test\npython3 tools/generate_mobile.py --check

Executed output

Still to execute

Xcode/SwiftUI compilation, iPhone simulator screenshots, real-device handoff and the retained PostgreSQL/pgTAP migration path. Android and Cloudflare prototype checks are recorded in the executed output above. A workflow file is not a successful run, and the real map requires actual imported station records, valid coordinates and a working API. Test location coordinates used in browser/emulator checks are explicitly injected for reproducibility, not the operator’s physical location.

The visual gate fails when approved real native screenshots are missing. Native parity · Database checks

"},{"id":"privacy","title":"Privacy & source boundaries","group":"Operate","description":"One-shot location permission, public reports and device-local saved areas.","body":"

Location

The web app requests a one-shot device location on entry. Your browser owns the permission decision. It displays the returned coordinates and accuracy; denial leaves city search and map exploration available. Coordinates go to OpenFuel to query nearby stations. The application does not request background location or track journeys.

Network requests

Cloudflare serves the website and public API. OpenFreeMap receives requests for the viewed map area (OpenStreetMap's tile servers if WebGL or OpenFreeMap is unavailable), including normal IP/browser metadata and the site origin. Canadian city search uses a GeoNames index through the OpenFuel API. Choosing directions opens your map provider with the selected station coordinates.

Local data

The browser stores up to four searched-area station snapshots, favourites, and an anonymous reporting identifier. Area keys round coordinates to two decimals. The browser’s HTTP cache may also keep recent API answers, whose URLs contain the searched coordinates. Last-returned device position is held in memory for this session. Clear browser data in the map’s Privacy panel.

When a connection drops, cached station details remain readable. Tiles are not downloaded for offline use. Unconfirmed report attempts keep an idempotency key; they are only retried when you choose to submit, never silently sent in the background.

Public price reports

A report contains the station ID, fuel grade, price and random identifiers. The service timestamps it. Reports are unverified public observations; no account is required. Report only the price you actually saw, without personal information.

Nearby-search URLs contain coordinates. Automatic Worker invocation logs and tracing are disabled. Hosting and map providers may still receive ordinary network metadata. Complete data-flow notice

"},{"id":"hosting","title":"Cloudflare hosting","group":"Operate","description":"Cloudflare Workers, static assets and D1, without a paid fuel feed.","body":"

A small hosting stack

Cloudflare Workers serves the website, API and static files. The site build writes the bundled OpenStreetMap station snapshot as one static file per 0.5° area, which the Worker reads for nearby searches; city search uses the bundled GeoNames list. Lookups do not depend on a paid geocoder or a runtime Overpass query. D1 stores current prices (read as one row per area), community reports and a daily usage count; its copy of the stations only validates reports. A stations request makes at most one D1 query for prices; now and then a Worker instance also adds its usage to the daily budget table after responding.

The base map comes from OpenFreeMap’s free public service, which is donation-funded with no availability guarantee. OpenStreetMap’s tile servers, used when WebGL is missing or OpenFreeMap refuses, have a best-effort usage policy. Both are separate from Cloudflare. No commercial live pump-price feed is connected.

Cloudflare costs depend on the account’s plan and actual usage. The Worker keeps its own daily D1 budget below the Workers Free limits. Once it is spent, the API answers HTTP 503 spending_cap until midnight UTC, and the website and Android app keep saved prices on screen under a notice. Donate links are optional settings. Running costs, limits and donations

Local development

npm install\nnpm run dev

Follow the root README for current migration and station-import commands. The local database is separate from hosted D1. Use local D1 for test price submissions.

Deploy a fork

Set your own Worker name, account ID, D1 binding and domain in wrangler.jsonc. Credentials remain in your development environment. Apply D1 migrations with npm run db:remote, and seed a changed station snapshot with npm run db:seed:remote, before npm run deploy. Build the mobile APK separately, then build the site to include it in the download folder. The source archive contains the monorepo, not private local credentials or databases.

"},{"id":"environment","title":"Environment variables","group":"Operate","description":"Public clients share an endpoint; the database binding stays in the Cloudflare Worker.","body":"

Client configuration

The web app uses same-origin /api/v1. Its public configuration and source download contain no database passwords. Android compiles a public HTTPS API endpoint, Expo reads EXPO_PUBLIC_API_URL and iOS uses its public Xcode configuration. Any setting shipped to a browser or mobile binary is inspectable.

Donate links are optional; without one no donate link appears. Set OPENFUEL_PUBLIC_DONATE_URL in .env before building the website, OPENFUEL_DONATE_URL as an Android Gradle property or environment variable, or EXPO_PUBLIC_DONATE_URL for Expo. Use an HTTPS URL; the website and Android builds reject anything else.

Worker and database

The root Wrangler configuration binds D1 as DB, the static site as ASSETS (the Worker also reads the station files through it) and the report limiter as REPORT_LIMITER. Worker variables set the daily D1 budget, D1_DAILY_READ_BUDGET (default 4,000,000 rows read) and D1_DAILY_WRITE_BUDGET (default 80,000 rows written), and the donate link in the limit answer, OPENFUEL_DONATE_URL. Cloudflare account authorization stays in your development environment, and D1 access runs only inside the Worker. Browsers and apps never receive an account token.

Local and remote state

npm run dev uses a local database. npm run db:remote applies migrations to the hosted database. A successful local test does not change the hosted deployment.

Earlier configuration

Supabase, PostgreSQL and migration-environment examples remain for the retained registry foundation. They are not required credentials for the current D1 prototype.

"},{"id":"migrations","title":"Database migrations","group":"Operate","description":"One versioned schema path; local checks, staged rollout and explicitly approved production changes.","body":"
Current prototype

The working app uses a Cloudflare Worker, static station files and D1 for real station locations and unverified community prices; its D1 migrations are in services/live/migrations/. PostgreSQL/PostGIS and Supabase material below describes the retained registry foundation, not a prerequisite for this prototype.

One source of schema truth

Only supabase/migrations is active. The old registry SQL is historical provenance. Keep source imports, private evidence, accepted audit events and schema migrations distinct.

supabase start\nsupabase migration new add_station_access_details\n# Edit the new migration; then rebuild LOCAL development only:\nsupabase db reset --local\nsupabase db lint --local --level warning\nsupabase test db

Guarded remote rollout

python3 tools/database.py plan --target staging\npython3 tools/database.py apply --target staging --confirm YOUR_EXACT_STAGING_PROJECT_REF\n# After review, same migration history to production with explicit confirmation.

Protect the production GitHub Environment with required reviewers. Choose one migration deployer, never an automatic second integration. No migration runs inside a website build, phone launch or ordinary API request.

Maintain old installed clients

Add first, backfill in bounded batches, update compatible servers/clients, then remove old fields after a support window. Test real restore points, private storage backups and RLS. No remote reset, automatic sample seed or destructive rollback shortcut.

Execution status: SQL and 20 pgTAP checks are written but have not run against PostgreSQL here.

Full migration and maintenance runbook

"},{"id":"licence","title":"Licence & contributions","group":"Project","description":"AGPL-3.0-only for first-party code; data and third-party rights remain separate.","body":"

Project licence

The requested first-party code and documentation licence is AGPL-3.0-only. The full unmodified text is in the root LICENSE. Real OpenStreetMap station data is ODbL. GeoNames city search data is CC BY 4.0. Leaflet is BSD-2-Clause, MapLibre GL JS is BSD-3-Clause and its Leaflet binding is ISC. The map style is a fork of OpenFreeMap Liberty: its code is BSD-3-Clause and its design CC BY 4.0. Original historical fictional fixtures/map retain CC0. Logos, dependency assets and imported datasets are not relicensed by our code licence.

Read the full AGPL licence · Read the notices

\n

Preserve the provenance

Earlier uploaded copies used MPL for client code and AGPL-or-later for some services. Their original scope notes are retained. Those past grants to recipients are not retroactively revoked. This source merge claims no authority over third-party marks or other owners’ artwork.

\n

Contribute in one place

Change the canonical component, update its tests and relevant documentation, and run the root checks. A PR can include website, native and contract changes without producing separate ZIP forks. Submit only work you may contribute under the project terms.

Download the source repository

"},{"id":"github","title":"GitHub & release workflow","group":"Project","description":"One public-source monorepo with independent application builds.","body":"

One repository

The project keeps web, native mobile, Expo, API, database migrations and documentation in one repository. Use the root README for the canonical GitHub URL and current release status. The site also offers a complete source archive.

Contribute

Fork the repository, work in the relevant app or service, and include the checks that exercise your change. Use local D1 for report tests; never fill the public database with test pump prices. Do not create nested Git repositories inside the apps.

Builds and releases

Native APK and iOS build workflows are separate from the web deployment. A workflow is not evidence of a successful run; check executed logs. Debug Android downloads are development builds and do not imply a Play Store release. iPhone signing and App Store distribution require a separate release process.

Before a public push, scan every Git ref and the generated source archive. The repository includes a pinned Gitleaks workflow with only exact test/type-annotation exceptions. Initial publication audit and its limits

"}]; +window.OPENFUEL_PAGES=[{"id":"start","title":"Get started","group":"Start here","description":"Open the real station map, install Android, or run the Expo project.","body":"

Try OpenFuel

OpenFuel is an open-source fuel map with a working web app, native Android app, React Native Expo project and SwiftUI source. The shared Cloudflare database contains real OpenStreetMap station locations.

Open the app Download Android APK

Real stations, community pump prices

Allow location to find stations near you, or search a Canadian city. Prices are blank until someone reports what they actually saw at the pump. Community reports are public and unverified. Station and price coverage may be incomplete.

Run locally

npm install\nnpm run dev

The local development database stays on your computer. Follow the root README for station data import and mobile build commands.

Explore the project

"},{"id":"repo","title":"Repository layout","group":"Start here","description":"One monorepo for web, Android, Expo, iOS and the Cloudflare API.","body":"

The source tree

apps/web/          Website + real station web app\napps/docs/         Handbook UI and content\napps/android/      Kotlin / Jetpack Compose\napps/expo/         React Native / Expo Go\napps/ios/          SwiftUI + Foundation core + XcodeGen\nservices/live/     Current Cloudflare Worker + D1\nservices/edge/     Earlier Worker references\nservices/api/      Earlier FastAPI / SQLite reference\nservices/registry/ Review model and PostgreSQL draft\npackages/         Shared contracts, data and design assets\ntools/            Build, import and verification commands\n.github/workflows/ CI and native build jobs

One source for each application

Each platform keeps its own build system and shares the public station API. One change can update the API, website and mobile clients together. Do not create nested Git repositories in the apps.

The earlier synthetic fixtures and interface studies are retained as historical design and test resources. The live web route reads real station data and rejects a sample API response.

"},{"id":"website","title":"Website & documentation","group":"Build","description":"A real Leaflet map, browser location and Canadian city search.","body":"

Edit the source

The landing page is in apps/web/. The working map is in apps/web/preview/. Documentation content is apps/docs/pages.json; the build generates pages.js.

Build the site

npm run build\nnpm run dev

The web app and API share an origin. A plain static server can render the shell but needs the API for city search and station lookup. Website /, map /preview/ and handbook /docs/ ship together, with the station snapshot as static files under /data/stations/ for the Worker.

To show donate links, set OPENFUEL_PUBLIC_DONATE_URL (HTTPS) in .env before building. Without it no donate link appears.

Use the map

On entry, the browser asks for location. The map shows the returned device coordinates and accuracy. If location is denied or unavailable, search a Canadian city, enter latitude and longitude, or move the map and choose Search this area. The app never substitutes a fictional current location.

Leaflet 1.9.4 and MapLibre GL JS 5.24.0 are bundled locally; MapLibre loads when the base map starts. The base map is OpenFreeMap vector tiles drawn in OpenFuel’s style from packages/map-style. Without WebGL, or if OpenFreeMap refuses its tiles, OpenStreetMap raster tiles are used instead. Tiles load for the viewed area with visible attribution and normal browser caching. No bulk download or offline tile feature is provided.

If OpenFuel’s database reaches its free daily limit, the map keeps saved prices on screen under a notice until live prices return after midnight UTC.

"},{"id":"android","title":"Android development","group":"Build","description":"Install the native APK or develop with React Native and Expo Go.","body":"

Install Android

Download Android APK

This is a development APK, not a Play Store release. Android may ask you to allow installation from your browser. Allow location in the app, or use a manual area. Real station coordinates are shared with the web app; missing prices stay unknown until reported.

Use Expo Go

The apps/expo/ project provides a React Native implementation for Expo Go. Follow its README to install dependencies, start Metro and open the project on your phone. Your phone must be able to reach the development server. Expo source and the compiled native Android APK are separate deliverables.

Native development

The Kotlin / Jetpack Compose project lives in apps/android/. Use its Gradle wrapper and README for build and device commands. The public API URL is safe to ship in the app; account credentials and signing secrets are not.

Verification

Check the root README and evidence files for the exact APK build and emulator runs. An emulator test does not replace testing location and navigation on your own phone.

"},{"id":"ios","title":"iOS development","group":"Build","description":"SwiftUI, MapKit and the current live station API; a Mac is required for Apple SDK and device verification.","body":"

Live SwiftUI source

The active app uses MapKit, foreground CoreLocation, Canadian city search and the current Cloudflare API. Stations without reported prices remain visible. Lists and map pins use direct remote brand logos with initials as a fallback. A compact control row and fully hideable results sheet preserve map space.

OpenFuelCore validates live station responses, report receipts and cached areas. The last approximate area loads before a fresh location or network request completes. Earlier sample fixtures remain separate historical tests.

swift test --package-path apps/ios/OpenFuelCore\n# On a Mac with Xcode + XcodeGen:\npython3 tools/project.py ios-build

Build on a Mac

XcodeGen reads apps/ios/project.yml, including public xcconfig inputs and the UI-test target. No signing credentials are embedded. Follow apps/ios/README.md when Xcode is available.

Verified here

The portable Swift tests and read-only live API checks pass on Linux. SwiftUI files passed syntax parsing. Apple SDK compilation, MapKit rendering, permission dialogs, iPhone layout and a signed IPA still require macOS/Xcode; these have not been verified.

Current build status · Xcode configuration

"},{"id":"api","title":"API & contracts","group":"Build","description":"Nearby real stations, Canadian city search and public unverified reports.","body":"

Shared station API

EndpointUse
GET /api/v1/healthService and database health
GET /api/v1/stations?lat=53.54&lon=-113.49&radius=10000&fuel=regularNearby real stations; radius is metres
GET /api/v1/geocode?q=EdmontonCanadian city search
GET /api/v1/regionsSnapshot coverage and attribution
POST /api/v1/reportsA price actually observed at a station

Live responses identify mode: live and is_demo: false. Station IDs have the form osm-node-123 or osm-way-123. Prices are nullable integer thousandths of CAD per litre: a value of 1499 means $1.499/L, displayed as 149.9 ¢/L. This is a unit example, not a reported pump price.

Station locations, city search and regions come from the bundled snapshot; D1 holds current prices and reports. Station answers carry Cache-Control: private, max-age=15, city search public, max-age=86400, regions public, max-age=3600 and health no-store. Prices can be about 15 seconds behind a new report on other Worker instances.

Price reporting

A request contains station_id, fuel_type, price_milli, a random client_id and a unique request_id. Reuse the same request ID when retrying an unconfirmed submission. Only show a report as accepted after the service confirms its ID, station, fuel and price. Rate limits and validation apply.

Daily database limit

When OpenFuel’s daily database budget is spent, the API answers HTTP 503 with {\"error\":\"spending_cap\",\"reason\":…,\"scope\":\"all\"|\"reports\",\"resets_at\":…,\"message\":…,\"donate_url\":…} until midnight UTC. scope: all pauses database reads: searches still answer for areas whose prices the Worker already holds. scope: reports pauses new reports only. When Cloudflare’s own daily request limit is reached, Cloudflare answers HTTP 429 with error 1027: an HTML page, or JSON with error_code 1027 and error_name workers_daily_limit when the client sends Accept: application/json. Treat either like scope: \"all\" until midnight UTC and keep saved stations on screen. Other Cloudflare 429s, such as 1015 rate limiting, are not the daily limit.

Reports are public, anonymous and unverified. Only submit prices actually checked at that station. Run integration tests against local D1; never seed made-up pump prices into the public database.

Earlier architecture

The FastAPI/SQLite service and PostgreSQL/PostGIS registry drafts remain as earlier reference work. They are separate from the deployed Cloudflare API.

Current live API contract · OpenAPI JSON

"},{"id":"parity","title":"Native visual parity","group":"Build","description":"Historical design references and honest native verification boundaries.","body":"

Platform behavior and design references

The current web app, Kotlin Android app and Expo project use real station coordinates. The earlier six-station fixture and illustrated map remain historical design/test resources. SwiftUI source now uses the live API and MapKit; Apple SDK compilation and iPhone verification remain outstanding.

Use evidence from each actual app

Browser screenshots verify browser rendering. Native screenshots must come from the Android app or iOS app being evaluated. JavaScript export proves bundling, not Expo device behavior. Do not claim pixel parity or an iPhone build from shared source alone.

Review map interaction, location permission denial, missing-price stations, reporting, cache age, keyboard behavior and large text on the intended device.

Current build status · Historical visual acceptance requirements

"},{"id":"state","title":"Station lifecycle","group":"Data & community","description":"Additions, corrections and closures stay separate from a raw vote count.","body":"
Current prototype

The working app uses a Cloudflare Worker, static station files and D1 for real station locations and unverified community prices. PostgreSQL/PostGIS and Supabase material below describes the retained registry foundation, not a prerequisite for this prototype.

What the included policy does

The tested model lives in services/registry/policy.py. Three proposal-scoped attestations raise triage priority; they do not publish a station. New records need checked evidence and review. Permanent closure requires two distinct reviewer inputs.

Reviewer strings are not authentication.

The production service still needs actual authorization, source verification, coordinated-abuse resistance and prevention of self-review. The model is not a deployed moderation API.

\n

Preserve station identity

A status change or reopening retains history and version checks. Old prices do not mean a station is closed. Nearby records are duplicate candidates, not automatic merges. Reviewed changes append public history; private reviewer/attester details are not exported.

PostgreSQL is the next data layer

The included schema is an unexecuted PostgreSQL/PostGIS draft. It is not the database currently running under the reference API. Test the schema, transaction paths and permissions before wiring either native app to it.

Read docs/reference/DATABASE_AND_STATION_LIFECYCLE.md in the source for the detailed original proposal. Imported OSM data, brand artwork and source code have separate licence obligations.

"},{"id":"maps","title":"Map, assets & navigation","group":"Data & community","description":"Real OpenStreetMap stations on an OpenFreeMap base map, with coordinate-based navigation.","body":"

Geography

OpenFuel imports Canadian fuel-station locations from OpenStreetMap. Records can have missing addresses, names or amenities. Their provenance and import date are recorded with the dataset. OpenStreetMap data is available under ODbL and city names from GeoNames under CC BY 4.0.

Interactive maps

The website and Android app draw OpenFreeMap vector tiles with MapLibre inside a Leaflet map, in OpenFuel’s own style: a fork of OpenFreeMap Liberty recoloured after CARTO Voyager. No map key or account is needed. Without WebGL, or if OpenFreeMap refuses its tiles, the map uses OpenStreetMap’s standard raster tiles. Expo uses those raster tiles, and the SwiftUI app uses MapKit.

On the website the credit reads “OpenFreeMap © OpenMapTiles · Style after CARTO Voyager · Data © OpenStreetMap contributors” and stays visible on desktop and mobile. The browser sends its normal user agent and an origin referrer, respects HTTP caching, and requests only tiles for the viewed map. There is no area-download feature.

OpenFreeMap · OpenStreetMap tile policy · OSM licence · GeoNames

Directions

Google Maps and Apple Maps are only used for directions; their links contain the actual selected station coordinates. The destination app handles origin location and route calculation. Straight-line distances in OpenFuel are not road distances or journey times.

Earlier design assets

The repository retains fictional map artwork and sample fixture sets for the earlier design study. They are not used as live station locations or current prices. Third-party brand logos are not bundled into the new web map.

"},{"id":"testing","title":"Build evidence","group":"Operate","description":"Executed checks, explicit unexecuted platform gates, and logs—not inferred release readiness.","body":"

Root checks

python3 tools/project.py check --native-cores --web\nnpm test\npython3 tools/generate_mobile.py --check

Executed output

Still to execute

Xcode/SwiftUI compilation, iPhone simulator screenshots, real-device handoff and the retained PostgreSQL/pgTAP migration path. Android and Cloudflare prototype checks are recorded in the executed output above. A workflow file is not a successful run, and the real map requires actual imported station records, valid coordinates and a working API. Test location coordinates used in browser/emulator checks are explicitly injected for reproducibility, not the operator’s physical location.

The visual gate fails when approved real native screenshots are missing. Native parity · Database checks

"},{"id":"privacy","title":"Privacy & source boundaries","group":"Operate","description":"One-shot location permission, public reports and device-local saved areas.","body":"

Location

The web app requests a one-shot device location on entry. Your browser owns the permission decision. It displays the returned coordinates and accuracy; denial leaves city search and map exploration available. Coordinates go to OpenFuel to query nearby stations. The application does not request background location or track journeys.

Network requests

Cloudflare serves the website and public API. OpenFreeMap receives requests for the viewed map area (OpenStreetMap's tile servers if WebGL or OpenFreeMap is unavailable), including normal IP/browser metadata and the site origin. Canadian city search uses a GeoNames index through the OpenFuel API. Choosing directions opens your map provider with the selected station coordinates.

Local data

The browser stores up to four searched-area station snapshots, favourites, and an anonymous reporting identifier. Area keys round coordinates to two decimals. The browser’s HTTP cache may also keep recent API answers, whose URLs contain the searched coordinates. Last-returned device position is held in memory for this session. Clear browser data in the map’s Privacy panel.

When a connection drops, cached station details remain readable. Tiles are not downloaded for offline use. Unconfirmed report attempts keep an idempotency key; they are only retried when you choose to submit, never silently sent in the background.

Public price reports

A report contains the station ID, fuel grade, price and random identifiers. The service timestamps it. Reports are unverified public observations; no account is required. Report only the price you actually saw, without personal information.

Nearby-search URLs contain coordinates. Automatic Worker invocation logs and tracing are disabled. Hosting and map providers may still receive ordinary network metadata. Complete data-flow notice

"},{"id":"hosting","title":"Cloudflare hosting","group":"Operate","description":"Cloudflare Workers, static assets and D1, without a paid fuel feed.","body":"

A small hosting stack

Cloudflare Workers serves the website, API and static files. The site build writes the bundled OpenStreetMap station snapshot as one static file per 0.5° area, which the Worker reads for nearby searches; city search uses the bundled GeoNames list. Lookups do not depend on a paid geocoder or a runtime Overpass query. D1 stores current prices (read as one row per area), community reports and a daily usage count; its copy of the stations only validates reports. A stations request makes at most one D1 query for prices; now and then a Worker instance also adds its usage to the daily budget table after responding.

The base map comes from OpenFreeMap’s free public service, which is donation-funded with no availability guarantee. OpenStreetMap’s tile servers, used when WebGL is missing or OpenFreeMap refuses, have a best-effort usage policy. Both are separate from Cloudflare. No commercial live pump-price feed is connected.

Cloudflare costs depend on the account’s plan and actual usage. The Worker keeps its own daily D1 budget below the Workers Free limits. Once it is spent, the API answers HTTP 503 spending_cap until midnight UTC, and the website and Android app keep saved prices on screen under a notice. Donate links are optional settings. Running costs, limits and donations

Local development

npm install\nnpm run dev

Follow the root README for current migration and station-import commands. The local database is separate from hosted D1. Use local D1 for test price submissions.

Deploy a fork

Set your own Worker name, account ID, D1 binding and domain in wrangler.jsonc. Credentials remain in your development environment. Apply D1 migrations with npm run db:remote, and seed a changed station snapshot with npm run db:seed:remote, before npm run deploy. Downloads are GitHub release assets, not part of the site: build the Android APK, run python3 tools/project.py release-assets, and attach the files it collects to a GitHub release. The source archive contains the monorepo, not private local credentials or databases.

"},{"id":"environment","title":"Environment variables","group":"Operate","description":"Public clients share an endpoint; the database binding stays in the Cloudflare Worker.","body":"

Client configuration

The web app uses same-origin /api/v1. Its public configuration and source download contain no database passwords. Android compiles a public HTTPS API endpoint, Expo reads EXPO_PUBLIC_API_URL and iOS uses its public Xcode configuration. Any setting shipped to a browser or mobile binary is inspectable.

Donate links are optional; without one no donate link appears. Set OPENFUEL_PUBLIC_DONATE_URL in .env before building the website, OPENFUEL_DONATE_URL as an Android Gradle property or environment variable, or EXPO_PUBLIC_DONATE_URL for Expo. Use an HTTPS URL; the website and Android builds reject anything else.

Worker and database

The root Wrangler configuration binds D1 as DB, the static site as ASSETS (the Worker also reads the station files through it) and the report limiter as REPORT_LIMITER. Worker variables set the daily D1 budget, D1_DAILY_READ_BUDGET (default 4,000,000 rows read) and D1_DAILY_WRITE_BUDGET (default 80,000 rows written), and the donate link in the limit answer, OPENFUEL_DONATE_URL. Cloudflare account authorization stays in your development environment, and D1 access runs only inside the Worker. Browsers and apps never receive an account token.

Local and remote state

npm run dev uses a local database. npm run db:remote applies migrations to the hosted database. A successful local test does not change the hosted deployment.

Earlier configuration

Supabase, PostgreSQL and migration-environment examples remain for the retained registry foundation. They are not required credentials for the current D1 prototype.

"},{"id":"migrations","title":"Database migrations","group":"Operate","description":"One versioned schema path; local checks, staged rollout and explicitly approved production changes.","body":"
Current prototype

The working app uses a Cloudflare Worker, static station files and D1 for real station locations and unverified community prices; its D1 migrations are in services/live/migrations/. PostgreSQL/PostGIS and Supabase material below describes the retained registry foundation, not a prerequisite for this prototype.

One source of schema truth

Only supabase/migrations is active. The old registry SQL is historical provenance. Keep source imports, private evidence, accepted audit events and schema migrations distinct.

supabase start\nsupabase migration new add_station_access_details\n# Edit the new migration; then rebuild LOCAL development only:\nsupabase db reset --local\nsupabase db lint --local --level warning\nsupabase test db

Guarded remote rollout

python3 tools/database.py plan --target staging\npython3 tools/database.py apply --target staging --confirm YOUR_EXACT_STAGING_PROJECT_REF\n# After review, same migration history to production with explicit confirmation.

Protect the production GitHub Environment with required reviewers. Choose one migration deployer, never an automatic second integration. No migration runs inside a website build, phone launch or ordinary API request.

Maintain old installed clients

Add first, backfill in bounded batches, update compatible servers/clients, then remove old fields after a support window. Test real restore points, private storage backups and RLS. No remote reset, automatic sample seed or destructive rollback shortcut.

Execution status: SQL and 20 pgTAP checks are written but have not run against PostgreSQL here.

Full migration and maintenance runbook

"},{"id":"licence","title":"Licence & contributions","group":"Project","description":"AGPL-3.0-only for first-party code; data and third-party rights remain separate.","body":"

Project licence

The requested first-party code and documentation licence is AGPL-3.0-only. The full unmodified text is in the root LICENSE. Real OpenStreetMap station data is ODbL. GeoNames city search data is CC BY 4.0. Leaflet is BSD-2-Clause, MapLibre GL JS is BSD-3-Clause and its Leaflet binding is ISC. The map style is a fork of OpenFreeMap Liberty: its code is BSD-3-Clause and its design CC BY 4.0. Original historical fictional fixtures/map retain CC0. Logos, dependency assets and imported datasets are not relicensed by our code licence.

Read the full AGPL licence · Read the notices

\n

Preserve the provenance

Earlier uploaded copies used MPL for client code and AGPL-or-later for some services. Their original scope notes are retained. Those past grants to recipients are not retroactively revoked. This source merge claims no authority over third-party marks or other owners’ artwork.

\n

Contribute in one place

Change the canonical component, update its tests and relevant documentation, and run the root checks. A PR can include website, native and contract changes without producing separate ZIP forks. Submit only work you may contribute under the project terms.

Download the source repository

"},{"id":"github","title":"GitHub & release workflow","group":"Project","description":"One public-source monorepo with independent application builds.","body":"

One repository

The project keeps web, native mobile, Expo, API, database migrations and documentation in one repository. Use the root README for the canonical GitHub URL and current release status. Each GitHub release includes the Android APK and a complete source archive.

Contribute

Fork the repository, work in the relevant app or service, and include the checks that exercise your change. Use local D1 for report tests; never fill the public database with test pump prices. Do not create nested Git repositories inside the apps.

Builds and releases

Native APK and iOS build workflows are separate from the web deployment. A workflow is not evidence of a successful run; check executed logs. Debug Android downloads are development builds and do not imply a Play Store release. iPhone signing and App Store distribution require a separate release process.

Before a public push, scan every Git ref and the generated source archive. The repository includes a pinned Gitleaks workflow with only exact test/type-annotation exceptions. Initial publication audit and its limits

"}]; diff --git a/apps/docs/pages.json b/apps/docs/pages.json index db42996..0a29487 100644 --- a/apps/docs/pages.json +++ b/apps/docs/pages.json @@ -4,7 +4,7 @@ "title": "Get started", "group": "Start here", "description": "Open the real station map, install Android, or run the Expo project.", - "body": "

Try OpenFuel

OpenFuel is an open-source fuel map with a working web app, native Android app, React Native Expo project and SwiftUI source. The shared Cloudflare database contains real OpenStreetMap station locations.

Open the app Download Android APK

Real stations, community pump prices

Allow location to find stations near you, or search a Canadian city. Prices are blank until someone reports what they actually saw at the pump. Community reports are public and unverified. Station and price coverage may be incomplete.

Run locally

npm install\nnpm run dev

The local development database stays on your computer. Follow the root README for station data import and mobile build commands.

Explore the project

" + "body": "

Try OpenFuel

OpenFuel is an open-source fuel map with a working web app, native Android app, React Native Expo project and SwiftUI source. The shared Cloudflare database contains real OpenStreetMap station locations.

Open the app Download Android APK

Real stations, community pump prices

Allow location to find stations near you, or search a Canadian city. Prices are blank until someone reports what they actually saw at the pump. Community reports are public and unverified. Station and price coverage may be incomplete.

Run locally

npm install\nnpm run dev

The local development database stays on your computer. Follow the root README for station data import and mobile build commands.

Explore the project

" }, { "id": "repo", @@ -25,7 +25,7 @@ "title": "Android development", "group": "Build", "description": "Install the native APK or develop with React Native and Expo Go.", - "body": "

Install Android

Download Android APK

This is a development APK, not a Play Store release. Android may ask you to allow installation from your browser. Allow location in the app, or use a manual area. Real station coordinates are shared with the web app; missing prices stay unknown until reported.

Use Expo Go

The apps/expo/ project provides a React Native implementation for Expo Go. Follow its README to install dependencies, start Metro and open the project on your phone. Your phone must be able to reach the development server. Expo source and the compiled native Android APK are separate deliverables.

Native development

The Kotlin / Jetpack Compose project lives in apps/android/. Use its Gradle wrapper and README for build and device commands. The public API URL is safe to ship in the app; account credentials and signing secrets are not.

Verification

Check the root README and evidence files for the exact APK build and emulator runs. An emulator test does not replace testing location and navigation on your own phone.

" + "body": "

Install Android

Download Android APK

This is a development APK, not a Play Store release. Android may ask you to allow installation from your browser. Allow location in the app, or use a manual area. Real station coordinates are shared with the web app; missing prices stay unknown until reported.

Use Expo Go

The apps/expo/ project provides a React Native implementation for Expo Go. Follow its README to install dependencies, start Metro and open the project on your phone. Your phone must be able to reach the development server. Expo source and the compiled native Android APK are separate deliverables.

Native development

The Kotlin / Jetpack Compose project lives in apps/android/. Use its Gradle wrapper and README for build and device commands. The public API URL is safe to ship in the app; account credentials and signing secrets are not.

Verification

Check the root README and evidence files for the exact APK build and emulator runs. An emulator test does not replace testing location and navigation on your own phone.

" }, { "id": "ios", @@ -81,7 +81,7 @@ "title": "Cloudflare hosting", "group": "Operate", "description": "Cloudflare Workers, static assets and D1, without a paid fuel feed.", - "body": "

A small hosting stack

Cloudflare Workers serves the website, API and static files. The site build writes the bundled OpenStreetMap station snapshot as one static file per 0.5° area, which the Worker reads for nearby searches; city search uses the bundled GeoNames list. Lookups do not depend on a paid geocoder or a runtime Overpass query. D1 stores current prices (read as one row per area), community reports and a daily usage count; its copy of the stations only validates reports. A stations request makes at most one D1 query for prices; now and then a Worker instance also adds its usage to the daily budget table after responding.

The base map comes from OpenFreeMap’s free public service, which is donation-funded with no availability guarantee. OpenStreetMap’s tile servers, used when WebGL is missing or OpenFreeMap refuses, have a best-effort usage policy. Both are separate from Cloudflare. No commercial live pump-price feed is connected.

Cloudflare costs depend on the account’s plan and actual usage. The Worker keeps its own daily D1 budget below the Workers Free limits. Once it is spent, the API answers HTTP 503 spending_cap until midnight UTC, and the website and Android app keep saved prices on screen under a notice. Donate links are optional settings. Running costs, limits and donations

Local development

npm install\nnpm run dev

Follow the root README for current migration and station-import commands. The local database is separate from hosted D1. Use local D1 for test price submissions.

Deploy a fork

Set your own Worker name, account ID, D1 binding and domain in wrangler.jsonc. Credentials remain in your development environment. Apply D1 migrations with npm run db:remote, and seed a changed station snapshot with npm run db:seed:remote, before npm run deploy. Build the mobile APK separately, then build the site to include it in the download folder. The source archive contains the monorepo, not private local credentials or databases.

" + "body": "

A small hosting stack

Cloudflare Workers serves the website, API and static files. The site build writes the bundled OpenStreetMap station snapshot as one static file per 0.5° area, which the Worker reads for nearby searches; city search uses the bundled GeoNames list. Lookups do not depend on a paid geocoder or a runtime Overpass query. D1 stores current prices (read as one row per area), community reports and a daily usage count; its copy of the stations only validates reports. A stations request makes at most one D1 query for prices; now and then a Worker instance also adds its usage to the daily budget table after responding.

The base map comes from OpenFreeMap’s free public service, which is donation-funded with no availability guarantee. OpenStreetMap’s tile servers, used when WebGL is missing or OpenFreeMap refuses, have a best-effort usage policy. Both are separate from Cloudflare. No commercial live pump-price feed is connected.

Cloudflare costs depend on the account’s plan and actual usage. The Worker keeps its own daily D1 budget below the Workers Free limits. Once it is spent, the API answers HTTP 503 spending_cap until midnight UTC, and the website and Android app keep saved prices on screen under a notice. Donate links are optional settings. Running costs, limits and donations

Local development

npm install\nnpm run dev

Follow the root README for current migration and station-import commands. The local database is separate from hosted D1. Use local D1 for test price submissions.

Deploy a fork

Set your own Worker name, account ID, D1 binding and domain in wrangler.jsonc. Credentials remain in your development environment. Apply D1 migrations with npm run db:remote, and seed a changed station snapshot with npm run db:seed:remote, before npm run deploy. Downloads are GitHub release assets, not part of the site: build the Android APK, run python3 tools/project.py release-assets, and attach the files it collects to a GitHub release. The source archive contains the monorepo, not private local credentials or databases.

" }, { "id": "environment", @@ -102,13 +102,13 @@ "title": "Licence & contributions", "group": "Project", "description": "AGPL-3.0-only for first-party code; data and third-party rights remain separate.", - "body": "

Project licence

The requested first-party code and documentation licence is AGPL-3.0-only. The full unmodified text is in the root LICENSE. Real OpenStreetMap station data is ODbL. GeoNames city search data is CC BY 4.0. Leaflet is BSD-2-Clause, MapLibre GL JS is BSD-3-Clause and its Leaflet binding is ISC. The map style is a fork of OpenFreeMap Liberty: its code is BSD-3-Clause and its design CC BY 4.0. Original historical fictional fixtures/map retain CC0. Logos, dependency assets and imported datasets are not relicensed by our code licence.

Read the full AGPL licence · Read the notices

\n

Preserve the provenance

Earlier uploaded copies used MPL for client code and AGPL-or-later for some services. Their original scope notes are retained. Those past grants to recipients are not retroactively revoked. This source merge claims no authority over third-party marks or other owners’ artwork.

\n

Contribute in one place

Change the canonical component, update its tests and relevant documentation, and run the root checks. A PR can include website, native and contract changes without producing separate ZIP forks. Submit only work you may contribute under the project terms.

Download the source repository

" + "body": "

Project licence

The requested first-party code and documentation licence is AGPL-3.0-only. The full unmodified text is in the root LICENSE. Real OpenStreetMap station data is ODbL. GeoNames city search data is CC BY 4.0. Leaflet is BSD-2-Clause, MapLibre GL JS is BSD-3-Clause and its Leaflet binding is ISC. The map style is a fork of OpenFreeMap Liberty: its code is BSD-3-Clause and its design CC BY 4.0. Original historical fictional fixtures/map retain CC0. Logos, dependency assets and imported datasets are not relicensed by our code licence.

Read the full AGPL licence · Read the notices

\n

Preserve the provenance

Earlier uploaded copies used MPL for client code and AGPL-or-later for some services. Their original scope notes are retained. Those past grants to recipients are not retroactively revoked. This source merge claims no authority over third-party marks or other owners’ artwork.

\n

Contribute in one place

Change the canonical component, update its tests and relevant documentation, and run the root checks. A PR can include website, native and contract changes without producing separate ZIP forks. Submit only work you may contribute under the project terms.

Download the source repository

" }, { "id": "github", "title": "GitHub & release workflow", "group": "Project", "description": "One public-source monorepo with independent application builds.", - "body": "

One repository

The project keeps web, native mobile, Expo, API, database migrations and documentation in one repository. Use the root README for the canonical GitHub URL and current release status. The site also offers a complete source archive.

Contribute

Fork the repository, work in the relevant app or service, and include the checks that exercise your change. Use local D1 for report tests; never fill the public database with test pump prices. Do not create nested Git repositories inside the apps.

Builds and releases

Native APK and iOS build workflows are separate from the web deployment. A workflow is not evidence of a successful run; check executed logs. Debug Android downloads are development builds and do not imply a Play Store release. iPhone signing and App Store distribution require a separate release process.

Before a public push, scan every Git ref and the generated source archive. The repository includes a pinned Gitleaks workflow with only exact test/type-annotation exceptions. Initial publication audit and its limits

" + "body": "

One repository

The project keeps web, native mobile, Expo, API, database migrations and documentation in one repository. Use the root README for the canonical GitHub URL and current release status. Each GitHub release includes the Android APK and a complete source archive.

Contribute

Fork the repository, work in the relevant app or service, and include the checks that exercise your change. Use local D1 for report tests; never fill the public database with test pump prices. Do not create nested Git repositories inside the apps.

Builds and releases

Native APK and iOS build workflows are separate from the web deployment. A workflow is not evidence of a successful run; check executed logs. Debug Android downloads are development builds and do not imply a Play Store release. iPhone signing and App Store distribution require a separate release process.

Before a public push, scan every Git ref and the generated source archive. The repository includes a pinned Gitleaks workflow with only exact test/type-annotation exceptions. Initial publication audit and its limits

" } ] diff --git a/apps/web/_headers b/apps/web/_headers index c82d5db..253413d 100644 --- a/apps/web/_headers +++ b/apps/web/_headers @@ -7,8 +7,3 @@ Cache-Control: no-store /preview/sw.js Cache-Control: no-cache -/downloads/* - Cache-Control: public, max-age=300 -/downloads/openfuel-android.apk - Content-Type: application/vnd.android.package-archive - Content-Disposition: attachment; filename="openfuel-android.apk" diff --git a/apps/web/designs/variant-b/index.html b/apps/web/designs/variant-b/index.html index f50fffe..1468b00 100644 --- a/apps/web/designs/variant-b/index.html +++ b/apps/web/designs/variant-b/index.html @@ -5,7 +5,7 @@ Accountable everywhere.

The product is simple. The commitments behind it should be just as clear.

See the map first.

Prices, freshness and a clear next step. No account wall in the preview.

Keep your route to yourself.

The native prototype requests no location or background-driving permissions. Maps opens only when you choose it.

Make the record portable.

Public exports and independently runnable code are the plan—not an exclusive database tied to one company.

Open data should look like open data.

No mystery scores. No invisible corrections. A station record with a source, a status and a history.

Three synthetic records. Downloadable without signing in.
StationFuelCAD ¢/LRecord status
Petro-CanadaRegular142.9Synthetic example
ShellRegular147.9Synthetic example
EssoRegular149.9Synthetic example

Native from -the first tap.

Native Android and iPhone clients, one shared sample database. Start on Android or build the SwiftUI source with Xcode.

AndroidKotlin / Jetpack Compose · installable prototype
Download APK
iPhoneSwift / SwiftUI · source for Xcode
Get iOS source

A few things worth knowing.

The ambition is national. The next step is a carefully tested local launch.

Are these live prices?

The six stations and map are fictional, so these are not real pump prices. Reports you share are stored online and visible in the web and native prototypes. Real coverage and licensed data feeds still need to be added.

Can I try the native apps?

Yes. The Android prototype uses Kotlin and Jetpack Compose and is available as a downloadable APK. The iPhone app uses Swift and SwiftUI; its source is included, and building it requires a Mac with Xcode.

Who can change a station?

The proposed model lets people suggest additions, edits and closures. Evidence is reviewed before changes reach the public map. A vote count alone does not delete a station.

Where is the source?

Download the full repository: website, Android and SwiftUI apps, Cloudflare API, D1 database migrations and tests. A public GitHub repository has not been published yet.

Take the next version for a spin.

Start with the map. Help shape what comes next.

Open the app

Explore OpenFuel

-

Open by design

Website, documentation, Android, iOS, API and registry source live in one AGPL-3.0-only repository. This source archive is separate from the Android APK download.

First-party code: AGPL-3.0-only. Synthetic fixtures: CC0. Third-party rights are separate.

Repository guide · Build evidence · Licence scope

Download the source repository
+the first tap.

Native Android and iPhone clients, one shared sample database. Start on Android or build the SwiftUI source with Xcode.

AndroidKotlin / Jetpack Compose · installable prototype
Download APK
iPhoneSwift / SwiftUI · source for Xcode
Get iOS source

A few things worth knowing.

The ambition is national. The next step is a carefully tested local launch.

Are these live prices?

The six stations and map are fictional, so these are not real pump prices. Reports you share are stored online and visible in the web and native prototypes. Real coverage and licensed data feeds still need to be added.

Can I try the native apps?

Yes. The Android prototype uses Kotlin and Jetpack Compose and is available as a downloadable APK. The iPhone app uses Swift and SwiftUI; its source is included, and building it requires a Mac with Xcode.

Who can change a station?

The proposed model lets people suggest additions, edits and closures. Evidence is reviewed before changes reach the public map. A vote count alone does not delete a station.

Where is the source?

Download the full repository: website, Android and SwiftUI apps, Cloudflare API, D1 database migrations and tests. A public GitHub repository has not been published yet.

Take the next version for a spin.

Start with the map. Help shape what comes next.

Open the app

Explore OpenFuel

+

Open by design

Website, documentation, Android, iOS, API and registry source live in one AGPL-3.0-only repository. This source archive is separate from the Android APK download.

First-party code: AGPL-3.0-only. Synthetic fixtures: CC0. Third-party rights are separate.

Repository guide · Build evidence · Licence scope

Download the source repository

What is ready today

Web: a working map-first app with shared sample prices, reporting, search, filters and saved stations. Cached prices remain available offline after your first visit.

Android: native Kotlin / Compose prototype with shared reports, local cached prices, favourites and bottom sheets. Download the APK to install and test it. It is a development build, not a Play Store release.

iPhone: native SwiftUI source with the same sample stations and cloud API. Build it on a Mac with Xcode. No signed iPhone download or App Store release is available.

Data: Cloudflare Workers and D1 store shared prototype reports. All six station records and the illustrated map are synthetic. No live fuel-price feed or verified nationwide coverage is connected.

openfuelWorking app · shared sample data
\ No newline at end of file diff --git a/apps/web/index.html b/apps/web/index.html index 4fd58d8..42836da 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -1,11 +1,11 @@ openfuel Canada — The everyday route

Your next fill-up. -A clearer choice.

An open-source fuel map for Canada. Find real stations near you, check community pump reports, and help fill the gaps.

Real station locations. Community prices, when reported.

Canadian pricing. Real maps. Community reports.
9:41●●● ▰
OpenFuel showing real OpenStreetMap stations in Edmonton; pump prices stay blank until reported
Real stations in Edmonton. Open the map.
+A clearer choice.

An open-source fuel map for Canada. Find real stations near you, check community pump reports, and help fill the gaps.

Open the appDownload Android APK

Real station locations. Community prices, when reported.

Canadian pricing. Real maps. Community reports.
9:41●●● ▰
OpenFuel showing real OpenStreetMap stations in Edmonton; pump prices stay blank until reported
Real stations in Edmonton. Open the map.
¢/LPrices in Canadian units
EN / FRA bilingual website
Open sourceInspect it. Improve it. Keep it open.

Less to get through. More of what matters.

The map stays in front. The details stay easy to reach. The project stays open to scrutiny.

¢/L

The price, in context.

See when a price was shared. Missing prices stay blank until someone checks the pump.

Built around the map.

A compact station list, familiar controls and bottom sheets that leave room to explore.

+−+

Changes with a history.

Share a pump price and see it on another device. Every community report is clearly marked unverified.

A new station? Make it a shared fact.

We’re designing a review process for openings, corrections and closures. Suggestions become evidence. Verified changes become part of the map.

1

Suggest a change

Pin the station. Describe what changed.

2

Check the evidence

Check sources, duplicates and independent reports.

3

Publish the decision

Keep the record. Allow corrections and appeals.

Native from -the first tap.

A native Android app and an Expo project connect to the same station database. SwiftUI source is also included for iPhone development.

AndroidKotlin / Jetpack Compose · installable prototype
Download APK
iPhoneSwift / SwiftUI · source for Xcode
Get iOS source

A few things worth knowing.

The ambition is national. The next step is a carefully tested local launch.

Are these live prices?

Station locations are real OpenStreetMap records. Prices are unverified reports from people who checked the pump; a missing price stays blank. This is not a commercial live-price feed, and coverage may be incomplete.

Can I try the native apps?

Install the Kotlin / Jetpack Compose Android APK, or run the React Native Expo project in Expo Go. SwiftUI iPhone source is also included; building that app requires a Mac with Xcode.

Who can change a station?

The proposed model lets people suggest additions, edits and closures. Evidence is reviewed before changes reach the public map. A vote count alone does not delete a station.

Where is the source?

Download the monorepo: website, Android, Expo and SwiftUI apps, Cloudflare API, D1 database migrations and tests. The repository guide explains how to run and contribute.

Take the next version for a spin.

Start with the map. Help shape what comes next.

Open the app

Explore OpenFuel

-

Open by design

Website, documentation, Android, iOS, API and registry source live in one AGPL-3.0-only repository. This source archive is separate from the Android APK download.

First-party code: AGPL-3.0-only. OpenStreetMap data: ODbL. GeoNames: CC BY 4.0. Historical synthetic fixtures: CC0. Dependencies retain their own licences.

Repository guide · Build evidence · Licence scope

Download the source repository
+the first tap.

A native Android app and an Expo project connect to the same station database. SwiftUI source is also included for iPhone development.

AndroidKotlin / Jetpack Compose · installable prototype
Download APK
iPhoneSwift / SwiftUI · source for Xcode
Get iOS source

A few things worth knowing.

The ambition is national. The next step is a carefully tested local launch.

Are these live prices?

Station locations are real OpenStreetMap records. Prices are unverified reports from people who checked the pump; a missing price stays blank. This is not a commercial live-price feed, and coverage may be incomplete.

Can I try the native apps?

Install the Kotlin / Jetpack Compose Android APK, or run the React Native Expo project in Expo Go. SwiftUI iPhone source is also included; building that app requires a Mac with Xcode.

Who can change a station?

The proposed model lets people suggest additions, edits and closures. Evidence is reviewed before changes reach the public map. A vote count alone does not delete a station.

Where is the source?

Download the monorepo: website, Android, Expo and SwiftUI apps, Cloudflare API, D1 database migrations and tests. The repository guide explains how to run and contribute.

Take the next version for a spin.

Start with the map. Help shape what comes next.

Open the app

Explore OpenFuel

+

Open by design

Website, documentation, Android, iOS, API and registry source live in one AGPL-3.0-only repository. This source archive is separate from the Android APK download.

First-party code: AGPL-3.0-only. OpenStreetMap data: ODbL. GeoNames: CC BY 4.0. Historical synthetic fixtures: CC0. Dependencies retain their own licences.

Repository guide · Build evidence · Licence scope

Download the source repository

What is ready today

Web: a map of OpenStreetMap data from OpenFreeMap vector tiles in OpenFuel’s style (OpenStreetMap raster tiles as a fallback), browser location permission, Canadian city search, station details, community reporting and saved stations. Cached station details remain readable when your connection drops.

Android: a native Kotlin / Compose app connected to the real station API. Download the APK to install and test it. It is a development build, not a Play Store release.

iPhone: current SwiftUI source uses real stations, MapKit, location and saved areas. Apple SDK build and simulator testing still need a Mac with Xcode. No signed iPhone download or App Store release is available.

Data: real Canadian station locations from OpenStreetMap, Canadian city search from GeoNames, and community prices stored in Cloudflare D1. Prices are unverified and only appear after a report. No nationwide real-time price feed is claimed.

OpenFuelReal station map · community prices
\ No newline at end of file diff --git a/apps/web/preview/index.html b/apps/web/preview/index.html index e8f0d21..416f12d 100644 --- a/apps/web/preview/index.html +++ b/apps/web/preview/index.html @@ -63,7 +63,7 @@
◎

Your next fill-up starts here.

Use your location or search a city to see real fuel stations. Prices appear when someone reports them.

Your browser asks first. No background tracking.

- + @@ -82,7 +82,7 @@

An open road ahead.

-

OpenFuel is a community fuel map for Canada. Station locations come from OpenStreetMap. Pump prices come from people checking the station, and are marked unverified.

What you see

A dash means there is no reported price. Distances are straight-line measurements, not driving distances. Station details may be incomplete; check the price at the pump.

Your location

Your browser asks before sharing your device location. Coordinates are sent to OpenFuel to find nearby stations. We do not track your journey or request background location. You can search a place instead.

Your last area is saved approximately, rounded to about a kilometre, so its station details can appear as soon as you return. The last few station snapshots, saved stations and an anonymous report identifier stay in this browser. A saved area is not a current device location.

Network operators receive normal request metadata. OpenFreeMap receives map tile requests; brand logos load directly from remote image providers. City searches use the GeoNames Canadian place index through our server.

Directions open Google Maps or Apple Maps with the station’s actual coordinates. The map app handles your route and any origin-location permission.

Saved station details remain available when the connection drops. Maps need a network connection; there is no offline tile download.

+

OpenFuel is a community fuel map for Canada. Station locations come from OpenStreetMap. Pump prices come from people checking the station, and are marked unverified.

What you see

A dash means there is no reported price. Distances are straight-line measurements, not driving distances. Station details may be incomplete; check the price at the pump.

Your location

Your browser asks before sharing your device location. Coordinates are sent to OpenFuel to find nearby stations. We do not track your journey or request background location. You can search a place instead.

Your last area is saved approximately, rounded to about a kilometre, so its station details can appear as soon as you return. The last few station snapshots, saved stations and an anonymous report identifier stay in this browser. A saved area is not a current device location.

Network operators receive normal request metadata. OpenFreeMap receives map tile requests; brand logos load directly from remote image providers. City searches use the GeoNames Canadian place index through our server.

Directions open Google Maps or Apple Maps with the station’s actual coordinates. The map app handles your route and any origin-location permission.

Saved station details remain available when the connection drops. Maps need a network connection; there is no offline tile download.

diff --git a/docs/BUILD_STATUS.md b/docs/BUILD_STATUS.md index 589c82a..4dfdeee 100644 --- a/docs/BUILD_STATUS.md +++ b/docs/BUILD_STATUS.md @@ -2,7 +2,8 @@ OpenFuel now uses a real Canadian station directory and community pump reports. The public site is https://openfuel.ca/ and map is https://openfuel.ca/preview/. -Cloudflare serves the website, API, source archive and compact Android APK. +Cloudflare serves the website and API; the source archive and compact Android APK are +GitHub release assets (https://github.com/AKolenda/openfuel/releases). | Check | Current result | | --- | --- | diff --git a/docs/HOSTING.md b/docs/HOSTING.md index a0365c6..c639447 100644 --- a/docs/HOSTING.md +++ b/docs/HOSTING.md @@ -29,15 +29,25 @@ before deploying the Worker that uses them, and seed a changed snapshot before deploying, so the files and the table match. [Running costs](RUNNING_COSTS.md) gives the exact order. -Build Android before the final web build so the APK and checksum included under -`/downloads/` match the current app. The source ZIP uses an explicit allowlist -and excludes credentials, build caches and local database state. The live API -contract is published at `/openapi.json` when generated in `packages/contracts/`. +Downloads are not part of the site. The website links to the latest +[GitHub release](https://github.com/AKolenda/openfuel/releases), which attaches +`openfuel-android.apk`, its `.sha256` checksum and `openfuel-source.zip`. To publish +one, build Android, run `python3 tools/project.py release-assets` (it collects the +three files in `dist/release/`), and create the release with those exact file names, +not marked as a pre-release, so the `releases/latest/download/` links resolve to it: + +```sh +gh release create v0.3.2 dist/release/* --title "OpenFuel 0.3.2" --notes "..." +``` + +The source ZIP uses an explicit allowlist and excludes credentials, build caches +and local database state. The live API contract is published at `/openapi.json` +when generated in `packages/contracts/`. ## Usage and operations -The architecture uses Workers Static Assets (website, map code, station files and -downloads) and D1 (current prices, reports and a daily usage count). Map tiles come +The architecture uses Workers Static Assets (website, map code and station files) +and D1 (current prices, reports and a daily usage count). Map tiles come from OpenFreeMap's free public service. It has no paid fuel-data feed or map subscription. Actual cost and capacity depend on the Cloudflare account plan and usage; consult [Workers pricing](https://developers.cloudflare.com/workers/platform/pricing/) diff --git a/docs/RUNNING_COSTS.md b/docs/RUNNING_COSTS.md index faac10f..2f0d898 100644 --- a/docs/RUNNING_COSTS.md +++ b/docs/RUNNING_COSTS.md @@ -1,8 +1,9 @@ # Running costs, limits and donations OpenFuel runs on one Cloudflare Worker with a D1 database. Static files (the website, -map code, the Android APK download) are served free and never touch the database. -Map tiles come from OpenFreeMap's free public service. +map code, station files) are served free and never touch the database. Downloads +(the Android APK and the source archive) are GitHub release assets, and map tiles +come from OpenFreeMap's free public service. ## How database use is kept low diff --git a/tests/browser_checks.py b/tests/browser_checks.py index 26fa458..3f117be 100644 --- a/tests/browser_checks.py +++ b/tests/browser_checks.py @@ -114,9 +114,8 @@ def main(): connection=http.client.HTTPConnection('127.0.0.1',server.server_port,timeout=5) connection.request('GET',path);r=connection.getresponse();body=r.read() check('HTTP route '+path,r.status==200 and len(body)>0);connection.close() - connection=http.client.HTTPConnection('127.0.0.1',server.server_port,timeout=5) - connection.request('GET','/downloads/openfuel-source.zip');r=connection.getresponse();raw=r.read();connection.close() - with ZipFile(BytesIO(raw)) as z: + # Downloads are GitHub release assets; the build still packages the source archive the release attaches. + with ZipFile(ROOT/'dist/openfuel-source.zip') as z: check('Source download contains all four apps',all('openfuel/'+name in z.namelist() for name in ['apps/web/index.html','apps/docs/index.html','apps/ios/project.yml','apps/android/settings.gradle.kts'])) wrapper='openfuel/apps/android/gradle/wrapper/gradle-wrapper.jar' check('Source download has no app binaries, fonts or local data',not any((x.endswith(('.apk','.jar','.ttf','.woff2','.sqlite3')) and x!=wrapper) or '/.local/' in x or '/.git/' in x for x in z.namelist())) @@ -220,13 +219,13 @@ def open_iframe(page,selector): load(page,'/') check(prefix+'approved wordmark is used on landing',page.locator('.site-header .wordmark img').get_attribute('src')=='brand/openfuel-wordmark-light.svg') check(prefix+'website no horizontal overflow',page.evaluate('document.documentElement.scrollWidth<=innerWidth')) - check(prefix+'website source and docs routes',page.locator('a[href="docs/"]').count()>=1 and page.locator('a[href="downloads/openfuel-source.zip"]').count()>=1) + check(prefix+'website source and docs routes',page.locator('a[href="docs/"]').count()>=1 and page.locator('a[href="https://github.com/AKolenda/openfuel/releases/latest/download/openfuel-source.zip"]').count()>=1) page.locator('#language').click();check(prefix+'French toggle',page.locator('html').get_attribute('lang')=='fr') check(prefix+'French build status names the OpenFreeMap base map','tuiles vectorielles OpenFreeMap' in page.locator('[data-i18n="status-web"]').text_content()) page.locator('#language').click() check(prefix+'build status names the OpenFreeMap base map','OpenFreeMap vector tiles' in page.locator('[data-i18n="status-web"]').text_content()) check(prefix+'primary app link opens the full application',page.locator('a[data-i18n="a-demo"]').get_attribute('href')=='/preview/') - check(prefix+'Android APK is a real download link',page.locator('a[data-i18n="a-build"]').get_attribute('href')=='/downloads/openfuel-android.apk') + check(prefix+'Android APK is a real download link',page.locator('a[data-i18n="a-build"]').get_attribute('href')=='https://github.com/AKolenda/openfuel/releases/latest/download/openfuel-android.apk') page.locator('.phone-play').click() frame=open_iframe(page,'#demo-frame') check(prefix+'website location denial has no sample fallback',frame.locator('.station-card').count()==0 and 'permission is off' in frame.locator('#location-status').inner_text()) diff --git a/tests/test_foundation.py b/tests/test_foundation.py index edd369c..bd0ef68 100644 --- a/tests/test_foundation.py +++ b/tests/test_foundation.py @@ -12,7 +12,7 @@ def test_public_defaults_use_live_api(tmp_path): c=read_public_config(tmp_path,{}) - assert c == {'environment':'development','apiBaseURL':'/api/v1','sourceURL':'/downloads/openfuel-source.zip','mode':'live','writesEnabled':True} + assert c == {'environment':'development','apiBaseURL':'/api/v1','sourceURL':'https://github.com/AKolenda/openfuel/releases/latest/download/openfuel-source.zip','mode':'live','writesEnabled':True} def test_public_build_never_serializes_private_env(tmp_path): diff --git a/tests/test_repository.py b/tests/test_repository.py index c5342bd..f39daca 100644 --- a/tests/test_repository.py +++ b/tests/test_repository.py @@ -104,7 +104,7 @@ def test_website_docs_preview_routes_and_source_links(): page=(ROOT/'apps/web/index.html').read_text() tags=Tags(page).tags assert any(t=='a' and a.get('href')=='docs/' for t,a in tags) - assert any(t=='a' and a.get('href')=='downloads/openfuel-source.zip' for t,a in tags) + assert any(t=='a' and a.get('href')=='https://github.com/AKolenda/openfuel/releases/latest/download/openfuel-source.zip' for t,a in tags) docs=(ROOT/'apps/docs/index.html').read_text() assert 'href="../#project"' in docs and 'href="../"' in docs assert 'REFERENCE=' not in (ROOT/'apps/docs/app.js').read_text() diff --git a/tools/project.py b/tools/project.py index aa25dbf..4cd7528 100644 --- a/tools/project.py +++ b/tools/project.py @@ -157,29 +157,29 @@ def build_site(*, include_source: bool=True) -> Path: # Station geography for the Worker, one file per 0.5 degree area, so stations requests do not read D1. from tools.import_live_data import write_areas write_areas(site/'data/stations') - if include_source: - source=package_source() - if source.stat().st_size > WORKERS_ASSET_LIMIT: - raise RuntimeError('The source archive exceeds the Workers 25 MiB asset limit; exclude generated or binary files from it.') - (site/'downloads').mkdir();shutil.copyfile(source,site/'downloads/openfuel-source.zip') - apk=Path(os.environ.get('OPENFUEL_ANDROID_APK', ROOT/'apps/android/app/build/outputs/apk/debug/app-debug.apk')) - if apk.is_file(): - if apk.stat().st_size > WORKERS_ASSET_LIMIT: - raise RuntimeError('The Android APK exceeds the Workers 25 MiB asset limit. Build the compact APK or set OPENFUEL_ANDROID_APK to a verified compact build.') - downloads=site/'downloads';downloads.mkdir(exist_ok=True) - shutil.copyfile(apk,downloads/'openfuel-android.apk') - digest=hashlib.sha256(apk.read_bytes()).hexdigest() - (downloads/'openfuel-android.apk.sha256').write_text(digest+' openfuel-android.apk\n') - (downloads/'release.json').write_text(json.dumps({ - 'product':'OpenFuel', 'channel':'public-alpha', 'data':'real OpenStreetMap stations; unverified community prices', - 'android':{'path':'/downloads/openfuel-android.apk','bytes':apk.stat().st_size, - 'sha256':digest,'minimumAndroid':'8.0','signing':'debug'}, - 'source':'/downloads/openfuel-source.zip', - 'ios':'SwiftUI source; macOS/Xcode required to build' - },indent=2)+'\n') + # Downloads (the Android APK and the source archive) are GitHub release assets, not part of the site; + # release-assets prepares them. The source archive is still built here, for the checks. + if include_source: package_source() print('Static website: dist/site/ — website /, handbook /docs/, station map /preview/') return site +def release_assets() -> Path: + """Collects a GitHub release's downloads in dist/release: the Android APK, its checksum and the source archive. + + The website links to https://github.com/AKolenda/openfuel/releases/latest/download/, so a release + must attach exactly these names and must not be marked as a pre-release. + """ + out=DIST/'release' + if out.exists(): shutil.rmtree(out) + out.mkdir(parents=True) + apk=Path(os.environ.get('OPENFUEL_ANDROID_APK', ROOT/'apps/android/app/build/outputs/apk/debug/app-debug.apk')) + if not apk.is_file(): raise RuntimeError('Build the Android APK first (python3 tools/project.py android-build) or set OPENFUEL_ANDROID_APK.') + shutil.copyfile(apk,out/'openfuel-android.apk') + (out/'openfuel-android.apk.sha256').write_text(hashlib.sha256(apk.read_bytes()).hexdigest()+' openfuel-android.apk\n') + package_source(out/'openfuel-source.zip') + print(f'Release assets: {out.relative_to(ROOT)}/') + return out + class QuietHandler(http.server.SimpleHTTPRequestHandler): def end_headers(self): self.send_header('X-Content-Type-Options','nosniff') @@ -280,7 +280,7 @@ def check_all(args): def main(): parser=argparse.ArgumentParser(description=__doc__) sub=parser.add_subparsers(dest='command',required=True) - for name in ['doctor','site','api-seed','ios-core','android-core','android-build','ios-generate','ios-build']:sub.add_parser(name) + for name in ['doctor','site','release-assets','api-seed','ios-core','android-core','android-build','ios-generate','ios-build']:sub.add_parser(name) for name,port in [('serve',4173),('api',8000)]: p=sub.add_parser(name);p.add_argument('--host',default='127.0.0.1');p.add_argument('--port',type=int,default=port) p=sub.add_parser('package');p.add_argument('--output',type=Path) @@ -288,7 +288,7 @@ def main(): p=sub.add_parser(name);p.add_argument('--check',action='store_true') p=sub.add_parser('check');p.add_argument('--web',action='store_true');p.add_argument('--native-cores',action='store_true') args=parser.parse_args() - actions={'doctor':doctor,'site':build_site,'api-seed':api_seed,'ios-core':ios_core,'android-core':android_core, + actions={'doctor':doctor,'site':build_site,'release-assets':release_assets,'api-seed':api_seed,'ios-core':ios_core,'android-core':android_core, 'android-build':android_build,'ios-generate':ios_generate,'ios-build':ios_build} if args.command in actions:actions[args.command]() elif args.command=='serve':serve(args) diff --git a/tools/public_config.py b/tools/public_config.py index 1b0a30f..36832e3 100644 --- a/tools/public_config.py +++ b/tools/public_config.py @@ -7,7 +7,8 @@ import os KEYS = ('OPENFUEL_PUBLIC_ENV', 'OPENFUEL_PUBLIC_API_BASE_URL', 'OPENFUEL_PUBLIC_SOURCE_URL') -DEFAULTS = dict(zip(KEYS, ('development', '/api/v1', '/downloads/openfuel-source.zip'))) +# Downloads are GitHub release assets; the site does not host them. +DEFAULTS = dict(zip(KEYS, ('development', '/api/v1', 'https://github.com/AKolenda/openfuel/releases/latest/download/openfuel-source.zip'))) # Optional donation page for the running costs (database, map tiles). Without it no donate UI appears. DONATE_KEY = 'OPENFUEL_PUBLIC_DONATE_URL'