Files
Flight-Ops/CHANGELOG.md
T

23 KiB
Raw Blame History

Changelog

v0.6.0 — 2026-09-17 11:49

  • Data foundation (PLAN.md "Simulation engine" increment 1): Drizzle ORM + SQLite via sql.js (WASM) — chosen over better-sqlite3 because it needs zero native compilation, so the existing oven/bun-alpine install stages and node:22-alpine runtime work unchanged; deps drizzle-orm@0.45, sql.js@1.14 (+ dev-only @types/sql.js)
  • Schema (lib/db/schema.ts, DDL mirrored in lib/db/client.ts): planes (id, model, status available|flying, fuel/maxFuel, locationIcao, contractId?, lat/lon), contracts (id, origin/dest ICAO, reward, cargoType/cargoCount, deadlineSimTime, status open|accepted|completed, planeId?, createdSimTime), single-row account (cash, reputation) and sim_state (clockMs, timeScale 1–60, paused)
  • First-run seed (lib/game/seed.ts) — pure buildSeed({airports, rng, nowMs}), fully deterministic with an injected RNG (mulberry32 PRNG exported for tests): starter plane ATR 72-600 (in-code catalog constant in lib/game/catalog.ts: maxFuel 6300 L, burnPerKm 1.75 L/km, speedKmh 510 — structured so more types can be added) grounded full-tank at Frankfurt (EDDF; EDDM is Munich), 8–12 open contracts between random distinct CSV airports with reward round(500 + 2.5 · haversineKm) (documented simple formula; real revenue/cost modeling is a later increment), cargo flavor (Passengers ≤68 / pallets ≤50), deadline = now + 6–72h game time; account starts at cash 20 000 / reputation 10; simState clockMs = real now, timeScale 1, paused false
  • Persistence: DB file db/flight.db on the host (gitignored) volume-mounted in docker-compose as ./db:/app/db — a dedicated dir, NOT over /app/data (which would mask airports_eu.csv); sql.js keeps the DB in memory, so persist() exports the bytes and writes them atomically (tmp file + rename) after every mutation; seeding happens lazily on first access when the account table is empty; FLIGHT_DB_PATH env override lets tests point at temp files
  • New read-only GET /api/state (app/api/state/route.ts): full snapshot {account, simState, planes[], contracts[]} for client polling — plane/contract shapes include airport names (locationName / originName / destName) resolved from the CSV; no tick loop yet, state is static after seed
  • OpenAPI: /api/state documented with component schemas StateSnapshot/AccountState/SimState/PlaneState/ContractState (lib/openapi.ts)
  • next.config.ts: serverExternalPackages: ['sql.js'] — the WASM blob resolves relative to sql.js's dist dir, so it must not be bundled into a .next chunk; known drizzle-orm 0.45.x quirk worked around in code (commented): the sql-js driver drops the result mapper for relational db.query.* queries and returns raw snake_case rows, so all reads use plain select()
  • Tests: unit — new lib/geo.test.ts (haversine), lib/game/seed.test.ts (determinism with fixed RNG, 8–12 count, distinct endpoints, reward formula vs haversine, deadline window, hub + fallback, account/simState values), lib/db/client.test.ts (first-run seed + on-disk file, no re-seed on reopen, persist() round-trip) and app/api/state/route.test.ts (200 + exact top-level shape against a temp DB); openapi tests updated for the third endpoint; e2e — new e2e/state.spec.ts: snapshot shape from the live origin, plus restart-persistence test that records cash/clockMs/ids, runs docker compose restart flight, waits for readiness and asserts everything unchanged (PLAN.md "sim clock persisted across sessions"); playwright.config gains workers: 1 so the mid-run container restart can't race the map tests
  • Verified: unit suite 47/47; tsc --noEmit clean; e2e 11/11 green against the rebuilt compose instance (incl. both new state tests); live curl /api/state → Frankfurt starter plane + 11 contracts, /api/airports still serves 242 features after the mount change; persistence across a full docker compose up -d --build verified manually (clockMs/cash/contract count identical before/after)

