120 lines
29 KiB
Markdown
120 lines
29 KiB
Markdown
# Changelog
|
||
|
||
## v0.7.0 — 2026-09-17 12:27
|
||
- **Server-authoritative sim engine** (PLAN.md "Simulation engine" increment 2) in new `lib/sim/` — no frontend/UI changes; dispatch of planes lands next increment, but all the clock/market/finalization machinery it builds on is live and tested now
|
||
- **Tick-on-read game clock** (`lib/sim/engine.ts`, NO background timer): every read/action endpoint calls `tickNow()` first (GET /api/state, POST /api/sim — and any future action endpoint); the tick advances `clockMs` by `(real ms since last tick) × timeScale`. New `sim_state.last_real_tick_ms` column stores the real-time anchor (seed sets it to the seed moment). **Clamp: `MAX_TICK_ELAPSED_MS = 2000`** — a single tick counts at most 2 s of real absence, chosen above a normal ~1 s poll interval (active play advances ~real time × scale) but small enough that one stale read after an absence moves game time by ≤ 2 s × scale (≤ 2 min at max scale 60), so the sim only meaningfully progresses while actively polled. While paused the clock is frozen but `lastRealTickMs` keeps tracking real time, so unpausing resumes with no catch-up jump. Ticks are serialized through a module-level promise lock (concurrent Next.js requests can't double-advance) and each tick persists via the existing sql.js atomic export
|
||
- **Analytic movement** (`lib/sim/movement.ts`, pure functions of the sim clock — no incremental stepping): `travelDurationMs(distanceKm, speedKmh)`, `arrivalTimeFor(departureSimTime, distanceKm, speedKmh)`, `progressAt(...)` (clamped 0–1), `positionAt(fromLat, fromLon, toLat, toLon, t)` (great-circle slerp — exact endpoints, stays on the arc), `fuelAt(startFuel, burnPerKm, distanceKm, progress)` (`start − burn·distanceSoFar`, clamped at 0; range exhaustion not modeled yet). New nullable `planes.departure_sim_time` column carries the flight context (route comes from the bound contract, speed/burn from the catalog by model); while flying, `fuel` is frozen at its departure value and GET /api/state reports live lat/lon/fuel computed from the clock
|
||
- **Arrival finalization** (in-tick): a flying plane whose `arrivalTime ≤ clock` gets its contract marked completed, the exact reward added to `account.cash`, reputation += `1 + 0.0005 × km` (rounded to 2 dp — ~1.15 rep for a 300 km hop, ~2.5 for a 3000 km leg; slow growth from the starting 10), and the plane grounded at the destination with remaining fuel + `departureSimTime = null`. Idempotent by construction: finalization leaves the 'flying' state, so an award can never be paid twice (unit-tested)
|
||
- **Contract expiry** (in-tick): any `open` contract with `deadlineSimTime ≤ clockMs` → status `'expired'` (new enum value in schema/DDL/snapshot/OpenAPI); accepted/in-flight contracts never expire — they complete. Expired contracts leave the available set the client sees
|
||
- **Market spawn** (in-tick, after expiry): if open contracts < `MARKET_TARGET_OPEN_CONTRACTS = 10`, new ones are generated to top it up using the SAME builder as the first-run seed — `generateContract({id, airports, rng, nowMs, deadlineOffsetMs?})` extracted from `buildSeed` (same RNG call order, reward formula, cargo flavor, 6–72h game-time deadlines; spawn uses crypto-random UUID ids and the sim clock as "now"). Injectable RNG for deterministic tests
|
||
- **Time controls endpoint**: new `POST /api/sim` — body `{ timeScale?: number, paused?: boolean }`; unknown fields / non-number timeScale / non-boolean paused → 400; timeScale clamped to [1, 60]; ticks the clock first so the returned simState already reflects current game time. Documented in `lib/openapi.ts` (new SimControlsRequest + Error component schemas; ContractState status enum gains 'expired'; /api/state description notes tick-on-read)
|
||
- **DB migration for existing installs**: `openGameDb()` now runs a guarded `ALTER TABLE ADD COLUMN` step (PRAGMA table_info check) adding `planes.departure_sim_time` and `sim_state.last_real_tick_ms` to pre-v0.7.0 files, then fixes up the ALTER-default `last_real_tick_ms = 0` to "now" so old DBs resume cleanly instead of taking one clamped catch-up jump
|
||
- **e2e test hook** (test-only affordance): new `POST /api/test/expire-contract`, enabled ONLY when `FLIGHT_TEST_HOOKS=1` (set in docker-compose.yml for the e2e instance; 404 everywhere else) — with `{contractId}` it moves that open contract's deadline ~10 game-seconds out, without a body it creates a fresh short-deadline open contract; lets the suite observe expiry at scale 60 instead of waiting hours
|
||
- Tests: unit — new `lib/sim/movement.test.ts` (duration/arrival math, progress boundaries incl. just-before-arrival, slerp endpoints/on-arc/equator-exact/same-point, fuel burn + zero clamp) and `lib/sim/engine.test.ts` (fake-timer clock advance × timeScale, pause freeze + clean resume, 1-hour-gap clamp to exactly one tick, same-ms no-op; expiry flips only open past-deadline contracts incl. the inclusive edge; finalize awards cash+rep exactly once with right remaining fuel and does nothing pre-arrival or without route info; spawn tops up to target via the shared builder — deterministic for an injected RNG); `lib/game/seed.test.ts` gains generateContract coverage (formula, window, determinism, deadlineOffsetMs, buildSeed ≡ builder output) and the simState assertion now includes lastRealTickMs; new `app/api/sim/route.test.ts` (200 shape, clamp to [1,60], pause freeze across ticks, 400 matrix); `lib/db/client.test.ts` gains a legacy-DB migration test; e2e — new `e2e/sim.spec.ts`: scale-60 clock advance bounded per tick by the clamp, exact freeze while paused, expiry of a hook-shortened deadline + market staying ≥ target (both mutate and create hook modes), POST /api/sim validation/clamping
|
||
- **Known interaction with v0.6.0 e2e**: `e2e/state.spec.ts` restart-persistence test previously asserted the sim clock was byte-identical across a container restart — impossible under tick-on-read by design (every GET advances ≤ one clamped tick). The assertion now checks persistence properly: account/planes/contracts still byte-identical, and post-restart `clockMs ≥ before` bounded by `MAX_TICK_ELAPSED_MS × timeScale`. `e2e/api.spec.ts` endpoint list updated to four
|
||
- Verified: unit suite 47 → 86 green; `tsc --noEmit` clean; e2e 11 → 16 green against the rebuilt compose instance (incl. restart-persistence); live curl of POST /api/sim + GET /api/state shows the clock advancing at scale 60 then frozen on pause
|
||
|
||
## 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)
|