diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7719f42..54ad9a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## v0.41.0 — 2026-09-21 13:26
+- **Queue UI fixes (ISS-077).** Three queue-related map/UI fixes, no server logic changed:
+ - **Would-409 controls are disabled.** The on-board "Fly to …" button (`components/LeftPane.tsx`) is additionally disabled while the plane is flying and its destination is already active or queued (title "Already in flight queue"); the Relocate section disables its GO button for the same reason AND renders the selected plane's active/queued airports as native disabled `
))}
@@ -560,7 +572,7 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
type="button"
data-testid="relocate-go"
className="pane-btn"
- disabled={actionsBusy || relocatePlane === null || relocateDest === null}
+ disabled={actionsBusy || relocatePlane === null || relocateDest === null || relocateBlocked}
// ISS-053 (v0.32.0): a flying plane can't start another leg — queue it instead, so the
// relocation takes off when the current one lands.
onClick={() => {
diff --git a/components/Map.tsx b/components/Map.tsx
index 51ce84b..c360eab 100644
--- a/components/Map.tsx
+++ b/components/Map.tsx
@@ -153,10 +153,20 @@ export default function FlightMap({ state, hovered, applySnapshot }: MapProps) {
// ISS-040 (v0.23.2): an on-board package's trace starts from the carrier plane's LIVE position,
// not its origin airport — identical to before while grounded at the pickup, different once it
// has departed with cargo still loaded. No carrier in state yet → fall back to the ICAO trace.
+ // ISS-077 (v0.41.0): a FLYING carrier's trace starts at its LANDING airport (destIcao), not the
+ // live mid-flight position — the hover previews what happens AFTER this leg lands, and the solid
+ // active-leg + dotted queue layers already show where it is now. Grounded / no active leg keeps
+ // today's behavior exactly (live position = the plane's airport). destIcao missing from the
+ // index → fall back to the live position rather than guessing.
const idx = airportsIndexRef.current ?? new Map();
const carrier = h?.kind === 'package' && h.planeId ? stateRef.current?.planes.find((p) => p.id === h.planeId) : undefined;
+ const landing = carrier?.status === 'flying' && carrier.destIcao ? idx.get(carrier.destIcao) : undefined;
void hoverSource.setData(
- carrier ? hoverTraceGeoJson({ lat: carrier.lat, lon: carrier.lon }, toIcao, idx) : hoverRouteGeoJson(fromIcao, toIcao, idx),
+ landing
+ ? hoverTraceGeoJson(landing, toIcao, idx)
+ : carrier
+ ? hoverTraceGeoJson({ lat: carrier.lat, lon: carrier.lon }, toIcao, idx)
+ : hoverRouteGeoJson(fromIcao, toIcao, idx),
);
}
// Package hovers ring BOTH endpoints (v0.15.0 behavior); 'route' hovers ring the TARGET only —
diff --git a/e2e/hover.spec.ts b/e2e/hover.spec.ts
index f8a1bf9..b95c827 100644
--- a/e2e/hover.spec.ts
+++ b/e2e/hover.spec.ts
@@ -15,6 +15,8 @@ const SCALE = 1000; // fastest sim speed (v0.14.0 ladder top) — keeps flights
const HUB = 'EDDF'; // must match HUB_ICAO in lib/game/seed.ts
// Madrid–Barajas — same long leg as e2e/map-interactions.spec.ts: ~1420 km → ~10 s at scale 1000.
const DEST = 'LEMD';
+// Amsterdam — a third airport, distinct from hub and destination (the queued stop in the ISS-077 test).
+const QUEUE = 'EHAM';
interface PlaneState {
id: string;
@@ -500,7 +502,7 @@ test('hovering the fleet row of a flying plane highlights its route', async ({ p
.toBe(true);
});
-test('hovering an on-board package while its carrier is mid-flight traces from current position', async ({ page }) => {
+test('hovering an on-board package while its carrier is mid-flight traces from the landing airport', async ({ page }) => {
test.setTimeout(600_000); // self-heal + one flight at scale 1000 + fly-home cleanup
await flyHomeViaApi(page);
@@ -511,44 +513,42 @@ test('hovering an on-board package while its carrier is mid-flight traces from c
await page.goto('/');
await waitForAirportCount(page);
- // Load + fly via the API (same pattern as e2e/map-interactions.spec.ts), then speed up.
- const pkg = await createPackage(page, HUB, DEST);
+ // ISS-077 (v0.41.0): the package is destined for the QUEUED stop (EHAM), not the active leg's
+ // destination — load it, queue EHAM while grounded (pre-built routes are allowed), then start the
+ // HUB→DEST leg via the API (same pattern as e2e/map-interactions.spec.ts). The hover must trace
+ // from DEST (where this flight lands) to EHAM — NOT from the plane's live mid-flight position.
+ const pkg = await createPackage(page, HUB, QUEUE);
const load = await page.request.post(`/api/packages/${pkg.id}/load`, { data: { planeId: 'plane-starter' } });
expect(load.status()).toBe(200);
+ const queueRes = await page.request.post('/api/planes/plane-starter/queue', { data: { destIcao: QUEUE } });
+ expect(queueRes.status()).toBe(200);
const fly = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: DEST } });
expect(fly.status()).toBe(200);
- await setScale(page, SCALE);
+ // The endpoint's OWN post-action snapshot already shows the plane flying — no poll race.
+ const flyBody = (await fly.json()) as StateBody;
+ expect(flyBody.planes.find((p) => p.id === 'plane-starter')?.status).toBe('flying');
- // Wait until the starter is GENUINELY mid-flight — its live position clearly away from EDDF in
- // both axes (well past HOVER_ROUTE_EPS_DEG) so "the trace starts at the plane" and "it starts at
- // the pickup airport" are distinguishable. The ~10 s leg at scale 1000 gives a wide window.
- const hubLonLat = await airportLonLat(page, HUB);
- await expect
- .poll(async () => {
- const p = (await getState(page)).planes.find((x) => x.id === 'plane-starter');
- if (!p || p.status !== 'flying') return false;
- return Math.abs(p.lon - hubLonLat[0]) > 0.2 && Math.abs(p.lat - hubLonLat[1]) > 0.2;
- }, { timeout: 60_000, message: 'waiting for plane-starter to be clearly away from the hub' })
- .toBe(true);
-
- // Freeze mid-flight so the position is stable while we assert (same pause pattern as the
- // flying-plane hover test above): paused, every /api/state snapshot reports the same lat/lon.
+ // Freeze immediately: at a leftover scale of 1000 the ~10 s leg could otherwise land under us and
+ // auto-chain the queued EHAM leg, which would change (or empty) the trace while we assert. Paused,
+ // every /api/state snapshot reports the same state — and the new rule's start point is the LANDING
+ // airport regardless of progress, so no mid-flight position window is needed at all.
await postSim(page, { paused: true });
const row = page.locator(`[data-testid="onboard-package-${pkg.id}"]`);
await expect(row, 'the loaded package appears in the On board section').toBeVisible({ timeout: 15_000 });
- // Both endpoints must be in-viewport for queryRenderedFeatures to see the trace.
- await fitToAirports(page, [HUB, DEST]);
+ // Both trace endpoints must be in-viewport for queryRenderedFeatures to see them.
+ await fitToAirports(page, [DEST, QUEUE]);
- const live = (await getState(page)).planes.find((x) => x.id === 'plane-starter')!;
- const liveLonLat: [number, number] = [live.lon, live.lat]; // stable — the sim is paused
const destLonLat = await airportLonLat(page, DEST);
+ const queueLonLat = await airportLonLat(page, QUEUE);
+ const hubLonLat = await airportLonLat(page, HUB);
await row.hover();
- // ISS-040 (v0.23.2): the trace starts at the carrier's LIVE position and ends at the destination,
- // and NO segment may start near the pickup airport (the old origin-based behavior). Same per-tile
- // clipping/quantization caveats as expectHoverRoute — check endpoints across all segments with EPS.
+ // ISS-077 (v0.41.0): the trace starts at the LANDING airport (DEST) and ends at the package's
+ // destination (the queued stop EHAM), and NO segment may start near the departure airport — that
+ // was the old live-position behavior right after takeoff. Same per-tile clipping/quantization
+ // caveats as expectHoverRoute — check endpoints across all segments with EPS.
await expect.poll(async () => {
const feats = (await renderedFeatures(page, ['hover-route'])) as Array<{ geometry?: { type: string; coordinates: [number, number][] } }>;
const segs = feats.filter(
@@ -558,11 +558,14 @@ test('hovering an on-board package while its carrier is mid-flight traces from c
if (!segs.length) return false;
const near = (a: [number, number], b: [number, number]) =>
Math.abs(a[0] - b[0]) <= HOVER_ROUTE_EPS_DEG && Math.abs(a[1] - b[1]) <= HOVER_ROUTE_EPS_DEG;
- const startsAtLive = segs.some((f) => near(f.geometry.coordinates[0], liveLonLat));
- const endsAtDest = segs.some((f) => near(f.geometry.coordinates[f.geometry.coordinates.length - 1], destLonLat));
+ const startsAtDest = segs.some((f) => near(f.geometry.coordinates[0], destLonLat));
+ const endsAtQueue = segs.some((f) => {
+ const c = f.geometry.coordinates;
+ return near(c[c.length - 1], queueLonLat);
+ });
const noneStartsAtHub = !segs.some((f) => near(f.geometry.coordinates[0], hubLonLat));
- return startsAtLive && endsAtDest && noneStartsAtHub;
- }, { timeout: 15_000, message: 'waiting for the hover-route trace to run from the live position to the destination' }).toBe(true);
+ return startsAtDest && endsAtQueue && noneStartsAtHub;
+ }, { timeout: 15_000, message: 'waiting for the hover-route trace to run from the landing airport (DEST) to the queued stop' }).toBe(true);
await unhover(page);
await expect.poll(async () => (await renderedFeatures(page, ['hover-route'])).length, {
@@ -570,9 +573,12 @@ test('hovering an on-board package while its carrier is mid-flight traces from c
message: 'waiting for the hover route trace to clear after mouseleave',
}).toBe(0);
- // Resume and fast-forward to arrival — landing at DEST auto-delivers the package (v0.12.0), so
- // flying home afterwards leaves the starter clean for the other specs.
- await postSim(page, { timeScale: SCALE, paused: false });
+ // Cleanup: cancel the queued leg first so landing at DEST is a plain grounding (no auto-chain),
+ // resume + fast-forward to arrival, abandon the still-loaded package (it was destined for EHAM,
+ // not DEST — it never delivered), then fly home.
+ const cancelRes = await page.request.post('/api/planes/plane-starter/queue/cancel', { data: { index: 0 } });
+ expect(cancelRes.status()).toBe(200);
+ await setScale(page, SCALE); // also clears the pause above
await expect
.poll(async () => {
const p = (await getState(page)).planes.find((x) => x.id === 'plane-starter');
@@ -580,6 +586,9 @@ test('hovering an on-board package while its carrier is mid-flight traces from c
}, { timeout: 120_000, message: `waiting for plane-starter to land at ${DEST}` })
.toBe(true);
+ const abandon = await page.request.post(`/api/packages/${pkg.id}/abandon`);
+ expect(abandon.status()).toBe(200); // only possible once grounded — the poll above guarantees it
+
// ISS-041: the arrival above paused the sim, so re-set SCALE for this leg (setScale clears pause).
await setScale(page, SCALE);
const home = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } });
diff --git a/e2e/queue.spec.ts b/e2e/queue.spec.ts
index 64df588..c9e3edb 100644
--- a/e2e/queue.spec.ts
+++ b/e2e/queue.spec.ts
@@ -350,6 +350,64 @@ test('dotted queue line renders the queued path on the map (ISS-043 part 2)', as
}).toBe(HUB);
});
+test('grounded plane with a pre-built queue draws its dotted route from its airport (ISS-077)', async ({ page }) => {
+ test.setTimeout(300_000); // self-heal + map assertions only — no flight in this test
+ await flyHomeViaApi(page); // deterministic: the starter sits grounded at EDDF
+
+ await page.goto('/');
+ await waitForAirportCount(page);
+
+ // Pre-build a two-leg route while GROUNDED via the raw API (queuing is allowed from any state,
+ // ISS-043 part 1) — no package or flight needed. The dotted line must chain HUB → DEST → QUEUE.
+ const q1 = await page.request.post('/api/planes/plane-starter/queue', { data: { destIcao: DEST } });
+ expect(q1.status()).toBe(200);
+ const q2 = await page.request.post('/api/planes/plane-starter/queue', { data: { destIcao: QUEUE } });
+ expect(q2.status()).toBe(200);
+
+ // All chain endpoints must be in-viewport for queryRenderedFeatures to see them.
+ await fitToAirports(page, [HUB, DEST, QUEUE]);
+
+ const hubLL = await airportLonLat(page, HUB);
+ const destLL = await airportLonLat(page, DEST);
+ const queueLL = await airportLonLat(page, QUEUE);
+ const near = (a: [number, number], b: [number, number]) =>
+ Math.abs(a[0] - b[0]) <= QUEUE_ROUTE_EPS_DEG && Math.abs(a[1] - b[1]) <= QUEUE_ROUTE_EPS_DEG;
+
+ await expect.poll(async () => {
+ const feats = (await renderedFeatures(page, ['queue-routes'])) as Array<{
+ properties?: Record;
+ geometry?: { type: string; coordinates: [number, number][] };
+ }>;
+ // Filter to real LineString segments — keeps this poll callback total. "None yet" → keep polling.
+ const segs = feats.filter(
+ (f): f is { properties?: Record; geometry: { type: 'LineString'; coordinates: [number, number][] } } =>
+ !!f.geometry && f.geometry.type === 'LineString' && Array.isArray(f.geometry.coordinates) && f.geometry.coordinates.length >= 2,
+ );
+ if (!segs.length) return false;
+ // The chain HUB → DEST → QUEUE may be clipped into per-tile segments — check across all of them:
+ // some segment STARTS within EPS of the hub (the plane's airport, NOT a flight destination)…
+ const startsAtHub = segs.some((f) => near(f.geometry.coordinates[0], hubLL));
+ // …some ENDS within EPS of the last queued airport…
+ const endsAtQueue = segs.some((f) => {
+ const c = f.geometry.coordinates;
+ return near(c[c.length - 1], queueLL);
+ });
+ // …and the chain passes THROUGH the first queued airport — an interior joint point survives in
+ // whichever tile contains it (not guaranteed to be a segment endpoint after clipping).
+ const passesThroughDest = segs.some((f) => f.geometry.coordinates.some((c) => near(c, destLL)));
+ // Every rendered feature belongs to plane-starter.
+ const ownedByStarter = feats.every((f) => f.properties?.planeId === 'plane-starter');
+ return startsAtHub && endsAtQueue && passesThroughDest && ownedByStarter;
+ }, { timeout: 15_000, message: `waiting for the dotted queue line ${HUB} → ${DEST} → ${QUEUE}` }).toBe(true);
+
+ // Cleanup: cancel both queued legs (index 0 twice — each cancel shifts the rest down) so the
+ // starter stays grounded at HUB with an empty queue. No flight happened, nothing else to reset.
+ const c1 = await page.request.post('/api/planes/plane-starter/queue/cancel', { data: { index: 0 } });
+ expect(c1.status()).toBe(200);
+ const c2 = await page.request.post('/api/planes/plane-starter/queue/cancel', { data: { index: 0 } });
+ expect(c2.status()).toBe(200);
+});
+
test('landing auto-starts the next queued leg (ISS-043 chain end-to-end)', async ({ page }) => {
test.setTimeout(300_000); // self-heal + two chained legs at scale 1000
await flyHomeViaApi(page);
diff --git a/lib/mapData.test.ts b/lib/mapData.test.ts
index a419917..4527941 100644
--- a/lib/mapData.test.ts
+++ b/lib/mapData.test.ts
@@ -140,14 +140,40 @@ describe('routesGeoJson (v0.12.0 — one line per flying plane\'s current leg)',
});
});
-describe('queuedRoutesGeoJson (ISS-043 v0.26.0 — dotted queue line: dest → queued legs only)', () => {
- it('draws nothing for a grounded plane, even with a non-empty queue', () => {
+describe('queuedRoutesGeoJson (ISS-043 v0.26.0 — dotted queue line: dest → queued legs only; ISS-077 v0.41.0 — grounded queues too)', () => {
+ it('grounded plane with a non-empty queue → one feature chaining locationIcao → queued legs (ISS-077)', () => {
const fc = queuedRoutesGeoJson(
[
+ // Pre-built route while grounded: the line starts at the plane's current airport, not at any dest.
{ id: 'p1', model: 'atr72', status: 'available', fuel: 1, maxFuel: 2, locationIcao: 'EDDF', locationName: 'F', destIcao: null, destName: null, departureSimTime: null, lat: 50.03, lon: 8.56, health: 100, repairUntilSimTime: null, queue: ['EHAM'] },
],
AIRPORTS,
);
+ expect(fc.features).toHaveLength(1);
+ expect(fc.features[0].properties).toEqual({ planeId: 'p1' });
+ const coords = fc.features[0].geometry.coordinates;
+ expect(coords.length).toBe(ROUTE_ARC_SEGMENTS + 1); // one arc, exact endpoints
+ expect(coords[0]).toEqual([8.56, 50.03]); // starts at the plane's airport (EDDF)
+ expect(coords[coords.length - 1]).toEqual([4.76, 52.31]); // ends at the queued airport (EHAM)
+ });
+
+ it('skips a grounded plane whose locationIcao is missing from the airport index', () => {
+ const fc = queuedRoutesGeoJson(
+ [
+ { id: 'p1', model: 'atr72', status: 'available', fuel: 1, maxFuel: 2, locationIcao: 'ZZZZ', locationName: '?', destIcao: null, destName: null, departureSimTime: null, lat: 0, lon: 0, health: 100, repairUntilSimTime: null, queue: ['EHAM'] },
+ ],
+ AIRPORTS,
+ );
+ expect(fc.features).toHaveLength(0);
+ });
+
+ it('draws nothing for a repairing plane, even with a non-empty queue', () => {
+ const fc = queuedRoutesGeoJson(
+ [
+ { id: 'p1', model: 'atr72', status: 'repairing', fuel: 1, maxFuel: 2, locationIcao: 'EDDF', locationName: 'F', destIcao: null, destName: null, departureSimTime: null, lat: 50.03, lon: 8.56, health: 0, repairUntilSimTime: 1000, queue: ['EHAM'] },
+ ],
+ AIRPORTS,
+ );
expect(fc.features).toHaveLength(0);
});
@@ -190,7 +216,7 @@ describe('queuedRoutesGeoJson (ISS-043 v0.26.0 — dotted queue line: dest → q
expect(fc.features).toHaveLength(0);
});
- it('one feature per qualifying plane — grounded and empty-queue planes draw nothing', () => {
+ it('one feature per qualifying plane — flying and grounded queues draw; empty-queue planes do not', () => {
const fc = queuedRoutesGeoJson(
[
{ id: 'p1', model: 'atr72', status: 'flying', fuel: 1, maxFuel: 2, locationIcao: 'EDDF', locationName: 'F', destIcao: 'EHAM', destName: 'A', departureSimTime: 0, lat: 51, lon: 9, health: 100, repairUntilSimTime: null, queue: ['LEMD'] },
@@ -199,7 +225,10 @@ describe('queuedRoutesGeoJson (ISS-043 v0.26.0 — dotted queue line: dest → q
],
AIRPORTS,
);
- expect(fc.features.map((f) => f.properties.planeId)).toEqual(['p1']);
+ expect(fc.features.map((f) => f.properties.planeId)).toEqual(['p1', 'p3']); // p2 has an empty queue
+ const p3 = fc.features[1].geometry.coordinates;
+ expect(p3[0]).toEqual([9.68, 52.46]); // grounded chain starts at the plane's airport (EDDV)
+ expect(p3[p3.length - 1]).toEqual([-3.56, 40.47]); // …and ends at the queued airport (LEMD)
});
});
@@ -254,6 +283,12 @@ describe('hoverTraceGeoJson (ISS-040 v0.23.2 — on-board package trace from the
it('returns an empty collection when the dest airport is missing from the dataset', () => {
expect(hoverTraceGeoJson({ lat: 0, lon: 0 }, 'ZZZZ', AIRPORTS)).toEqual({ type: 'FeatureCollection', features: [] });
});
+
+ // ISS-077 (v0.41.0): a flying carrier's trace starts at its landing airport — when that IS the
+ // package's destination there is nothing to draw (the solid active-leg layer covers it).
+ it('returns an empty collection when the start point equals the dest airport', () => {
+ expect(hoverTraceGeoJson(AIRPORTS.get('EHAM')!, 'EHAM', AIRPORTS)).toEqual({ type: 'FeatureCollection', features: [] });
+ });
});
describe('greatCircleArc (v0.12.2 — curved route traces)', () => {
diff --git a/lib/mapData.ts b/lib/mapData.ts
index 4cab195..7f0e46b 100644
--- a/lib/mapData.ts
+++ b/lib/mapData.ts
@@ -116,7 +116,10 @@ export function hoverRouteGeoJson(
// ISS-040 (v0.23.2): the on-board package hover trace — same shape as hoverRouteGeoJson but the
// start is a live COORDINATE (the carrier plane's current position), not an airport ICAO: once the
// plane has departed with cargo still loaded, the preview must begin where it actually is. Empty
-// collection when the destination is missing from the dataset — never guessed.
+// collection when the destination is missing from the dataset — never guessed. ISS-077 (v0.41.0):
+// also empty when the start point IS the destination airport itself (a flying carrier's trace now
+// starts at its landing airport, which can equal the package's dest) — nothing to draw there; the
+// solid active-leg layer covers that case.
export function hoverTraceGeoJson(
from: AirportCoord,
toIcao: string,
@@ -124,6 +127,7 @@ export function hoverTraceGeoJson(
): FeatureCollection {
const to = airportsByIcao.get(toIcao);
if (!to) return EMPTY_LINES;
+ if (from.lat === to.lat && from.lon === to.lon) return EMPTY_LINES; // start == dest — zero-length trace
return {
type: 'FeatureCollection',
features: [
@@ -166,19 +170,25 @@ export function routesGeoJson(
return { type: 'FeatureCollection', features };
}
-// ISS-043 (v0.26.0): the dotted queue line — one LineString per FLYING plane with a non-empty
-// queue, chaining dest → queue[0] → … → queue[n−1]: ONLY the not-yet-started legs (the active
-// leg locationIcao→dest stays in routesGeoJson's solid layer). Arcs are concatenated with the
-// shared joint point dropped once. A plane is skipped — never guessed — when its dest or any
-// queued ICAO is missing from the airport index; empty collection when nothing qualifies.
+// ISS-043 (v0.26.0): the dotted queue line — one LineString per plane with a non-empty queue,
+// chaining the not-yet-started legs: a FLYING plane starts at its active leg's destination
+// (dest → queue[0] → …; the active leg locationIcao→dest stays in routesGeoJson's solid layer),
+// and ISS-077 (v0.41.0) extends it to GROUNDED ('available') planes, which start at their current
+// airport (locationIcao → queue[0] → …). 'repairing' planes draw nothing — they can't fly. Arcs are
+// concatenated with the shared joint point dropped once. A plane is skipped — never guessed — when
+// its first point or any queued ICAO is missing from the airport index; empty collection when
+// nothing qualifies.
export function queuedRoutesGeoJson(
planes: PlaneState[],
airportsByIcao: ReadonlyMap,
): FeatureCollection {
const features: MapLineFeature[] = [];
for (const p of planes) {
- if (p.status !== 'flying' || !p.destIcao || p.queue.length === 0) continue;
- const pts = [p.destIcao, ...p.queue].map((icao) => airportsByIcao.get(icao));
+ if (p.queue.length === 0) continue;
+ // First point of the chain: a flying plane's landing airport, a grounded one's current airport.
+ const firstIcao = p.status === 'flying' ? p.destIcao : p.status === 'available' ? p.locationIcao : null;
+ if (!firstIcao) continue;
+ const pts = [firstIcao, ...p.queue].map((icao) => airportsByIcao.get(icao));
if (pts.some((c) => c === undefined)) continue; // an endpoint is missing from the dataset — skip
const coords: [number, number][] = [];
for (let i = 0; i < pts.length - 1; i++) {
diff --git a/package.json b/package.json
index 2b73086..83c86bd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "flight-ops",
- "version": "0.40.0",
+ "version": "0.41.0",
"private": true,
"scripts": {
"dev": "next dev -p 3000",