Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ac133f750 | ||
|
|
d3038b3c7f |
@@ -1,5 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## v0.35.0 — 2026-09-21 02:26
|
||||
- **Job list shows cargo tonnage instead of count (ISS-066).** The open-package rows in the right pane's Jobs tab (`components/RightPane.tsx`) now show cargo as type + tonnage ("Mail 10.0t") via a new `formatCargo` in `lib/format.ts` — `(kg / 1000).toFixed(1) + 't'`, identical to the loadout display — instead of "type × count"; unit tests added to `lib/format.test.ts`; e2e assertion added to `e2e/right-pane.spec.ts` (the hook-created package is forced to 10 t and its row must contain "10.0t"). No server or OpenAPI changes — `weightKg` was already in the snapshot
|
||||
|
||||
## v0.34.0 — 2026-09-21 01:26
|
||||
- **Duplicate queue legs rejected with 409 (ISS-065).** `queueLeg` (`lib/game/actions.ts`) now refuses to append a destination that is already in the plane's flight queue ("{icao} is already in the flight queue"), and — while the plane is flying — re-queuing the active leg's destination ("already flying to {icao}"). Previously the same route could be queued any number of times, producing pointless repeated/zero-length hops when landing auto-chains `queue[0]`. No UI or OpenAPI changes (the endpoint already documents error responses)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
||||
import type { PackageState, PlaneState, StateSnapshot } from '@/lib/game/state';
|
||||
import { PLANE_TYPES, SELL_FRACTION } from '@/lib/game/catalog';
|
||||
import { maxRangeKm } from '@/lib/sim/movement';
|
||||
import { formatCash, formatDeadlineRelative } from '@/lib/format';
|
||||
import { formatCargo, formatCash, formatDeadlineRelative } from '@/lib/format';
|
||||
import { useGameAction } from '@/lib/useGameAction';
|
||||
import type { HoverTarget } from './hoverTypes';
|
||||
|
||||
@@ -225,7 +225,8 @@ export default function RightPane({ state, loading, applySnapshot, onHover, onEr
|
||||
</div>
|
||||
<div className="contract-sub">
|
||||
<span data-testid={`open-package-reward-${pkg.id}`}>{formatCash(pkg.reward)}</span>
|
||||
<span>{pkg.cargoType} × {pkg.cargoCount}</span>
|
||||
{/* ISS-066 (v0.35.0): cargo shows as type + tonnage ("Mail 10.0t"), not "type × count". */}
|
||||
<span data-testid={`open-package-cargo-${pkg.id}`}>{formatCargo(pkg.cargoType, pkg.weightKg)}</span>
|
||||
<span data-testid={`open-package-deadline-${pkg.id}`}>
|
||||
{formatDeadlineRelative(pkg.deadlineSimTime, clockMs)}
|
||||
</span>
|
||||
|
||||
@@ -158,9 +158,9 @@ function maxRangeKm(burnPerKm: number, emptyWeightKg: number, maxFuel: number):
|
||||
}
|
||||
|
||||
// Test hook (FLIGHT_TEST_HOOKS=1): create an open package on a known route.
|
||||
async function createPackage(page: Page, fromIcao: string, toIcao: string, deadlineOffsetMs?: number): Promise<PackageState> {
|
||||
async function createPackage(page: Page, fromIcao: string, toIcao: string, deadlineOffsetMs?: number, weightKg?: number): Promise<PackageState> {
|
||||
const created = await page.request.post('/api/test/create-package', {
|
||||
data: { originIcao: fromIcao, destIcao: toIcao, deadlineOffsetMs },
|
||||
data: { originIcao: fromIcao, destIcao: toIcao, deadlineOffsetMs, weightKg },
|
||||
});
|
||||
expect(created.status()).toBe(201);
|
||||
return (await created.json()) as PackageState;
|
||||
@@ -213,9 +213,12 @@ test('open packages render with names, reward and deadline; Load is enabled at a
|
||||
|
||||
// Deterministic target: a hook-created EDDF package — Load is enabled (starter at origin, free slot).
|
||||
const nearest = await nearestToHub(page);
|
||||
const pkg = await createPackage(page, HUB, nearest.icao);
|
||||
const pkg = await createPackage(page, HUB, nearest.icao, undefined, 4_000);
|
||||
const row = page.locator(`li[data-testid="open-package-${pkg.id}"]`);
|
||||
await expect(row, 'the hook-created package appears in the list').toBeVisible({ timeout: 15_000 });
|
||||
// ISS-066 (v0.35.0): cargo shows as type + tonnage ("… 4.0t"), not "type × count".
|
||||
// 4 t is within the starter ATR's 4.8 t maxPayloadKg, so Load stays enabled below.
|
||||
await expect(page.getByTestId(`open-package-cargo-${pkg.id}`)).toContainText('4.0t');
|
||||
await expect(page.getByTestId(`open-package-reward-${pkg.id}`)).toContainText(eur(pkg.reward));
|
||||
const loadBtn = page.getByTestId(`load-${pkg.id}`);
|
||||
await expect(loadBtn).toBeEnabled();
|
||||
|
||||
+10
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { formatCash, formatDeadlineRelative, formatDuration, formatFuel, formatSimClock } from './format';
|
||||
import { formatCargo, formatCash, formatDeadlineRelative, formatDuration, formatFuel, formatSimClock } from './format';
|
||||
|
||||
describe('formatCash', () => {
|
||||
it('formats EUR with thousands separators and two decimals', () => {
|
||||
@@ -21,6 +21,15 @@ describe('formatFuel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatCargo', () => {
|
||||
it('shows cargo type + tonnage with one decimal, like the loadout display', () => {
|
||||
expect(formatCargo('Mail', 10_000)).toBe('Mail 10.0t');
|
||||
// (250 / 1000).toFixed(1) === '0.3' — half rounds up at this value.
|
||||
expect(formatCargo('Electronics', 250)).toBe('Electronics 0.3t');
|
||||
expect(formatCargo('Mail', 40)).toBe('Mail 0.0t');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatSimClock', () => {
|
||||
it('formats epoch ms as a UTC wall-clock string with seconds (deterministic across timezones)', () => {
|
||||
expect(formatSimClock(Date.UTC(2026, 8, 17, 14, 5))).toBe('2026-09-17 14:05:00 UTC');
|
||||
|
||||
@@ -11,6 +11,11 @@ export function formatFuel(fuel: number, maxFuel: number): string {
|
||||
return `${Math.round(fuel)} / ${maxFuel} L`;
|
||||
}
|
||||
|
||||
// Job-list cargo label: "Mail 10.0t" — type + tonnage, one decimal like the loadout display.
|
||||
export function formatCargo(cargoType: string, weightKg: number): string {
|
||||
return `${cargoType} ${(weightKg / 1000).toFixed(1)}t`;
|
||||
}
|
||||
|
||||
// The sim clock is epoch ms (seeded from real time) — shown as a UTC wall-clock with
|
||||
// seconds so the display is deterministic regardless of the viewer's timezone.
|
||||
export function formatSimClock(clockMs: number): string {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "flight-ops",
|
||||
"version": "0.34.0",
|
||||
"version": "0.35.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000",
|
||||
|
||||
Reference in New Issue
Block a user