Merge pull request 'Top-bar version links to CHANGELOG; on-board rows show ICAO tags (ISS-080, ISS-081, v0.40.0)' (#83) from feat/topbar-changelog-link-icao-tags into master

This commit is contained in:
2026-09-21 15:20:08 +02:00
7 changed files with 33 additions and 6 deletions
+4
View File
@@ -1,5 +1,9 @@
# Changelog # Changelog
## v0.40.0 — 2026-09-21 13:06
- **Top-bar version link (ISS-080).** The header's version tag (`components/Map.tsx`) is now an anchor to the repo's `CHANGELOG.md` on Gitea (`app-version` testid, opens in a new tab); one small CSS rule keeps it subtle against the dark header. E2E: `e2e/map.spec.ts` pins the exact href
- **On-board ICAO tags (ISS-081).** The on-board package row in the fleet list (`components/LeftPane.tsx`) shows 4-char ICAO tags instead of full airport names — both the route span (new `onboard-route-{id}` testid) and the "Fly to …" button label + title. Other name displays (in-flight leg, plane location, map popup, open-package rows, toasts) are unchanged. E2E: one new assertion in `e2e/actions.spec.ts` pins the ICAO route on a loaded package
## v0.39.0 — 2026-09-21 12:02 ## v0.39.0 — 2026-09-21 12:02
- **Maintenance (ISS-069).** Planes wear out and can be repaired: distance-based health, a broken state that burns more fuel, and a repair action that grounds the plane for 3 sim days. New balance constants in `lib/game/catalog.ts` (all ponytail-tunable): `WEAR_PER_KM = 0.005`, `BROKEN_BURN_MULTIPLIER = 1.5`, `REPAIR_COST = 1_500` (flat), `REPAIR_DURATION_MS = 3 × 86_400_000`, plus the pure helper `conditionBurnMultiplier(health)` — exactly 1 above zero health, ×1.5 at/below it - **Maintenance (ISS-069).** Planes wear out and can be repaired: distance-based health, a broken state that burns more fuel, and a repair action that grounds the plane for 3 sim days. New balance constants in `lib/game/catalog.ts` (all ponytail-tunable): `WEAR_PER_KM = 0.005`, `BROKEN_BURN_MULTIPLIER = 1.5`, `REPAIR_COST = 1_500` (flat), `REPAIR_DURATION_MS = 3 × 86_400_000`, plus the pure helper `conditionBurnMultiplier(health)` — exactly 1 above zero health, ×1.5 at/below it
- **Schema.** Two new `planes` columns in all four places: Drizzle schema (`lib/db/schema.ts`), raw DDL + guarded `ALTER TABLE … ADD COLUMN` migrations in `migrateColumns()` (pre-existing rows backfill to full health / NULL gate, mirroring the weight_kg/special precedent), and the seed insert path — starter plane in `buildSeed` (`lib/game/seed.ts`) and every `buyPlane` delivery start at `health: 100`. New plane status `'repairing'` added to the three pinned unions (schema enum, `PlaneState`, OpenAPI) plus `mapData.ts`'s feature props - **Schema.** Two new `planes` columns in all four places: Drizzle schema (`lib/db/schema.ts`), raw DDL + guarded `ALTER TABLE … ADD COLUMN` migrations in `migrateColumns()` (pre-existing rows backfill to full health / NULL gate, mirroring the weight_kg/special precedent), and the seed insert path — starter plane in `buildSeed` (`lib/game/seed.ts`) and every `buyPlane` delivery start at `health: 100`. New plane status `'repairing'` added to the three pinned unions (schema enum, `PlaneState`, OpenAPI) plus `mapData.ts`'s feature props
+10
View File
@@ -48,6 +48,16 @@ body {
color: #94a3b8; color: #94a3b8;
} }
/* ISS-080 (v0.40.0): the version tag links to the repo's CHANGELOG.md — keep it subtle, like the rest of the header. */
.app-header a {
color: #7dd3fc;
text-decoration: none;
}
.app-header a:hover {
text-decoration: underline;
}
/* Style switcher — kept subtle, like the version tag. */ /* Style switcher — kept subtle, like the version tag. */
.style-switch { .style-switch {
font-size: 12px; font-size: 12px;
+7 -3
View File
@@ -429,8 +429,9 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
// so "Fly to …" works from any state and the queue needs no dedicated input. // so "Fly to …" works from any state and the queue needs no dedicated input.
onClick={() => void runAction(`/api/planes/${p.id}/${p.status === 'flying' ? 'queue' : 'fly'}`, { destIcao: pkg.destIcao })} onClick={() => void runAction(`/api/planes/${p.id}/${p.status === 'flying' ? 'queue' : 'fly'}`, { destIcao: pkg.destIcao })}
> >
<span className="onboard-fly-label" title={`Fly to ${pkg.destName}`}> {/* ISS-081 (v0.40.0): ICAO tags instead of full airport names. */}
Fly to {pkg.destName} <span className="onboard-fly-label" title={`Fly to ${pkg.destIcao}`}>
Fly to {pkg.destIcao}
</span> </span>
</button> </button>
<div className="onboard-actions"> <div className="onboard-actions">
@@ -455,7 +456,10 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
</button> </button>
</div> </div>
<div className="contract-sub"> <div className="contract-sub">
<span className="contract-route">{pkg.originName} → {pkg.destName}</span> {/* ISS-081 (v0.40.0): ICAO tags instead of full airport names. */}
<span data-testid={`onboard-route-${pkg.id}`} className="contract-route">
{pkg.originIcao} → {pkg.destIcao}
</span>
{/* v0.37.0 (ISS-073): special-status badge — rendered only when the package has one. */} {/* v0.37.0 (ISS-073): special-status badge — rendered only when the package has one. */}
{pkg.special && ( {pkg.special && (
<span data-testid={`package-special-${pkg.id}`} className="special-badge"> <span data-testid={`package-special-${pkg.id}`} className="special-badge">
+3 -1
View File
@@ -537,7 +537,9 @@ export default function FlightMap({ state, hovered, applySnapshot }: MapProps) {
<div className="map-wrap"> <div className="map-wrap">
<header data-testid="app-header" className="app-header"> <header data-testid="app-header" className="app-header">
<h1>Flight Ops</h1> <h1>Flight Ops</h1>
<small>v{pkg.version}</small> <a data-testid="app-version" href="https://git.kkhost.pl/OpenCode/Flight-Ops/src/branch/master/CHANGELOG.md" target="_blank" rel="noreferrer">
<small>v{pkg.version}</small>
</a>
<select <select
data-testid="style-switch" data-testid="style-switch"
className="style-switch" className="style-switch"
+3 -1
View File
@@ -192,8 +192,10 @@ test('full core loop via UI: load → fly to destination → exact reward + rep
// package shows as loaded on the starter in the on-board section. // package shows as loaded on the starter in the on-board section.
const onboardRow = page.getByTestId(`onboard-package-${pkg.id}`); const onboardRow = page.getByTestId(`onboard-package-${pkg.id}`);
await expect(onboardRow, 'loaded package appears in the on-board list').toBeVisible(); await expect(onboardRow, 'loaded package appears in the on-board list').toBeVisible();
// ISS-081 (v0.40.0): the on-board route shows ICAO tags, not full airport names.
await expect(page.getByTestId(`onboard-route-${pkg.id}`)).toContainText(`${HUB} → ${nearest.icao}`);
// FLY via the UI: click the destination name button in the on-board list. // FLY via the UI: click the fly-to button in the on-board list (ISS-081: ICAO label).
const flyBtn = page.getByTestId(`fly-to-${pkg.id}`); const flyBtn = page.getByTestId(`fly-to-${pkg.id}`);
await expect(flyBtn).toBeEnabled(); await expect(flyBtn).toBeEnabled();
await flyBtn.click(); await flyBtn.click();
+5
View File
@@ -33,6 +33,11 @@ test('header shows the app version sourced from package.json', async ({ page })
await page.goto(APP_URL); await page.goto(APP_URL);
const header = page.getByTestId('app-header'); const header = page.getByTestId('app-header');
await expect(header).toContainText(`v${pkg.version}`); await expect(header).toContainText(`v${pkg.version}`);
// ISS-080 (v0.40.0): the version links to the repo's CHANGELOG.md.
await expect(page.getByTestId('app-version')).toHaveAttribute(
'href',
'https://git.kkhost.pl/OpenCode/Flight-Ops/src/branch/master/CHANGELOG.md',
);
}); });
test('clicking an airport marker opens a popup with name and ICAO', async ({ page }) => { test('clicking an airport marker opens a popup with name and ICAO', async ({ page }) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "flight-ops", "name": "flight-ops",
"version": "0.39.0", "version": "0.40.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3000", "dev": "next dev -p 3000",