Finalizes the closed issue #117 spec: the world clock runs at the fixed WORLD_TIME_SCALE = 24 while any plane is flying or in repair, and stays frozen when the whole fleet is grounded. Replaces v0.56.0's idle slowdown (IDLE_WORLD_TIME_SCALE, ISS-128).
Production rate is exactly 24x regardless of legacy stored values
The live instance ran at ×1000 because pre-v0.52.0 saves hold timeScale: 1000 (the old UI ladder) — and the production compose set FLIGHT_TEST_HOOKS=1, so nothing ignored it. Now:
tickOnce in lib/sim/engine.ts: advanceMs = advance ? min(elapsed, MAX_TICK_ELAPSED_MS) × scale : 0 where scale = FLIGHT_TEST_HOOKS === '1' ? sim.timeScale : WORLD_TIME_SCALE. Production (env var unset) always advances at ×24 no matter what the save holds; only CI/test deployments honor the stored override written by POST /api/test/set-timescale (range [1, 100_000] — kept for tests per #117).
Separate finding + fix: production docker-compose.yml set FLIGHT_TEST_HOOKS: "1" — it enabled the /api/test/* hooks in prod AND made the live instance honor its stale stored 1000. Removed (with a comment explaining why it must stay out); e2e runs only in CI against docker-compose.ci.yml, which sets the var itself, so CI is unaffected.
No DB migration — per #117 saves reset when multiplayer ships; the env gate needs no schema change.
Freeze semantics (kept from first revision)
A frozen tick skips expiry/delivery/repair/spawn entirely and only updates lastRealTickMs, so resuming activity never fast-forwards the frozen span. The client mirror (advanceSimClock in lib/useSimClock.ts) holds its anchor value while grounded, so polls re-anchor without any jump. Removed: IDLE_WORLD_TIME_SCALE from lib/worldClock.ts. Unchanged on purpose: the ~1 s background tick (started at boot), the test hook endpoint, and the inert legacy columns paused/lastSelectedTimeScale (nothing reads them). The OpenAPI description of /api/state ("while no plane is flying or in repair the clock stays frozen") now matches the code.
UI time controls — already gone
The #117 spec's "remove ALL player-facing time controls" was completed in v0.52.0 (commit a0560ec): LeftPane pause button + scale slider and POST /api/sim were deleted then; no component renders any ladder/pause today (verified: full testid inventory of all components, no /api/sim route, prod v0.59.0 serves none), and e2e/panes.spec.ts:143-145 already pins their absence (pause-toggle, time-scale-slider → count 0). Nothing to remove in this PR; that e2e assertion is kept as the guard.
Tests
Unit: lib/sim/engine.test.ts — both rate branches pinned: with FLIGHT_TEST_HOOKS=1 stubbed (file default, like CI) stored overrides are honored; a new production test unsets the var and proves a legacy stored timeScale: 1000 still advances at exactly ×24. Grounded-fleet tests pin a FROZEN clock (no idle creep, resume-without-catch-up after takeoff, raised timeScale ignored while grounded); describe renamed to "grounded fleet freezes the world clock (#135)" and pins WORLD_TIME_SCALE = 24.
E2E: e2e/sim.spec.ts's idle test becomes an exact freeze (delta === 0 after ~2.5 s of idleness); e2e/state.spec.ts's restart test tightens from bounded delta to EXACT clock equality across docker compose restart; e2e/right-pane.spec.ts verifies the frozen clock, then asserts the abandon-penalty preview matches the server exactly (zero drift under a frozen clock — replaces the DRIFT_MS slope tolerance); e2e/events.spec.ts relaxes strict clock advance between SSE frames to non-decreasing (a grounded fleet pushes identical-clockMs frames; advance itself is pinned in sim.spec.ts). The activateWorld/deactivateWorld wrappers are unchanged — a repairing plane still means full scale.
Verification (local, per AGENTS.md)
Rebased onto master after v0.61.0 (#137) merged — no conflicts beyond the expected package.json/CHANGELOG version entries; no spec depends on the old 1119-airport dataset size.
npx tsc --noEmit — clean
npx vitest run — 386/386 passed (incl. v0.61.0's new airport tests)
npx playwright test --list — 62 tests in 16 files
Closes #135
Finalizes the closed issue #117 spec: the world clock runs at the fixed `WORLD_TIME_SCALE = 24` while any plane is flying or in repair, and stays **frozen** when the whole fleet is grounded. Replaces v0.56.0's idle slowdown (`IDLE_WORLD_TIME_SCALE`, ISS-128).
## Production rate is exactly 24x regardless of legacy stored values
The live instance ran at ×1000 because pre-v0.52.0 saves hold `timeScale: 1000` (the old UI ladder) — and the production compose set `FLIGHT_TEST_HOOKS=1`, so nothing ignored it. Now:
- `tickOnce` in `lib/sim/engine.ts`: `advanceMs = advance ? min(elapsed, MAX_TICK_ELAPSED_MS) × scale : 0` where **`scale = FLIGHT_TEST_HOOKS === '1' ? sim.timeScale : WORLD_TIME_SCALE`**. Production (env var unset) always advances at ×24 no matter what the save holds; only CI/test deployments honor the stored override written by `POST /api/test/set-timescale` (range [1, 100_000] — kept for tests per #117).
- **Separate finding + fix:** production `docker-compose.yml` set `FLIGHT_TEST_HOOKS: "1"` — it enabled the `/api/test/*` hooks in prod AND made the live instance honor its stale stored 1000. Removed (with a comment explaining why it must stay out); e2e runs only in CI against `docker-compose.ci.yml`, which sets the var itself, so CI is unaffected.
- No DB migration — per #117 saves reset when multiplayer ships; the env gate needs no schema change.
## Freeze semantics (kept from first revision)
A frozen tick skips expiry/delivery/repair/spawn entirely and only updates `lastRealTickMs`, so resuming activity never fast-forwards the frozen span. The client mirror (`advanceSimClock` in `lib/useSimClock.ts`) holds its anchor value while grounded, so polls re-anchor without any jump. Removed: `IDLE_WORLD_TIME_SCALE` from `lib/worldClock.ts`. Unchanged on purpose: the ~1 s background tick (started at boot), the test hook endpoint, and the inert legacy columns `paused`/`lastSelectedTimeScale` (nothing reads them). The OpenAPI description of `/api/state` ("while no plane is flying or in repair the clock stays frozen") now matches the code.
## UI time controls — already gone
The #117 spec's "remove ALL player-facing time controls" was completed in v0.52.0 (commit a0560ec): LeftPane pause button + scale slider and `POST /api/sim` were deleted then; no component renders any ladder/pause today (verified: full testid inventory of all components, no `/api/sim` route, prod v0.59.0 serves none), and `e2e/panes.spec.ts:143-145` already pins their absence (`pause-toggle`, `time-scale-slider` → count 0). Nothing to remove in this PR; that e2e assertion is kept as the guard.
## Tests
- Unit: `lib/sim/engine.test.ts` — both rate branches pinned: with `FLIGHT_TEST_HOOKS=1` stubbed (file default, like CI) stored overrides are honored; a new production test unsets the var and proves a legacy stored `timeScale: 1000` still advances at exactly ×24. Grounded-fleet tests pin a FROZEN clock (no idle creep, resume-without-catch-up after takeoff, raised timeScale ignored while grounded); describe renamed to "grounded fleet freezes the world clock (#135)" and pins `WORLD_TIME_SCALE = 24`.
- E2E: `e2e/sim.spec.ts`'s idle test becomes an exact freeze (delta === 0 after ~2.5 s of idleness); `e2e/state.spec.ts`'s restart test tightens from bounded delta to EXACT clock equality across `docker compose restart`; `e2e/right-pane.spec.ts` verifies the frozen clock, then asserts the abandon-penalty preview matches the server exactly (zero drift under a frozen clock — replaces the DRIFT_MS slope tolerance); `e2e/events.spec.ts` relaxes strict clock advance between SSE frames to non-decreasing (a grounded fleet pushes identical-clockMs frames; advance itself is pinned in sim.spec.ts). The activateWorld/deactivateWorld wrappers are unchanged — a repairing plane still means full scale.
## Verification (local, per AGENTS.md)
Rebased onto master after v0.61.0 (#137) merged — no conflicts beyond the expected package.json/CHANGELOG version entries; no spec depends on the old 1119-airport dataset size.
- `npx tsc --noEmit` — clean
- `npx vitest run` — 386/386 passed (incl. v0.61.0's new airport tests)
- `npx playwright test --list` — 62 tests in 16 files
kamilkrzywda
scheduled this pull request to auto merge when all checks succeed 2026-09-23 09:42:10 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #135
Finalizes the closed issue #117 spec: the world clock runs at the fixed
WORLD_TIME_SCALE = 24while any plane is flying or in repair, and stays frozen when the whole fleet is grounded. Replaces v0.56.0's idle slowdown (IDLE_WORLD_TIME_SCALE, ISS-128).Production rate is exactly 24x regardless of legacy stored values
The live instance ran at ×1000 because pre-v0.52.0 saves hold
timeScale: 1000(the old UI ladder) — and the production compose setFLIGHT_TEST_HOOKS=1, so nothing ignored it. Now:tickOnceinlib/sim/engine.ts:advanceMs = advance ? min(elapsed, MAX_TICK_ELAPSED_MS) × scale : 0wherescale = FLIGHT_TEST_HOOKS === '1' ? sim.timeScale : WORLD_TIME_SCALE. Production (env var unset) always advances at ×24 no matter what the save holds; only CI/test deployments honor the stored override written byPOST /api/test/set-timescale(range [1, 100_000] — kept for tests per #117).docker-compose.ymlsetFLIGHT_TEST_HOOKS: "1"— it enabled the/api/test/*hooks in prod AND made the live instance honor its stale stored 1000. Removed (with a comment explaining why it must stay out); e2e runs only in CI againstdocker-compose.ci.yml, which sets the var itself, so CI is unaffected.Freeze semantics (kept from first revision)
A frozen tick skips expiry/delivery/repair/spawn entirely and only updates
lastRealTickMs, so resuming activity never fast-forwards the frozen span. The client mirror (advanceSimClockinlib/useSimClock.ts) holds its anchor value while grounded, so polls re-anchor without any jump. Removed:IDLE_WORLD_TIME_SCALEfromlib/worldClock.ts. Unchanged on purpose: the ~1 s background tick (started at boot), the test hook endpoint, and the inert legacy columnspaused/lastSelectedTimeScale(nothing reads them). The OpenAPI description of/api/state("while no plane is flying or in repair the clock stays frozen") now matches the code.UI time controls — already gone
The #117 spec's "remove ALL player-facing time controls" was completed in v0.52.0 (commit
a0560ec): LeftPane pause button + scale slider andPOST /api/simwere deleted then; no component renders any ladder/pause today (verified: full testid inventory of all components, no/api/simroute, prod v0.59.0 serves none), ande2e/panes.spec.ts:143-145already pins their absence (pause-toggle,time-scale-slider→ count 0). Nothing to remove in this PR; that e2e assertion is kept as the guard.Tests
lib/sim/engine.test.ts— both rate branches pinned: withFLIGHT_TEST_HOOKS=1stubbed (file default, like CI) stored overrides are honored; a new production test unsets the var and proves a legacy storedtimeScale: 1000still advances at exactly ×24. Grounded-fleet tests pin a FROZEN clock (no idle creep, resume-without-catch-up after takeoff, raised timeScale ignored while grounded); describe renamed to "grounded fleet freezes the world clock (#135)" and pinsWORLD_TIME_SCALE = 24.e2e/sim.spec.ts's idle test becomes an exact freeze (delta === 0 after ~2.5 s of idleness);e2e/state.spec.ts's restart test tightens from bounded delta to EXACT clock equality acrossdocker compose restart;e2e/right-pane.spec.tsverifies the frozen clock, then asserts the abandon-penalty preview matches the server exactly (zero drift under a frozen clock — replaces the DRIFT_MS slope tolerance);e2e/events.spec.tsrelaxes strict clock advance between SSE frames to non-decreasing (a grounded fleet pushes identical-clockMs frames; advance itself is pinned in sim.spec.ts). The activateWorld/deactivateWorld wrappers are unchanged — a repairing plane still means full scale.Verification (local, per AGENTS.md)
Rebased onto master after v0.61.0 (#137) merged — no conflicts beyond the expected package.json/CHANGELOG version entries; no spec depends on the old 1119-airport dataset size.
npx tsc --noEmit— cleannpx vitest run— 386/386 passed (incl. v0.61.0's new airport tests)npx playwright test --list— 62 tests in 16 files65226a9375to71dea3ffb871dea3ffb8tobb4e3b96f3