diff --git a/CHANGELOG.md b/CHANGELOG.md index cae7e87..51d8eed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v0.19.0 — 2026-09-19 18:05 +- **Auto-slow to 1× when all planes have landed (ISS-011).** When the last plane in flight lands, the sim slows down on its own so an idle fleet doesn't sit at 1000×. Server-side and authoritative: `tickOnce` (`lib/sim/engine.ts`) now remembers how many planes `deliverPackages` finalized during a tick, and when ≥1 plane landed this tick AND no plane remains with status `'flying'` AND the sim is not paused, it persists `timeScale = 1` on the same `sim_state` write as the clock — so the UI slider reflects it on the very next snapshot (no API or OpenAPI change: timeScale is still set via the existing `POST /api/sim`, and `clampTimeScale` semantics are untouched since 1 is a valid ladder value) +- **Arrival-triggered, not idle-enforced.** The trigger fires only on the tick that processes a landing — a grounded fleet at high scale with no flight ever happening is left alone (this keeps the existing slider e2e in `panes.spec.ts` green: it sets 1000× and expects the server to confirm it while nothing is flying). Paused state is untouched: while paused no landings are processed, so a paused sim never auto-slows +- **No auto-restore by design.** The previous scale is not remembered — once the game slows to 1× after the last landing, the player speeds up manually again (slider or `POST /api/sim`). Deliberately minimal scope: no "resume my old speed" behavior +- Tests: unit — new `describe('auto-slow …')` block in `lib/sim/engine.test.ts`: last plane lands → stored timeScale becomes 1; a landing while another plane is still flying → no change; already at 1× → stays 1; paused sim with all planes landed → no change. e2e — new `e2e/auto-slow.spec.ts` (serial mode, SCALE=1000, self-heal fly-home in afterEach like relocation.spec.ts): sets 1000× via the API, dispatches plane-starter on the shortest leg from its hub (EDDF → nearest airport), waits for the landing and asserts the server's `timeScale` is exactly 1 in that snapshot, then proves manual override still works (`POST /api/sim {timeScale: 1000}` confirmed by response + fresh snapshot) +- **e2e suite adapted to the new behavior.** Every existing spec that flies out-and-back relied on 1000× persisting across a landing — with ISS-011 it no longer does, so each return leg now re-sets SCALE explicitly: `flyHomeViaApi` in actions/relocation/right-pane/hover (re-set before the repositioning flight after waiting for an in-flight leg to land) and the restore legs inside the test bodies of actions.spec.ts (core loop, expired packages, validation paths ×2), right-pane.spec.ts (fly-to-destination, refuel-for-leg) and hover.spec.ts (flying-plane route highlight). map-interactions.spec.ts needed no change — its `loadAndFly` helper already re-sets SCALE before every landing wait +- Verified: `tsc --noEmit` clean; unit suite green locally; e2e not run locally by design — CI verifies against an isolated stack + ## v0.18.0 — 2026-09-19 16:02 - **Relocate a grounded (empty) plane to any airport (ISS-010).** `flyPlane` has allowed pure repositioning since v0.12.0, but the UI only exposed flying for planes that carried cargo — an empty grounded plane had no fly control anywhere. Two new surfaces now drive the EXISTING `POST /api/planes/{id}/fly {destIcao}` endpoint (no API or OpenAPI change): - **Right-pane market row** (`components/RightPane.tsx`): a fourth `.market-row` with `[data-testid="relocate-plane"]` (grounded planes, "ATR 72-600 — EDDF" labels), `[data-testid="relocate-dest"]` (all airports from a one-time `GET /api/airports` fetch per page life — same promise-ref pattern as Map.tsx — sorted by name for stable e2e selection) and the `[data-testid="relocate-go"]` "Relocate" button, disabled while busy or until both selects have a value. Same effective-selection + `runAction` pattern as the buy/sell/refuel rows (first grounded plane / first airport preselected); the row wraps (`market-row-relocate`) since two selects don't fit one 360 px line diff --git a/e2e/actions.spec.ts b/e2e/actions.spec.ts index 02302d0..6faad17 100644 --- a/e2e/actions.spec.ts +++ b/e2e/actions.spec.ts @@ -117,6 +117,9 @@ async function flyHomeViaApi(page: Page): Promise { plane = s.planes.find((p) => p.id === 'plane-starter')!; } if (plane.locationIcao !== HUB) { + // ISS-011: the landing above auto-slown the sim to 1× — re-set SCALE so the return leg + // fast-forwards instead of taking real-time hours. + await setScale(page, SCALE); const res = await page.request.post(`/api/planes/${plane.id}/fly`, { data: { destIcao: HUB } }); expect(res.ok()).toBeTruthy(); // repositioning needs no package (v0.12.0 manual flight) await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === 'plane-starter')!.locationIcao), { timeout: 240_000 }).toBe(HUB); @@ -221,7 +224,9 @@ test('full core loop via UI: load → fly to destination → exact reward + rep const refuelCost = Math.round((afterRefuel.maxFuel - landed.fuel) * FUEL_PRICE_PER_L * 100) / 100; expect(s.account.cash).toBeCloseTo(before.account.cash + pkg.reward - refuelCost, 6); - // Restore: fly home (repositioning — no package needed in the v0.12.0 model). + // Restore: fly home (repositioning — no package needed in the v0.12.0 model). ISS-011: the + // arrival above auto-slown the sim to 1×, so re-set SCALE for the return leg. + await setScale(page, SCALE); const homeRes = await page.request.post(`/api/planes/${planeId}/fly`, { data: { destIcao: HUB } }); expect(homeRes.ok()).toBeTruthy(); await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === planeId)!.locationIcao), { timeout: 120_000 }).toBe(HUB); @@ -308,8 +313,10 @@ test('expired packages deliver and abandon with no reward and no penalty', async expect(s.packages.find((p) => p.id === pkgB.id)!.status).toBe('abandoned'); expect(s.account.cash).toBe(cashBeforeAbandon); // free — expired abandons cost nothing - // Restore: refuel + fly home (scale is still SCALE from the expiry wait above). + // Restore: refuel + fly home. ISS-011: the LEMD arrival above auto-slown the sim to 1×, so + // re-set SCALE for the return leg (the expiry wait's scale no longer persists past a landing). await page.request.post('/api/planes/plane-starter/refuel'); + await setScale(page, SCALE); const homeRes = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } }); expect(homeRes.ok()).toBeTruthy(); await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === 'plane-starter')!.locationIcao), { timeout: 240_000 }).toBe(HUB); @@ -347,7 +354,9 @@ test('load and fly validation paths (404 / 409 / 400)', async ({ page }) => { res = await page.request.post(`/api/packages/${pkg.id}/abandon`); expect(res.status()).toBe(409); - // Fly home from the hop so the fuel section starts at the hub. + // Fly home from the hop so the fuel section starts at the hub. ISS-011: the hop arrival above + // auto-slown the sim to 1×, so re-set SCALE for this leg. + await setScale(page, SCALE); res = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } }); expect(res.ok()).toBeTruthy(); await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === 'plane-starter')!.locationIcao), { timeout: 120_000 }).toBe(HUB); @@ -370,6 +379,8 @@ test('load and fly validation paths (404 / 409 / 400)', async ({ page }) => { const atFar = sFar.planes.find((p) => p.id === 'plane-starter')!; expect(atFar.locationIcao).toBe(FAR); // The return leg fits: the weighted burn over it is less than what's left in the tank. + // ISS-011: the FAR arrival above auto-slown the sim to 1×, so re-set SCALE for this leg. + await setScale(page, SCALE); const backRes = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } }); expect(backRes.ok()).toBeTruthy(); await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === 'plane-starter')!.locationIcao), { timeout: 240_000 }).toBe(HUB); diff --git a/e2e/auto-slow.spec.ts b/e2e/auto-slow.spec.ts new file mode 100644 index 0000000..46d7b37 --- /dev/null +++ b/e2e/auto-slow.spec.ts @@ -0,0 +1,130 @@ +import type { Page } from '@playwright/test'; +import { expect, test } from '@playwright/test'; + +// v0.19.0 — ISS-011: when the LAST plane lands (no plane remains in flight) the sim auto-slows +// to 1×, server-side and persisted on the sim_state row so the UI slider reflects it on the next +// snapshot. Arrival-triggered by design: a grounded fleet at high scale is left alone, there is no +// auto-restore — the player speeds up manually again (asserted below). + +const SCALE = 1000; // fastest sim speed — top of the v0.14.0 ladder (same convention as e2e/sim.spec.ts) +const HUB = 'EDDF'; // Frankfurt Main — the seeded starter plane's home base (lib/game/seed.ts) + +interface SimState { + clockMs: number; + timeScale: number; + paused: boolean; +} + +interface StateBody { + simState: SimState; + planes: Array<{ id: string; status: string; locationIcao: string; destIcao: string | null }>; +} + +async function getState(page: Page): Promise { + const res = await page.request.get('/api/state'); + expect(res.ok()).toBeTruthy(); + return (await res.json()) as StateBody; +} + +async function postSim(page: Page, body: Record): Promise { + const res = await page.request.post('/api/sim', { data: body }); + expect(res.status()).toBe(200); +} + +// Set the time scale and VERIFY from the response that it stuck — if something external ever +// resets the live instance's controls mid-test, fail loudly here instead of timing out later. +async function setScale(page: Page, scale: number): Promise { + const res = await page.request.post('/api/sim', { data: { timeScale: scale } }); + expect(res.status()).toBe(200); + const body = (await res.json()) as SimState; + expect(body.timeScale, `timeScale should be ${scale} after POST /api/sim`).toBe(scale); +} + +// Closest airport to the hub from /api/airports → shortest possible leg (fastest e2e flight). +async function nearestToHub(page: Page): Promise<{ icao: string }> { + const res = await page.request.get('/api/airports'); + expect(res.ok()).toBeTruthy(); + const fc = (await res.json()) as { features: Array<{ properties: { icao: string; lat: number; lon: number } }> }; + const hub = fc.features.find((f) => f.properties.icao === HUB)!.properties; + let nearest = fc.features.filter((f) => f.properties.icao !== HUB)[0].properties; + for (const f of fc.features) { + if (f.properties.icao === HUB) continue; + const d = Math.hypot(f.properties.lat - hub.lat, f.properties.lon - hub.lon); + const dn = Math.hypot(nearest.lat - hub.lat, nearest.lon - hub.lon); + if (d < dn) nearest = f.properties; + } + return { icao: nearest.icao }; +} + +// Self-heal helper (same pattern as e2e/relocation.spec.ts): let any in-flight leg finish and +// reposition plane-starter home via the API so every test starts from the canonical state even +// after an interrupted previous run. +async function flyHomeViaApi(page: Page): Promise { + let s = await getState(page); + let plane = s.planes.find((p) => p.id === 'plane-starter')!; + if (plane.status === 'flying' || plane.locationIcao !== HUB) { + // Fast-forward any leftover leg / repositioning flight (~20 s real time worst case at 1000×). + await setScale(page, SCALE); + } + if (plane.status === 'flying') { + // Let the in-flight leg finish before repositioning. While flying, locationIcao stays at the + // DEPARTURE airport — re-read state after landing before deciding whether a repositioning + // flight is needed. + await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === 'plane-starter')!.status), { timeout: 240_000 }).toBe('available'); + s = await getState(page); + plane = s.planes.find((p) => p.id === 'plane-starter')!; + } + if (plane.locationIcao !== HUB) { + // ISS-011: the landing above auto-slown the sim to 1× — re-set SCALE so the return leg + // fast-forwards instead of taking real-time hours. + await setScale(page, SCALE); + const res = await page.request.post(`/api/planes/${plane.id}/fly`, { data: { destIcao: HUB } }); + expect(res.ok()).toBeTruthy(); // repositioning needs no package (v0.12.0 manual flight) + await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === 'plane-starter')!.locationIcao), { timeout: 240_000 }).toBe(HUB); + } +} + +// Self-heal in CLEANUP (afterEach): the test deliberately leaves plane-starter at the far end, so +// fast-forward + reposition home here; then restore scale=1 / unpaused for later specs. +test.afterEach(async ({ page }) => { + await flyHomeViaApi(page); + await postSim(page, { timeScale: 1, paused: false }); +}); + +test.describe.configure({ mode: 'serial' }); // shared live DB — order matters within this file + +test('auto-slow to 1× when the last plane lands; manual speed-up still works (ISS-011)', async ({ page }) => { + test.setTimeout(300_000); // self-heal (worst case ~20 s at 1000×) + one short leg + await flyHomeViaApi(page); // deterministic: the starter sits grounded at EDDF + + const nearest = await nearestToHub(page); + + // Set 1000× via the API and verify it stuck. + await setScale(page, SCALE); + + // Dispatch plane-starter on the shortest leg from its hub (repositioning — no package needed). + const flyRes = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: nearest.icao } }); + expect(flyRes.status()).toBe(200); + + // Wait for the landing… + let landedBody: StateBody | undefined; + await expect + .poll(async () => { + const b = await getState(page); + const p = b.planes.find((x) => x.id === 'plane-starter')!; + if (p.status === 'available' && p.locationIcao === nearest.icao) landedBody = b; + return !!landedBody; + }, { timeout: 120_000, message: `waiting for plane-starter to land at ${nearest.icao}` }) + .toBe(true); + + // …and the server has auto-slown to 1× — persisted on sim_state, so this very snapshot shows it. + expect(landedBody!.simState.timeScale).toBe(1); + + // Manual override still works: POST /api/sim {timeScale: 1000} → confirmed by the response… + const post = await page.request.post('/api/sim', { data: { timeScale: SCALE } }); + expect(post.status()).toBe(200); + expect(((await post.json()) as SimState).timeScale).toBe(SCALE); + // …and by a fresh snapshot (no auto-restore of the previous scale — and no re-slow either, + // since nothing is in flight: the trigger is arrival-based, not idle-enforcement). + expect((await getState(page)).simState.timeScale).toBe(SCALE); +}); diff --git a/e2e/hover.spec.ts b/e2e/hover.spec.ts index 11341ee..5036d05 100644 --- a/e2e/hover.spec.ts +++ b/e2e/hover.spec.ts @@ -89,6 +89,9 @@ async function flyHomeViaApi(page: Page): Promise { .toBe(true); } if (starter.locationIcao !== HUB) { + // ISS-011: the landing above auto-slown the sim to 1× — re-set SCALE so the return leg + // fast-forwards instead of taking real-time hours. + await setScale(page, SCALE); const res = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } }); expect(res.status()).toBe(200); // repositioning needs no package (v0.12.0 manual flight) await expect @@ -291,6 +294,8 @@ test('hovering an on-board block of a flying plane highlights its route', async }, { timeout: 120_000, message: `waiting for plane-starter to land at ${DEST}` }) .toBe(true); + // ISS-011: the arrival above auto-slown the sim to 1×, so re-set SCALE for this leg. + await setScale(page, SCALE); const home = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } }); expect(home.status()).toBe(200); await expect diff --git a/e2e/relocation.spec.ts b/e2e/relocation.spec.ts index 3e21af9..844f04e 100644 --- a/e2e/relocation.spec.ts +++ b/e2e/relocation.spec.ts @@ -72,6 +72,9 @@ async function flyHomeViaApi(page: Page): Promise { plane = s.planes.find((p) => p.id === 'plane-starter')!; } if (plane.locationIcao !== HUB) { + // ISS-011: the landing above auto-slown the sim to 1× — re-set SCALE so the return leg + // fast-forwards instead of taking real-time hours. + await setScale(page, SCALE); const res = await page.request.post(`/api/planes/${plane.id}/fly`, { data: { destIcao: HUB } }); expect(res.ok()).toBeTruthy(); // repositioning needs no package (v0.12.0 manual flight) await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === 'plane-starter')!.locationIcao), { timeout: 240_000 }).toBe(HUB); diff --git a/e2e/right-pane.spec.ts b/e2e/right-pane.spec.ts index 345d5bc..8fd8a89 100644 --- a/e2e/right-pane.spec.ts +++ b/e2e/right-pane.spec.ts @@ -174,6 +174,9 @@ async function flyHomeViaApi(page: Page): Promise { .toBe(true); } if (starter.locationIcao !== HUB) { + // ISS-011: the landing above auto-slown the sim to 1× — re-set SCALE so the return leg + // fast-forwards instead of taking real-time hours. + await setScale(page, SCALE); const res = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } }); expect(res.status()).toBe(200); // repositioning needs no package (v0.12.0 manual flight) await expect @@ -284,6 +287,8 @@ test('on-board list: fly-to-destination + abandon with penalty preview; flying p await expect(onboardRow).not.toBeVisible({ timeout: 15_000 }); // Restore: fly home so the live instance stays in its onboarding state for the next run. + // ISS-011: the arrival above auto-slown the sim to 1×, so re-set SCALE for this leg. + await setScale(page, SCALE); const homeRes = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } }); expect(homeRes.status()).toBe(200); await expect @@ -401,7 +406,9 @@ test('on-board row Refuel tops up just enough for the package leg; market button await expect(page.getByTestId('market-refuel')).toHaveText('Refuel to full'); // Restore: fly home — pkg2's destination IS the hub, so it delivers on arrival. The tank holds - // exactly the leg requirement, so this also proves "just enough" is enough. + // exactly the leg requirement, so this also proves "just enough" is enough. ISS-011: the far-end + // arrival above auto-slown the sim to 1×, so re-set SCALE for this leg. + await setScale(page, SCALE); expect((await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } })).status()).toBe(200); await expect .poll(async () => (await getState(page)).planes.find((p) => p.id === 'plane-starter')?.locationIcao === HUB, { diff --git a/lib/sim/engine.test.ts b/lib/sim/engine.test.ts index 6c77879..fa68488 100644 --- a/lib/sim/engine.test.ts +++ b/lib/sim/engine.test.ts @@ -237,6 +237,80 @@ describe('deliverPackages (v0.12.0 arrival delivery)', () => { }); }); +describe('auto-slow to 1× when the last plane lands (ISS-011)', () => { + const FROM = 'EDDF'; + const TO = 'LEMD'; + + // Starter flying EDDF→LEMD with departureSimTime set so its arrival is EXACTLY T0 — + // any advancing tick past T0 finalizes it. timeScale is the value under test. + async function setupLanding(name: string, timeScale: number) { + const inst = await freshDb(name); + const fc = await loadAirportsGeoJson(); + const from = fc.features.find((f) => f.properties.icao === FROM)!.properties; + const to = fc.features.find((f) => f.properties.icao === TO)!.properties; + const km = haversineKm(from.lat, from.lon, to.lat, to.lon); + const type = PLANE_TYPES.atr72; + + await inst.db.update(schema.simState).set({ timeScale }); + await inst.db.update(schema.planes) + .set({ status: 'flying', destIcao: TO, departureSimTime: T0 - travelMs(km, type.speedKmh), fuel: type.maxFuel }) + .where(eq(schema.planes.id, 'plane-starter')); + + return { inst, from, km, type }; + } + + it('last plane lands → stored timeScale becomes 1', async () => { + const { inst } = await setupLanding('autoslow-last.db', 1000); + vi.setSystemTime(T0 + 2_000); // one full clamped tick at scale 1000 → clock T0+2e6 ≥ arrival (T0) + + const sim = await tickOnce(inst); + + expect(sim.timeScale).toBe(1); // persisted on the sim_state row… + expect((await inst.db.select().from(schema.simState))[0].timeScale).toBe(1); // …and visible to the next snapshot + expect((await inst.db.select().from(schema.planes))[0].status).toBe('available'); // landing itself unchanged + }); + + it('a landing while another plane is still flying → no change', async () => { + const { inst, from, type } = await setupLanding('autoslow-other.db', 1000); + // A second plane on the same route departing at T0 — arrives far after this tick's clock. + await inst.db.insert(schema.planes).values({ + id: 'p-b', model: 'atr72', status: 'flying', fuel: type.maxFuel, maxFuel: type.maxFuel, + locationIcao: FROM, destIcao: TO, lat: from.lat, lon: from.lon, departureSimTime: T0, + }); + + vi.setSystemTime(T0 + 2_000); + const sim = await tickOnce(inst); + + expect(sim.timeScale).toBe(1000); // p-b still in flight — no auto-slow + const byId = new Map((await inst.db.select().from(schema.planes)).map((p) => [p.id, p.status])); + expect(byId.get('plane-starter')).toBe('available'); // the starter did land… + expect(byId.get('p-b')).toBe('flying'); // …but the fleet is not idle + }); + + it('already at 1× → stays 1', async () => { + const { inst } = await setupLanding('autoslow-one.db', 1); + vi.setSystemTime(T0 + 2_000); // one tick at scale 1 advances 2e3 game ms — past the T0 arrival + + const sim = await tickOnce(inst); + + expect(sim.timeScale).toBe(1); + expect((await inst.db.select().from(schema.planes))[0].status).toBe('available'); + }); + + it('paused sim with all planes landed → no change', async () => { + const inst = await freshDb('autoslow-paused.db'); + // The fleet is fully grounded (the seeded starter) — a paused tick must not touch the + // scale even though "no plane in flight" holds: the trigger requires a landing this tick. + await inst.db.update(schema.simState).set({ timeScale: 1000, paused: true }); + + vi.setSystemTime(T0 + 5_000); + const sim = await tickOnce(inst); + + expect(sim.timeScale).toBe(1000); // no landing processed while paused → trigger never fires + expect((await inst.db.select().from(schema.planes))[0].status).toBe('available'); + }); +}); + describe('spawnOpenPackages', () => { it('tops the market up to MARKET_TARGET_OPEN_PACKAGES using the shared seed builder', async () => { const inst = await freshDb('spawn.db'); diff --git a/lib/sim/engine.ts b/lib/sim/engine.ts index 123d97c..5080acd 100644 --- a/lib/sim/engine.ts +++ b/lib/sim/engine.ts @@ -71,17 +71,33 @@ export async function tickOnce(inst: GameDb): Promise { const advanceMs = sim.paused ? 0 : Math.min(elapsed, MAX_TICK_ELAPSED_MS) * sim.timeScale; const clockMs = Math.round(sim.clockMs + advanceMs); + let landedThisTick = 0; if (advanceMs > 0) { await expirePackages(inst, clockMs); - await deliverPackages(inst, clockMs); + // ISS-011: remember the landings so the tick can auto-slow to 1× when this was + // the last plane in flight. + landedThisTick = await deliverPackages(inst, clockMs); await spawnOpenPackages(inst, clockMs); // AFTER delivery on purpose: a plane that just landed gets work spawned // at its new location within the same tick. await spawnCoveragePackages(inst, clockMs); } + const sets: Partial> = { + clockMs, + lastRealTickMs: nowMs, + }; + // Auto-slow (ISS-011): when ≥1 plane landed this tick and NO plane remains in flight, + // drop the stored timeScale to 1× — persisted here so the UI slider reflects it on the + // next snapshot. Arrival-triggered by design: a grounded fleet at high scale is left + // alone (the player speeds up manually again; there is no auto-restore). + if (landedThisTick > 0 && !sim.paused) { + const flying = await inst.db.select().from(schema.planes).where(eq(schema.planes.status, 'flying')); + if (flying.length === 0) sets.timeScale = MIN_TIME_SCALE; + } + // Persist lastRealTickMs even when nothing advanced — it is the tick anchor. - await inst.db.update(schema.simState).set({ clockMs, lastRealTickMs: nowMs }); + await inst.db.update(schema.simState).set(sets); await persist(inst); const updated = (await inst.db.select().from(schema.simState))[0]; if (!updated) throw new Error('sim_state row missing after tick'); diff --git a/package.json b/package.json index adc239c..5f533a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flight-ops", - "version": "0.18.0", + "version": "0.19.0", "private": true, "scripts": { "dev": "next dev -p 3000",