- TypeScript 95.3%
- JavaScript 3.6%
- CSS 0.8%
- HTML 0.2%
- Dockerfile 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .github/workflows | ||
| deploy | ||
| docs | ||
| e2e | ||
| public | ||
| src | ||
| tools | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc.json | ||
| Dockerfile | ||
| eslint.config.js | ||
| index.html | ||
| openapi.yaml | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.ts | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
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.tsis the mock simulator. Nothing undersrc/featuresorsrc/routesimports it except the dev-onlyMockSimPanel.
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.