diff --git a/CHANGELOG.md b/CHANGELOG.md index 49d57fb..965ef6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## v0.11.0 — 2026-09-18 07:25 +- **Map interactions per PLAN.md "Middle — Map"** (`components/Map.tsx` now receives the shell's polled `/api/state` snapshot from `app/page.tsx`, same single poll loop as the panes): (a) airport popup expanded — name + ICAO kept, plus **approximate local time** and a **generated weather** line; (b) **animated plane markers** — every plane rendered on the map at its polled lat/lon (server computes flying positions analytically each tick), grounded planes as small sky-blue circles at their airport, flying planes bigger + green so they stand out; movement between 1 s polls is acceptable for this increment (no client-side interpolation); (c) **plane status popup** on click — catalog model label ("ATR 72-600"), status, fuel "x / y L", and while flying the contract route "originName → destName"; (d) **active contract routes highlighted** — one origin→dest line per accepted contract: dashed muted slate when pending (accepted, not yet dispatched), solid bright amber once in-flight +- **Pure deterministic weather generator** `lib/weather.ts`: `weatherFor(icao: string, dayKey?: number)` → `{condition: 'Clear'|'Cloudy'|'Rain'|'Fog', windDirDeg, windKt, visibilityM}` — seeded by FNV-1a hash of the ICAO through mulberry32 (same PRNG as game seeding), so the same airport always shows the same weather across polls/sessions; optional `dayKey` varies it per sim-day. Display-only per PLAN.md — no gameplay effects +- **Approximate local time** `lib/localTime.ts`: zone band N = round(lon/15) with the Etc/GMT±N POSIX sign flip (UTC+1 is "Etc/GMT-1"); formatted via `Intl.DateTimeFormat` as a UTC-format of a shifted timestamp so it works on runtimes whose reduced ICU lacks the positive-offset Etc zones. Labelled "(approx)" in the popup — no DST, no political boundaries +- **Pure GeoJSON builders** `lib/mapData.ts`: `planesGeoJson(state)` (one Point per plane with popup properties; catalog label resolved from the snapshot) and `routesGeoJson(state, airportsByIcao)` (accepted contracts only; kind 'inflight' iff its planeId's status is 'flying'; endpoints missing from the dataset are skipped, never guessed) — kept out of the component so the classification logic is unit-testable +- **New layers coexist with the ensure-on-styledata pattern**: `ensureAirportsLayer` became `ensureLayers` and now idempotently re-establishes all six sources/layers (airports circles; routes-pending + routes-inflight line layers over one 'routes' source, added first so lines render under planes; planes-grounded + planes-flying circle layers over one 'planes' source) on every styledata event — `map.setStyle()` still wipes everything not in the new style and this re-adds it all. Latest snapshot + airport coordinate index live in refs (`stateRef`/`airportsIndexRef`) so both the mount effect's styledata wiring and a `[state]` effect can push data: every poll calls `setData` on the two dynamic sources (no-op until map/sources exist; routes wait for the airports fetch), and `ensureLayers` re-pushes after each re-establish. Layer-scoped click listeners are off+on'd with the same references as before, so no duplicate popups +- **No runways in the popup**: the OurAirports export (`data/airports_eu.csv`) has no runways column (closest usable extras: `elevation_ft`, `municipality`, `iata_code` — none shown this increment), so per "don't fabricate" the field is omitted +- No API changes: `/api/state` shape untouched, OpenAPI spec unchanged; test hooks (`/api/test/create-contract`) reused as-is +- Tests: unit — new `lib/weather.test.ts` (determinism same ICAO → identical weather; 10 ICACOs mostly distinct; all values in sane ranges incl. per-condition visibility bands; dayKey determinism + effect), `lib/localTime.test.ts` (Etc/GMT±N sign flip, band rounding/clamping, HH:MM formatting, midnight wrap both directions) and `lib/mapData.test.ts` (plane features at polled coords with catalog label/fuel/routeText incl. missing-contract fallback; route classification open/completed/expired → nothing, accepted-undispatched → pending, accepted+flying → inflight, grounded-plane defensive case, missing endpoint skipped). e2e — new `e2e/map-interactions.spec.ts`: (1) airport popup keeps name+ICAO and gains local-time (HH:MM) + weather (condition + kt) fields; (2) hook-created EDDF→EDDV contract accepted → pending route line feature appears in the 'routes' source, dispatch at scale 60 → flying plane feature appears in 'planes', position changes across two /api/state polls, route line flips to inflight for that contract id, clicking the flying plane (fresh fetch+project in one evaluate) opens a popup with model label + "flying" + fuel + "Frankfurt Main Airport → Hannover Airport"; flight completed and starter flown home so the live instance stays in its onboarding state; scale restored to 1×/unpaused in `afterEach` +- Verified: unit suite 171 → 192 green; `tsc --noEmit` clean; e2e 25 → 27 green against the rebuilt compose instance (all pre-existing specs — map, sim, state-restart, actions full-loop, panes, right-pane, api — still pass) + ## v0.10.1 — 2026-09-18 06:36 - **Bugfix (reported): dispatch picker selections appeared shared across accepted contracts** — selecting a plane in one contract's dispatch picker made every other accepted contract's picker show that same plane until the page was refreshed. Root cause confirmed by code audit of `components/RightPane.tsx`: NOT a single shared state variable and NOT missing list keys (each row already had its own `useState` inside `DispatchControls` and stable `key={contract.id}`) — it was the "effective selection" fallback, which auto-tracked the **first eligible plane** in every row whenever no explicit pick existed: with one grounded plane all pickers displayed that same plane by default, so any selection looked like it propagated to the other rows; additionally there was no placeholder `