diff --git a/CHANGELOG.md b/CHANGELOG.md
index 445f94f..651253c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# Changelog
+## v0.33.0 — 2026-09-21 00:45
+- **Merged On board into the Fleet list (ISS-054).** The left pane's separate "On board" section is deleted; each Fleet row (`components/LeftPane.tsx`) is now the single per-plane block — status/fuel/loadout plus, when present, that plane's loaded/expired cargo rows (fly-to / refuel-for-flight / abandon + penalty preview) and its queue legs. The header shows "in flight" + the current leg for flying planes and the location ICAO when grounded; the empty "Queue" heading is no longer rendered (the sub-section renders only with ≥1 queued leg). All per-item testids unchanged (`onboard-package-*`, `fly-to-*`, `refuel-for-*`, `abandon-*`, `abandon-penalty-*`, `plane-leg-*`, `queue-leg-*`, `queue-cancel-*`); the removed one is `onboard-{plane.id}` (use `plane-{id}`). CSS: `.onboard-block .contract-row*` re-targeted to `.plane-row .contract-row*`; no server or OpenAPI changes
+- **E2E updates.** The four `onboard-{plane.id}` references now target the fleet rows (`e2e/hover.spec.ts`, `e2e/right-pane.spec.ts`, `e2e/two-planes.spec.ts`); `e2e/panes.spec.ts` no longer asserts an "On board" heading — it asserts the heading is absent from both panes
+
## v0.32.0 — 2026-09-20 23:47
- **Queue via fly-to and Relocate; dedicated queue-add input removed (ISS-053).** The flight queue no longer has its own entry point in the On board block (`components/LeftPane.tsx`): the per-cargo "Fly to …" button is now enabled for ANY plane state, and posts to `/api/planes/{id}/queue` instead of `/fly` when the plane is `flying` (grounded → `/fly`, unchanged); Relocate's plane list widened from grounded-only to ALL planes (`relocatable = state.planes`; Refuel keeps its own `grounded` filter), and `relocate-go` posts to `/queue` when the SELECTED plane is flying, else `/fly`. The queue-add select + "Add to queue" button (and the shared `queueAddDestIcao` state) are deleted; leg rows + cancel buttons unchanged. No server or OpenAPI changes — both endpoints already existed
- **E2E updates.** `e2e/queue.spec.ts` test 1 now adds its queued leg via the Relocate section (selects plane-starter, which is flying at that point → `/queue`) instead of the removed queue-add control; `e2e/two-planes.spec.ts` "per-plane queues are independent" adds the starter's second leg via API POST `/api/planes/plane-starter/queue` (the starter is grounded there, so fly-to would start rather than queue). All `queue-leg-*` / `queue-cancel-*` assertions unchanged
diff --git a/app/globals.css b/app/globals.css
index aa8844a..55636d5 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -376,17 +376,10 @@ body {
font-variant-numeric: tabular-nums;
}
-/* v0.12.0 — on-board blocks (per plane); moved to the left pane in ISS-021 (v0.20.0) */
-.onboard-block {
- background: rgba(30, 41, 59, 0.6);
- border-radius: 6px;
- padding: 8px;
- margin-bottom: 8px;
-}
-
-/* ISS-028 (v0.20.0 part 2) — on-board cargo rows are two lines: a full-width fly-to button,
- then refuel + abandon side by side below it; long destination names truncate with an ellipsis. */
-.onboard-block .contract-row {
+/* ISS-054 (v0.33.0) — per-plane cargo rows nested inside the Fleet list's plane cards: two lines,
+ a full-width fly-to button then refuel + abandon side by side below it; long destination names
+ truncate with an ellipsis. Transparent sub-rows with a top border under the card header. */
+.plane-row .contract-row {
background: transparent;
padding: 6px 0 0;
border-top: 1px solid rgba(148, 163, 184, 0.2);
@@ -395,7 +388,7 @@ body {
gap: 5px;
}
-.onboard-block .contract-row .contract-sub {
+.plane-row .contract-row .contract-sub {
margin-top: 0; /* row spacing comes from the container's gap */
}
diff --git a/components/LeftPane.tsx b/components/LeftPane.tsx
index 5a95fe0..89b4bf1 100644
--- a/components/LeftPane.tsx
+++ b/components/LeftPane.tsx
@@ -19,7 +19,7 @@ interface LeftPaneProps {
// The shell's single-poll refresh — called after a sim-control POST so the pane updates immediately.
onRefresh: () => void;
// ISS-021 (v0.20.0): push the full snapshot returned by an action endpoint into the shared shell
- // state — needed now that the On board section's refuel/fly/abandon actions live in this pane.
+ // state — needed now that the per-plane cargo rows' refuel/fly/abandon actions live in this pane.
applySnapshot: (snapshot: StateSnapshot) => void;
// ISS-002 (v0.15.0): report fleet-row hovers to the shell so the map can highlight the plane.
onHover: (target: HoverTarget | null) => void;
@@ -92,7 +92,7 @@ export async function retrySimPost(
export default function LeftPane({ state, loading, error, onRefresh, applySnapshot, onHover, onError }: LeftPaneProps) {
const [busy, setBusy] = useState(false);
- // ISS-021 (v0.20.0): shared action runner for the On board section (moved here from RightPane).
+ // ISS-021 (v0.20.0): shared action runner for the per-plane cargo/queue actions (moved here from RightPane).
const { runAction, busy: actionsBusy, actionError } = useGameAction(state, applySnapshot, onError);
// ISS-023 (v0.21.0): Relocate selections — moved here from the right-pane market row; same
@@ -195,15 +195,8 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
const paused = state?.simState.paused ?? false;
const timeScale = state?.simState.timeScale ?? 1;
- // On board (v0.12.0, moved from the right pane in ISS-021 v0.20.0): every plane that carries
- // cargo or is mid-flight gets a block — its loaded/expired packages with fly-to-destination +
- // abandon controls, and flying planes show their current leg.
+ // Snapshot clock — feeds the deadline/penalty previews in the per-plane cargo rows.
const clockMs = state?.simState.clockMs ?? 0;
- // ISS-043 (v0.26.0): a plane with queued legs always shows its block too — the queue is the
- // reason it's here even when grounded with nothing on board.
- const onboard = state
- ? state.planes.filter((p) => p.status === 'flying' || cargoWeightFor(state, p.id) > 0 || (p.queue?.length ?? 0) > 0)
- : [];
// ISS-023 (v0.21.0): Relocate — any plane can be repositioned to any airport via the existing fly
// endpoint (pure repositioning, no package needed since v0.12.0). Moved here from the right-pane
@@ -340,161 +333,143 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
No planes.
) : (
- {state.planes.map((p) => (
- // ISS-002: hovering a fleet row highlights the plane on the map.
-
- ))}
+ {state.planes.map((p) => {
+ // ISS-054 (v0.33.0): the merged per-plane block — status/fuel/loadout plus the former
+ // On board section's cargo rows and queue legs, so all plane info lives in one place.
+ const cargo = state.packages.filter(
+ (pkg) => pkg.planeId === p.id && (pkg.status === 'loaded' || pkg.status === 'expired'),
+ );
+ return (
+ // ISS-002: hovering a fleet row highlights the plane on the map. Handlers live only on
+ // the outer
— React's enter/leave don't fire when moving to a child, so this covers
+ // the whole block without flicker between sub-elements; cargo rows override with their
+ // own package hover (below).
+
+ {cargo.map((pkg) => {
+ const penalty = abandonPenalty(pkg, clockMs);
+ return (
+ // ISS-025 (v0.22.0): hovering a loaded package draws its full origin→dest trace on the map —
+ // same as job-list rows — and keeps this plane's halo lit via planeId. Leaving the row restores
+ // the block's plain plane highlight; leaving the whole block still ends at null (the child's
+ // mouseleave fires before the parent's).
+
+ );
+ })}
+ {/* ISS-043 (v0.26.0): flight queue — the not-yet-started legs with cancel.
+ ISS-053 (v0.32.0): the dedicated add input is gone — fly-to and Relocate post to
+ /queue while a plane is flying, so no separate entry point is needed.
+ ISS-054 (v0.33.0): rendered only when non-empty. */}
+ {p.queue.length > 0 && (
+
+ Queue
+ {/* ISS-055 (v0.31.0): testids scoped by plane id — with two blocks the unscoped ids
+ duplicated in the DOM and broke Playwright strict mode. */}
+ {p.queue.map((icao, i) => (
+
)}
- {/* ISS-021 (v0.20.0): moved here from the right pane — between Fleet and Time. */}
-
-
On board
- {onboard.length === 0 ? (
-
Nothing on board.
- ) : (
- onboard.map((plane) => {
- const cargo = state.packages.filter(
- (pkg) => pkg.planeId === plane.id && (pkg.status === 'loaded' || pkg.status === 'expired'),
- );
- return (
- // ISS-002: hovering the on-board block (including its cargo rows) highlights the plane.
- // Handlers live only on the outer div — React's enter/leave don't fire when moving to a
- // child, so this covers the whole block without flicker between sub-elements.
-
- {cargo.map((pkg) => {
- const penalty = abandonPenalty(pkg, clockMs);
- return (
- // ISS-025 (v0.22.0): hovering a loaded package draws its full origin→dest trace on the map —
- // same as job-list rows — and keeps this plane's halo lit via planeId. Leaving the row restores
- // the block's plain plane highlight; leaving the whole block still ends at null (the child's
- // mouseleave fires before the parent's).
-
- );
- })}
- {/* ISS-043 (v0.26.0): flight queue — the not-yet-started legs with cancel.
- ISS-053 (v0.32.0): the dedicated add input is gone — fly-to and Relocate post to
- /queue while a plane is flying, so no separate entry point is needed. */}
-
- Queue
- {/* ISS-055 (v0.31.0): testids scoped by plane id — with two on-board blocks the
- unscoped ids duplicated in the DOM and broke Playwright strict mode. */}
- {plane.queue.map((icao, i) => (
-
- );
- })
- )}
-
-
{/* ISS-023 (v0.21.0): moved here from the right-pane market row — it belongs with
- everything else about our planes, between On board and Time. */}
+ everything else about our planes, between Fleet and Time. */}
{/* ISS-027 (v0.22.0): hovering the section previews the planned relocation on the map —
dashed trace from the selected plane's airport to the destination + a ring on the target.
With no planes or no loaded airport options there is nothing to preview → null. */}
@@ -562,7 +537,7 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
Refuel
{/* Moved verbatim from the right-pane Market section — same testids/labels; the per-package
- "Refuel" button in On board (mode 'forFlight') is a different feature and stays there. */}
+ "Refuel" button in the fleet rows (mode 'forFlight') is a different feature and stays there. */}
{refuelable.length === 0 ? (
diff --git a/e2e/hover.spec.ts b/e2e/hover.spec.ts
index ccd9a3c..f8a1bf9 100644
--- a/e2e/hover.spec.ts
+++ b/e2e/hover.spec.ts
@@ -439,7 +439,7 @@ test('hovering the Relocate section previews the planned route and target airpor
}).toBe(0);
});
-test('hovering an on-board block of a flying plane highlights its route', async ({ page }) => {
+test('hovering the fleet row of a flying plane highlights its route', async ({ page }) => {
test.setTimeout(600_000); // self-heal + one flight at scale 1000 + fly-home cleanup
await flyHomeViaApi(page);
@@ -458,8 +458,8 @@ test('hovering an on-board block of a flying plane highlights its route', async
expect(fly.status()).toBe(200);
await setScale(page, SCALE);
- // The on-board block shows the plane in flight once a snapshot with status 'flying' lands.
- const block = page.getByTestId('onboard-plane-starter');
+ // The fleet row shows the plane in flight once a snapshot with status 'flying' lands (ISS-054).
+ const block = page.getByTestId('plane-plane-starter');
await expect(block).toContainText(/in flight/i, { timeout: 15_000 });
// Freeze mid-flight so the ~10 s leg can't land under us while we assert (same pattern as
diff --git a/e2e/panes.spec.ts b/e2e/panes.spec.ts
index e765af1..2665e29 100644
--- a/e2e/panes.spec.ts
+++ b/e2e/panes.spec.ts
@@ -70,10 +70,9 @@ test('left pane renders wallet, reputation and the starter fleet at Frankfurt',
// Sim clock renders (UTC wall-clock of the game time).
await expect(page.getByTestId('sim-clock')).toContainText(/UTC/);
- // ISS-021 (v0.20.0): the On board section moved to the left pane (between Fleet and Time) —
- // its heading renders here…
- await expect(page.getByTestId('left-pane').getByRole('heading', { name: 'On board', exact: true })).toBeVisible();
- // …and no longer in the right pane.
+ // ISS-054 (v0.33.0): On board merged into the Fleet list — its heading is gone from both panes,
+ // per-plane cargo rows now render inside the fleet rows.
+ await expect(page.getByTestId('left-pane').getByRole('heading', { name: 'On board', exact: true })).toHaveCount(0);
await expect(page.getByTestId('right-pane').getByRole('heading', { name: 'On board', exact: true })).toHaveCount(0);
// ISS-035 (v0.23.0): the Refuel-to-full row moved to the left pane next to Relocate — its section
diff --git a/e2e/right-pane.spec.ts b/e2e/right-pane.spec.ts
index dc1463f..edd03fb 100644
--- a/e2e/right-pane.spec.ts
+++ b/e2e/right-pane.spec.ts
@@ -268,8 +268,8 @@ test('on-board list: fly-to-destination + abandon with penalty preview; flying p
await expect(flyBtn).toBeEnabled();
await flyBtn.click();
- // The on-board block now shows the plane in flight with its current leg…
- await expect(page.getByTestId('onboard-plane-starter')).toContainText(/in flight/i, { timeout: 10_000 });
+ // The fleet row now shows the plane in flight with its current leg (ISS-054)…
+ await expect(page.getByTestId('plane-plane-starter')).toContainText(/in flight/i, { timeout: 10_000 });
const leg = page.getByTestId('plane-leg-plane-starter');
await expect(leg).toBeVisible();
await expect(leg).toContainText('→');
diff --git a/e2e/two-planes.spec.ts b/e2e/two-planes.spec.ts
index e949cd8..1a75582 100644
--- a/e2e/two-planes.spec.ts
+++ b/e2e/two-planes.spec.ts
@@ -323,9 +323,9 @@ test('two grounded planes: Load is one-click with a preselected plane; the picke
}, { timeout: 15_000, message: 'waiting for the second package to load onto a plane' })
.toBe(secondId);
- // Both planes carry cargo simultaneously — both on-board blocks render.
- await expect(page.getByTestId(`onboard-${STARTER}`), 'the starter block renders').toBeVisible({ timeout: 15_000 });
- await expect(page.getByTestId(`onboard-${secondId}`), 'the second plane block renders').toBeVisible({ timeout: 15_000 });
+ // Both planes carry cargo simultaneously — both fleet rows render their cargo (ISS-054).
+ await expect(page.getByTestId(`plane-${STARTER}`), 'the starter row renders').toBeVisible({ timeout: 15_000 });
+ await expect(page.getByTestId(`plane-${secondId}`), 'the second plane row renders').toBeVisible({ timeout: 15_000 });
});
test('two planes fly in parallel; the last landing slows to 1× and the next dispatch resumes', async ({ page }) => {
diff --git a/package.json b/package.json
index e0b152d..d92d78e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "flight-ops",
- "version": "0.32.0",
+ "version": "0.33.0",
"private": true,
"scripts": {
"dev": "next dev -p 3000",