v0.5.0 — 2026-09-17 10:15

  • New GET /api/version (app/api/version/route.ts): returns { "version": "<semver>" }, version read from package.json via a server-side import (same single-source-of-truth pattern as the header tag)
  • OpenAPI 3.1 spec in code: lib/openapi.ts exports an object documenting exactly the two public endpoints — GET /api/airports (GeoJSON FeatureCollection, with component schemas for feature collection/feature/point geometry/properties) and GET /api/version; info.version is imported from package.json so it stays in sync automatically; servers: [{ url: "" }] means "current origin"
  • New GET /api/openapi (app/api/openapi/route.ts) serves that spec as JSON — machine-readable API docs for AI agents and external tooling (no interactive docs page, no new dependencies)
  • AGENTS.md gains a short "## API" section pointing agents at the live base URL and the spec at https://flight.dev.kkhost.pl/api/openapi, noting the spec is generated from lib/openapi.ts and must be updated whenever endpoints change; this commit also includes the pending local note in AGENTS.md's "## Other" section (use pty_* tools for longer terminal tasks)
  • Tests: unit — new app/api/version/route.test.ts (200 + JSON version equals package.json's, exact body shape) and lib/openapi.test.ts (both paths present, info.version in sync with package.json, empty server url, airports schema refs the FeatureCollection/Feature/Properties component schemas with name/icao/lat/lon); e2e — new e2e/api.spec.ts: fetches /api/version from the live origin and asserts 200 + version matching package.json read at test time; fetches /api/openapi and checks content-type, title, info.version and both paths

v0.4.1 — 2026-09-17 08:50

  • Map-asset pack (~100 MB, 780 files) removed from git and from the Docker image: git rm -r --cached public/{styles,fonts,sprites} (files kept on disk), .gitignore now excludes exactly the generated paths (public/styles/, public/fonts/, public/sprites/, public/map-assets-manifest.json) — not all of public/; .dockerignore gets the same four entries; the v0.4.0 runtime-stage COPY --from=build /app/public ./public line is gone from the Dockerfile
  • The pack now reaches the container via a docker-compose volume mount: ./public:/app/public on the flight service — host dir present with assets → served locally; absent/empty → 404s → runtime fallback (below). Establishes the pattern for the future multi-GB tile file (v0.5.0) that must never go into git
  • scripts/download-map-assets.mjs now writes public/map-assets-manifest.json as its LAST step ({name, generatedAt ISO, styles: [ids]}) so the manifest's presence proves the whole pack is in place; new npm script "assets:download": "node scripts/download-map-assets.mjs"
  • Runtime fallback for fresh clones without the pack: lib/mapStyles.ts gains pure styleUrlFor(id, localAssetsAvailable) → /styles/{id}.json or https://tiles.openfreemap.org/styles/{id}, plus async localAssetsAvailable() — fetches /map-assets-manifest.json once (module-level cached promise; any non-200/network error → false); the single manifest probe is designed so a future self-hosted tile pack plugs into the same availability mechanism without rework
  • components/Map.tsx: map creation in the mount effect is deferred until the probe resolves (one cheap local request before new Map), and the persisted-style restore was folded into that same post-mount flow — localStorage is still read only after mount (SSR-safe), applied at creation time so there's no fiord→saved flash; style switcher changes reuse the cached probe via styleUrlFor; all v0.4.0 behavior intact (fiord default, persistence key, idempotent ensureAirportsLayer on every styledata, single /api/airports fetch, window.__flightMap)
  • Tests: unit — new styleUrlFor cases (local path / exact remote OFM URL) and localAssetsAvailable cases (false on non-200, true + cached with exactly one fetch across awaits via module reset, false on network error); e2e — new test "falls back to remote OFM styles when the local asset pack is absent" routes /map-assets-manifest.json → 404, lets *.openfreemap.org/styles/** through while aborting all other OFM traffic, and asserts the map initializes from the REMOTE style (glyphs still point at openfreemap.org), the airport count renders, and a UKBB marker click opens its popup — proving fresh-clone-without-assets usability end-to-end
  • Verified: unit suite 21/21; tsc --noEmit clean; e2e suite green against the rebuilt compose instance with assets served from the bind mount (curl: /styles/fiord.json, a font .pbf, /sprites/ofm.json all 200); note the asset blobs remain in git history — no history rewrite was performed

v0.4.0 — 2026-09-17 08:22

  • Self-hosted all OpenFreeMap map-styling assets (tiles stay remote): new scripts/download-map-assets.mjs fetches the 5 OFM styles with a browser UA, rewrites each via rewriteStyle, and extracts the font/sprite tarballs into public/ — committed so the image is self-contained: public/styles/{liberty,bright,positron,dark,fiord}.json (0.15 MB), public/fonts/Noto Sans {Regular,Italic,Bold}/*.pbf (768 files, 99.76 MB — the font payload), public/sprites/ofm{.json,.png,@2x.json,@2x.png} (0.21 MB); total public/ = 100.13 MB
  • New shared module lib/mapStyles.ts: STYLES registry (5 ids + labels), DEFAULT_STYLE_ID = 'fiord', and pure rewriteStyle() — glyphs https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf → /fonts/{fontstack}/{range}.pbf, sprite https://tiles.openfreemap.org/sprites/ofm_f384/ofm → /sprites/ofm; the vector tile url (/planet) and ne2_shaded raster tiles (/natural_earth/ne2sr/...) are left remote; every source lacking an attribution gets "© OpenStreetMap contributors © OpenFreeMap" (OFM styles ship none; ODbL requires visible OSM credit)
  • components/Map.tsx: map now loads the local /styles/fiord.json by default; new top-bar style switcher (<select data-testid="style-switch">, subtle styling in app/globals.css) listing all 5 styles, persisted to localStorage key flight-ops-style; SSR-safe — state initializes to DEFAULT_STYLE_ID and the saved choice is read/applied only in a post-mount effect (no localStorage reads during render/hydration)
  • components/Map.tsx: airports layer now survives style switches — /api/airports is fetched at most once per page life (promise cached in a ref; count set exactly once), and an idempotent ensureAirportsLayer() re-adds the source+layer if missing on EVERY styledata event (initial load AND after each setStyle, which wipes sources/layers not in the new style); the click→popup handler is re-wired with off+on using a stable reference since MapLibre layer-scoped listeners survive setStyle and would otherwise duplicate
  • Dockerfile: runtime stage now copies public/ from the build stage (COPY --from=build /app/public ./public) so next start serves the self-hosted assets; .dockerignore already lets public/ through
  • Tests: new unit suite lib/mapStyles.test.ts (registry shape, fiord default, rewriteStyle against an inline OFM fixture — local glyphs/sprite, remote tiles preserved, attribution added/preserved, input not mutated); e2e/map.spec.ts: openfreemap mock simplified to abort ALL requests from that host (only tiles remain there), new test asserting the switcher has 5 options with Fiord default, switching changes the applied style's layer set AND the airports layer re-renders after the switch + exactly one popup on marker click (guards the setStyle-wipes-layers and duplicate-listener regressions), plus a reload-persistence test for the localStorage restore path
  • Verified: unit suite 16/16; tsc --noEmit clean; e2e suite 6/6 green against the rebuilt compose instance (all styles/fonts/sprites served locally, fiord default, switch + persistence verified in a real browser)

v0.3.1 — 2026-09-17 06:35

  • Header now shows the app version between the "Flight Ops" title and the airport count (<small>v{version}</small> in components/Map.tsx), sourced dynamically from package.json via a JSON import — future version bumps show up automatically with no code change; subtle 11px muted style added to app/globals.css
  • Tests: new unit suite components/version.test.ts (valid semver in package.json + guard that the header sources the version from package.json rather than hardcoding it); e2e/map.spec.ts: new test asserting the header shows v{version} with the expected value read from package.json at test time, so it stays in sync automatically
  • Verified: unit suite 10/10; tsc --noEmit clean; e2e suite 4/4 green against the rebuilt compose instance

v0.3.0 — 2026-09-17 00:32

  • Removed the zoom-driven camera tilt (lib/camera.ts pitch curve + the map move handler): the map is now always flat/top-down with standard pan/zoom only; deleted lib/camera.ts and its unit tests
  • Removed terrain: no more DEM raster source / map.setTerrain, and the ?terrain=0 / NEXT_PUBLIC_TERRAIN toggles are gone — pure 2D view, no 3D elements
  • MAX_ZOOM 14 → 11: at European latitudes (~52°N) z11 ≈ 47 m/px, so the viewport spans ~90–135 km across a typical desktop width (≈100 km on screen); street-level zoom had no gameplay value
  • e2e/map.spec.ts: APP_URL is now / (terrain flag removed), MAX_ZOOM mirror updated to 11; PLAN.md map section updated to flat-2D-only
  • Verified: unit suite 8/8; tsc --noEmit clean; e2e suite 3/3 green against the rebuilt compose instance (flat view, wheel zoom clamped to [3, 11])

v0.2.9 — 2026-09-17 00:21

  • data/airports_eu.csv: multi-airport metro areas reduced to one dot each on the map — Paris→CDG, London→LHR, Moscow→SVO, Milan→MXP, Istanbul→IST, Warsaw→WAW, Brussels→BRU (Rhine-Ruhr keeps both DUS and CGN as distinct cities); dropped 18 satellite airports incl. Orly, Gatwick, Luton, Stansted, Domodedovo, Vnukovo, Zhukovsky, Linate, Caravaggio, Atatürk, Modlin, Charleroi, Weeze, Prestwick, Treviso, Ciampino, Băneasa — 260 → 242 rows; /api/airports and the circle layer pick it up with no code changes
  • Verified: unit suite 8/8; tsc --noEmit clean; e2e suite 3/3 green against the rebuilt compose instance (/api/airports serves 242 features)

v0.2.8 — 2026-09-16 23:53

  • data/airports_eu.csv: reduced the dataset to only large_airport rows (dropped all 731 medium_airport + 54 seaplane_base) — the map now shows just the 260 biggest EU airports; /api/airports and the circle layer pick it up with no code changes
  • e2e/map.spec.ts: marker-click test repointed from ESSV Visby Airport (a medium_airport, now removed) to UKBB Boryspil International Airport — with only large_airport rows left it is the most isolated marker inside the initial zoom-4 viewport: its nearest surviving airport is Chişinău (LUKK), ~126 px away at the initial zoom (Kyiv Zhuliany, geographically closer, was a medium and got cut)
  • Verified: unit suite 12/12; tsc --noEmit clean; e2e suite 3/3 green against the rebuilt compose instance (/api/airports serves 260 features)

v0.2.7 — 2026-09-16 23:39

  • data/airports_eu.csv: removed all 6400 small_airport rows from the OurAirports EU export — the map now shows only the 1045 bigger airports (260 large + 731 medium + 54 seaplane_base); /api/airports and the circle layer pick it up with no code changes
  • e2e/map.spec.ts: marker-click test repointed from EKRN Bornholm Airport (a small_airport, now removed) to ESSV Visby Airport on Gotland — its nearest remaining airport is Stockholm Skavsta across the Baltic Sea (~59 px away at the initial zoom), so its marker cannot be overlapped by a neighbouring circle
  • Verified: unit suite 12/12; e2e suite 3/3 green against the rebuilt compose instance (/api/airports serves 1045 features)

v0.2.6 — 2026-09-16 22:41

  • Dockerfile: install stages switched from npm to bun (oven/bun:1.4.2-alpine, bun install --frozen-lockfile); package-lock.json replaced by bun.lock; no BuildKit type=cache mounts — they don't persist across builds on this daemon, and bun install is fast enough (~3 s cold) that cross-build caching isn't worth it
  • Dockerfile: new prod-deps stage (bun install --production) so the runtime image carries prod-only node_modules — devDeps (typescript, vitest, @types) are no longer shipped; build and runtime stages otherwise unchanged (still node:22-alpine, full .next, no standalone output)
  • .dockerignore: excludes docs/deployment files (*.md, Dockerfile*, docker-compose*) from the build context so changelog-only commits don't invalidate the COPY layer and re-run next build
  • Verified: full cold build 77 s → 58 s; e2e suite 3/3 green against the rebuilt compose instance

v0.2.5 — 2026-09-16 21:22

  • AGENTS.md: new "E2E tests (mandatory)" section codifies the e2e workflow — the compose instance behind Traefik (https://flight.dev.kkhost.pl) is always the app under test, never a local next dev on :3000; deploy current code first with docker compose up -d --build; browsers run through the shared remote Playwright server at 192.168.1.4:3000, and plain npx playwright test handles deploy + readiness + remote connect
  • playwright.config.ts: dropped the E2E_LOCAL=1 local-dev path accordingly — no more local :3000 dev instance for e2e; the default (and only) mode is browsers on the shared remote Playwright server against the prod build behind Traefik, with webServer running docker compose up -d --build; E2E_REMOTE=1 and explicit PW_TEST_CONNECT_WS_ENDPOINT/E2E_BASE_URL overrides are unchanged

v0.2.4 — 2026-09-16 21:06

  • playwright.config.ts: default e2e mode now targets the prod build behind Traefik (https://flight.dev.kkhost.pl) — baseURL defaults to that URL and webServer runs docker compose up -d --build (300 s timeout) instead of npm run dev, because next dev does not render for non-loopback clients: Turbopack's HMR WebSocket fails from the remote browsers, so the page loads blank
  • E2E_LOCAL=1 keeps the local iteration path (browsers on this box, webServer auto-starts npm run dev, baseURL localhost:3000) as a fallback when the proxy or compose stack is down; E2E_REMOTE=1 and explicit PW_TEST_CONNECT_WS_ENDPOINT/E2E_BASE_URL overrides are unchanged

v0.2.3 — 2026-09-16 20:30

  • playwright.config.ts: e2e now launches browsers on the shared remote Playwright server (Docker, ws://192.168.1.4:3000/) by default — when no env vars are set it exports PW_TEST_CONNECT_WS_ENDPOINT pointing at that box; in this mode baseURL defaults to this box's LAN IP (http://192.168.1.26:3000) because pages load from the remote browser's perspective, while the local webServer still auto-starts npm run dev
  • E2E_LOCAL=1 restores fully-local runs (browsers on this box, localhost:3000 baseURL) as a fallback when the remote server is down; E2E_REMOTE=1 keeps its previous behavior for runners on other machines — skips webServer entirely and pairs with E2E_BASE_URL; an explicit PW_TEST_CONNECT_WS_ENDPOINT always wins over the default

v0.2.2 — 2026-09-16 20:05

  • playwright.config.ts: e2e suite can now run on a different machine than the app — with E2E_REMOTE=1 the local webServer block is skipped entirely and baseURL defaults to http://flight.dev.kkhost.pl; E2E_BASE_URL overrides baseURL in both modes; with no env vars set behavior is unchanged (local webServer auto-starts npm run dev)

v0.2.1 — 2026-09-16 19:49

  • Clamp map zoom to the useful gameplay range: minZoom: 3 / maxZoom: 14 on the MapLibre options in components/Map.tsx (exported as MIN_ZOOM/MAX_ZOOM) — wheel travel from the Europe overview is now ~10 levels instead of ~16, and street-level zoom past the z12 pitch transition no longer exists
  • e2e: new test that wheels well past both limits via page.mouse.wheel (basemap tile fetches blocked for speed/determinism; clamping is pure camera behavior) and asserts via expect.poll on the map instance that getZoom() settles at 14 / 3, i.e. clamped within [3, 14]

v0.2.0 — 2026-09-16 18:32

  • Docker deployment: multi-stage Dockerfile (node:22-alpine; deps → build → runtime) running next start on port 3000; no output: 'standalone' — the runtime stage keeps full node_modules because /api/mlworker/[file] reads maplibre-gl dist files from disk at runtime (and /api/airports reads data/*.csv)
  • .dockerignore: node_modules, .next, data/*.db, test-results, *.tsbuildinfo, plus .git/.env/.codenomad and the e2e harness (@playwright/test is a host-global install, not a project dependency — Next's build-time type check would fail on it inside the image)
  • docker-compose.yml: single service flight, no published ports, joins external network dev_net with Traefik labels routing Host(flight.dev.kkhost.pl) → port 3000 (plain HTTP entrypoint web; TLS terminated upstream)
  • Verified: container builds and runs; router registered in dev_traefik rawdata; curl -H "Host: flight.dev.kkhost.pl" http://localhost/ → 200, /api/airports → 200 GeoJSON (7445 features), mlworker .mjs files served from node_modules inside the container; unit suite 12/12 and e2e suite 2/2 still green

v0.1.0 — 2026-09-16 18:24

  • Scaffolded Next.js (App Router) + TypeScript project; dev server on port 3000 (npm run dev)
  • Full-viewport MapLibre GL map with OpenFreeMap "liberty" vector basemap (attribution included via style)
  • Zoom-driven camera controller: top-down at zoom ≤ 6, smoothstep tilt to 45° pitch by zoom ≥ 12; pitch is a pure function of zoom so it never fights pan/zoom gestures
  • Lite 3D terrain from AWS terrarium raster-DEM tiles (map.setTerrain); toggleable off via ?terrain=0 or NEXT_PUBLIC_TERRAIN=0 (off in e2e for deterministic headless rendering)
  • EU airport seed data: OurAirports CSV (official mirror at davidmegginson.github.io/ourairports-data — the legacy ourairports.com/data URL now 404s) filtered to continent=EU with types small/medium/large_airport + seaplane_base → committed as data/airports_eu.csv (7445 airports; heliports, closed and balloon sites excluded — rationale documented in lib/airports.ts)
  • GET /api/airports: server-side CSV parse with in-memory cache, returns GeoJSON FeatureCollection (properties: name, icao, lat, lon)
  • Client renders airports as a GeoJSON circle layer (no DOM markers); click → popup with name + ICAO; header shows loaded airport count (data-testid="airport-count")
  • MapLibre worker served from node_modules via /api/mlworker/* route — bundlers break the URL MapLibre derives for its worker, which silently hangs GeoJSON source updates
  • Tests: vitest unit suite (CSV parse/filter, GeoJSON build, pitch curve, API handler) + self-contained Playwright e2e suite (webServer auto-starts npm run dev; page load + airport count + marker click popup)