No description
  • TypeScript 96.6%
  • JavaScript 2.3%
  • CSS 0.9%
  • HTML 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Tim Janke b4e4417423
All checks were successful
CI / check (push) Successful in 59s
CI / e2e (push) Successful in 4m49s
CI / publish (push) Successful in 22s
✅ (e2e): Add a run against a real backend
The Playwright suite has only ever driven the mock world, which cannot tell us
the one thing worth knowing: that a real server answers on the contract this
build generated its types from. Setting FLYG_API_PROXY now points the dev server
at a backend and adds a project of specs that need no seeded fixtures.

Four of them, and deliberately narrow: the served contract matches this build,
the landing page renders from the real stats endpoint, an unauthenticated pilot
is sent to sign in rather than to an error, and a rejected pairing code comes
back as problem+json rather than an HTML error page.

Anything past sign-in needs a passkey a browser cannot be handed here, so the
rest of the assembly stays a person's job. The mock suite still gates a merge
and is unchanged; a live run is one variable away rather than a separate setup.

Run against a backend on this machine: four passed.
2026-09-07 01:36:05 +01:00
.github/workflows 👷 (ci): Publish the frontend image to the Forgejo registry 2026-08-06 07:37:12 +02:00
deploy 🐳 (deploy): Ship the website as an nginx container image 2026-08-06 07:37:05 +02:00
docs ✨ (api): Name the resume after a simulator crash in the event vocabulary 2026-09-06 22:08:19 +01:00
e2e ✅ (e2e): Add a run against a real backend 2026-09-07 01:36:05 +01:00
public 🧪 (mocks): Serve the contract over MSW and test it end to end 2026-08-03 05:16:38 +01:00
src ✨ (api): Let the server say which events count against a verdict 2026-09-07 01:20:50 +01:00
tools 🔨 (tools): Turn a plugin trackdump into a mock-world fixture 2026-08-03 05:17:25 +01:00
.dockerignore 🐳 (deploy): Ship the website as an nginx container image 2026-08-06 07:37:05 +02:00
.env.example 🔧 (build): Scaffold Vite, React 19, TypeScript and Tailwind 2026-08-03 05:16:16 +01:00
.gitignore 🔧 (build): Scaffold Vite, React 19, TypeScript and Tailwind 2026-08-03 05:16:16 +01:00
.prettierignore 🔧 (build): Scaffold Vite, React 19, TypeScript and Tailwind 2026-08-03 05:16:16 +01:00
.prettierrc.json 🔧 (build): Scaffold Vite, React 19, TypeScript and Tailwind 2026-08-03 05:16:16 +01:00
Dockerfile 🐳 (deploy): Ship the website as an nginx container image 2026-08-06 07:37:05 +02:00
eslint.config.js 🔧 (build): Scaffold Vite, React 19, TypeScript and Tailwind 2026-08-03 05:16:16 +01:00
index.html 🔧 (build): Scaffold Vite, React 19, TypeScript and Tailwind 2026-08-03 05:16:16 +01:00
openapi.yaml ✨ (api): Let the server say which events count against a verdict 2026-09-07 01:20:50 +01:00
package-lock.json 🔧 (build): Scaffold Vite, React 19, TypeScript and Tailwind 2026-08-03 05:16:16 +01:00
package.json 🔧 (build): Scaffold Vite, React 19, TypeScript and Tailwind 2026-08-03 05:16:16 +01:00
playwright.config.ts ✅ (e2e): Add a run against a real backend 2026-09-07 01:36:05 +01:00
README.md 📝 (docs): Record that the backend exists and how to run against it 2026-09-05 21:50:59 +01:00
tsconfig.json 🔧 (build): Scaffold Vite, React 19, TypeScript and Tailwind 2026-08-03 05:16:16 +01:00
vite.config.ts 🔧 (build): Scaffold Vite, React 19, TypeScript and Tailwind 2026-08-03 05:16:16 +01:00

flyg — website

The web interface of the flyg platform (virtual aircraft rental/ownership, orders, economy). Pilots manage their account and paired simulators here, rent aircraft, accept ferry and assignment orders, configure fuel and payload, watch a flight live, and review the verified logbook and its payouts.

Status: implemented. Every screen in docs/FRONTEND_PLAN.md exists and works end to end against an in-browser mock backend, which is still how this repo is developed and tested. flyg-backend now implements the same contract, so a build can be pointed at a real server instead — see Pointing at a real backend.

The three projects

Project Role
flyg-xplane X-Plane 12 plugin (C++23) — records and streams verified flight tracks, applies fuel/payload in the sim. Speaks protobuf over WS/HTTPS.
flyg-backend Rust. One process serving the plugin (protobuf under /v1) and this website (JSON under /api/v1), and bridging the two.
flyg-frontend This repo. React SPA, and the owner of openapi.yaml — the web-facing contract the backend implements.

Quick start

npm install
npm run dev          # http://localhost:5173, mock backend on

Sign in with Demo sign-in on /login (the mock accepts any credential), or use a real passkey — the WebAuthn ceremony works against the mock too, because the browser creates a genuine credential for localhost and the mock verifies nothing.

