No description
  • TypeScript 95.3%
  • JavaScript 3.6%
  • CSS 0.8%
  • HTML 0.2%
  • Dockerfile 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Tim Janke 36739379c3
All checks were successful
CI / check (push) Successful in 1m26s
CI / e2e (push) Successful in 3m42s
CI / publish (push) Successful in 26s
👷 (ci): Publish the frontend image to the Forgejo registry
2026-08-06 07:37:12 +02: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 🐳 (deploy): Ship the website as an nginx container image 2026-08-06 07:37:05 +02:00
e2e (e2e): Fly an order end to end in a real browser 2026-08-03 05:17:25 +01:00
public 🧪 (mocks): Serve the contract over MSW and test it end to end 2026-08-03 05:16:38 +01:00
src (shell): Wire the app shell, lazy routing and the entry point 2026-08-03 05:17:13 +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): Define the web API contract as openapi.yaml 2026-08-03 05:16:27 +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): Fly an order end to end in a real browser 2026-08-03 05:17:25 +01:00
README.md 📝 (docs): Record what was built, what deviated and what is still open 2026-08-03 05:17:25 +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 against mocks. Every screen in docs/FRONTEND_PLAN.md exists and works end to end against an in-browser mock backend. No real backend exists yet; this repo owns the contract it will implement.

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 Not started. Serves both the plugin (protobuf) and this website (JSON).
flyg-frontend This repo. React SPA, owns the web-facing API contract until the backend exists.

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.

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.mjsnet::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.