The demo pilot Oskar starts with eight logged flights, a wallet, a paired simulator and both the pilot and admin roles, so every screen including /admin/* is reachable.

To fly one yourself: rent an aircraft, accept an order it can fly, set the fuel, then use the dashed Mock simulator panel (dev-only) to take off. It stands in for the plugin and plays a synthesised flight back at 12× real time.

Scripts

Script What it does
npm run dev Vite dev server with the mock backend
npm run build Typecheck, then a production bundle in dist/
npm run preview Serve the built bundle
npm run typecheck tsc --noEmit
npm run lint ESLint over everything
npm run format / format:check Prettier
npm test vitest (unit, contract and component tests)
npm run test:e2e Playwright against the dev server (npx playwright install chromium first)
npm run api:types Regenerate src/api/schema.d.ts from openapi.yaml
npm run fixture:trackdump Turn a real plugin trackdump into a mock fixture

CI runs format, lint, typecheck, unit tests, build and e2e, and fails if openapi.yaml was changed without regenerating the types.

Contract-first

openapi.yaml is the web API contract and lives in this repo. npm run api:types generates src/api/schema.d.ts; nothing in src/ declares an API shape by hand, so a change to the spec that a screen or a mock does not follow is a type error rather than a runtime surprise.

Conventions the spec fixes: integer cents, kilograms, metres, metres per second, ISO-8601 Zulu timestamps, application/problem+json errors (RFC 9457), cursor pagination, and a cookie session with a double-submit CSRF header.

Track data is columnar ({t: [], lat: [], lon: [], alt_m: [], …}) and downsampled per ?resolution=map|profile|raw. A two-hour flight is ~30 000 samples; the map wants ~2 000 and the altitude profile ~5 000. The seeded world contains exactly such a flight (Kiruna → Copenhagen, 4 Hz, ~34 000 samples) so those numbers are exercised rather than assumed.

Pointing at a real backend

VITE_API_MODE=live VITE_API_BASE_URL=/api/v1 npm run build

VITE_API_MODE=live skips the mock entirely — MSW then never enters the main bundle, it stays in a chunk that is never fetched. In dev, vite.config.ts proxies /api to http://localhost:8080 (override with VITE_API_PROXY).

See .env.example for every variable.

A real server starts out empty, and the platform's aircraft and orders are created through the /admin screens. So that someone can reach them at all, the first account to register on a fresh backend becomes its administrator; every account after it is an ordinary pilot. Register first, then create aircraft under /admin/aircraft and orders under /admin/orders — until both exist, the market and the order board are empty for everyone.

The mock backend

src/mocks/ is a small implementation of the whole contract over a seeded, deterministic world: airports, 26 aircraft, ~46 open orders, a logbook with verified, flagged, rejected and crashed flights, a wallet, an admin view, and a simulator that can fly an order and stream it.

  • The same world runs in dev, vitest and Playwright, so a screen that works in one works in all three.
  • Mutations are persisted to sessionStorage, so renting an aircraft and reloading the page does not throw the rental away. The seeded logbook and its tracks are regenerated from the seed rather than stored.
  • A flight flown during the session does not survive a reload — its track only ever existed in memory. Everything else does.
  • src/mocks/sim.ts is the mock simulator. Nothing under src/features or src/routes imports it except the dev-only MockSimPanel.

Known limitation: the map in mock mode

MapLibre parses styles, tiles and GeoJSON in a module web worker. While MSW's service worker is intercepting, the request for that worker script fails (observed in Chromium 151 with MSW 2.15: maplibre-gl-worker.mjs → net::ERR_FAILED), and MapLibre then renders an empty background with no error of its own.

TrackMap therefore watches for its own GeoJSON source finishing and, if nothing renders within five seconds, falls back to StaticTrack — the same track drawn as an SVG with no basemap, labelled "Map tiles unavailable". That is what a mock-mode demo shows.

The same MapLibre setup, the same style URL and the same track geometry render correctly in this environment when MSW is not intercepting, which is the configuration a live build runs in. That has been verified standalone, not against a live backend, since none exists yet.

Architecture

src/
  api/         generated contract types, typed fetch client, endpoints, query keys
  components/  design system, layout shell, map, charts
  features/    one folder per domain: queries plus the pieces only it needs
  routes/      one file per screen, lazily loaded
  lib/         units, time, geo, i18n catalogue, env
  mocks/       the seeded world, MSW handlers, the mock simulator
  store/       zustand, UI-only state (theme, nav, map layers)
  • Server state is TanStack Query. Live telemetry is pushed into the same cache by the SSE client, so the map and the HUD read from one place.
  • Client state is zustand, and only for things the server does not own.
  • Units come from the pilot's profile; no component formats a number itself.
  • Strings live in src/lib/i18n/en.ts, keyed and exhaustively typed. A second locale is a copy of that file.
  • The live transport is SSE with automatic reconnect and backfill, and it degrades to polling the same REST endpoints when a stream cannot be opened.

Deployment

The output is a static bundle. deploy/ has an nginx config and a Caddyfile with the two rules that matter: SPA history fallback, and never caching index.html while hashing everything else forever.

VITE_API_MODE=live npm run build
rsync -a dist/ user@host:/srv/flyg/

docs/OPERATIONS.md covers the rest.