diff --git a/CHANGELOG.md b/CHANGELOG.md
index 71a0a51..95b03ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# Changelog
+## v0.3.1 — 2026-09-17 06:35
+- Header now shows the app version between the "Flight Ops" title and the airport count (`v{version}` in `components/Map.tsx`), sourced dynamically from `package.json` via a JSON import — future version bumps show up automatically with no code change; subtle 11px muted style added to `app/globals.css`
+- Tests: new unit suite `components/version.test.ts` (valid semver in package.json + guard that the header sources the version from package.json rather than hardcoding it); `e2e/map.spec.ts`: new test asserting the header shows `v{version}` with the expected value read from `package.json` at test time, so it stays in sync automatically
+- Verified: unit suite 10/10; `tsc --noEmit` clean; e2e suite 4/4 green against the rebuilt compose instance
+
## v0.3.0 — 2026-09-17 00:32
- Removed the zoom-driven camera tilt (`lib/camera.ts` pitch curve + the map `move` handler): the map is now always flat/top-down with standard pan/zoom only; deleted `lib/camera.ts` and its unit tests
- Removed terrain: no more DEM raster source / `map.setTerrain`, and the `?terrain=0` / `NEXT_PUBLIC_TERRAIN` toggles are gone — pure 2D view, no 3D elements
diff --git a/app/globals.css b/app/globals.css
index d99b938..e796eb4 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -43,6 +43,11 @@ body {
font-weight: 600;
}
+.app-header small {
+ font-size: 11px;
+ color: #94a3b8;
+}
+
[data-testid='airport-count'] {
font-size: 12px;
color: #cbd5e1;
diff --git a/components/Map.tsx b/components/Map.tsx
index 5f403e6..4a45c0c 100644
--- a/components/Map.tsx
+++ b/components/Map.tsx
@@ -3,6 +3,7 @@
import { useEffect, useRef, useState } from 'react';
import * as maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
+import pkg from '../package.json';
const EUROPE_CENTER: [number, number] = [10, 52];
const INITIAL_ZOOM = 4;
@@ -93,6 +94,7 @@ export default function Map() {
Flight Ops
+ v{pkg.version}
{airportCount === null ? 'loading airports…' : `${airportCount} EU airports`}
diff --git a/components/version.test.ts b/components/version.test.ts
new file mode 100644
index 0000000..f581c35
--- /dev/null
+++ b/components/version.test.ts
@@ -0,0 +1,16 @@
+import { readFileSync } from 'node:fs';
+import pkg from '../package.json';
+import { describe, expect, it } from 'vitest';
+
+const mapSource = readFileSync(new URL('./Map.tsx', import.meta.url), 'utf8');
+
+describe('app version display', () => {
+ it('package.json carries a valid semver version (single source of truth)', () => {
+ expect(pkg.version).toMatch(/^\d+\.\d+\.\d+$/);
+ });
+
+ it('the header sources the version from package.json instead of hardcoding it', () => {
+ expect(mapSource).toContain("from '../package.json'");
+ expect(mapSource).toContain('v{pkg.version}');
+ });
+});
diff --git a/e2e/map.spec.ts b/e2e/map.spec.ts
index c635c11..b61e335 100644
--- a/e2e/map.spec.ts
+++ b/e2e/map.spec.ts
@@ -1,7 +1,12 @@
+import { readFileSync } from 'node:fs';
+import { join } from 'node:path';
import { test, expect, type Page } from '@playwright/test';
const APP_URL = '/';
+// Read at test time so the assertion tracks package.json automatically.
+const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')) as { version: string };
+
// Must match MIN_ZOOM / MAX_ZOOM exported from components/Map.tsx.
const MIN_ZOOM = 3;
const MAX_ZOOM = 11;
@@ -24,6 +29,12 @@ test('page loads with the map and EU airport count', async ({ page }) => {
expect(count).toBeGreaterThan(0);
});
+test('header shows the app version sourced from package.json', async ({ page }) => {
+ await page.goto(APP_URL);
+ const header = page.getByTestId('app-header');
+ await expect(header).toContainText(`v${pkg.version}`);
+});
+
test('clicking an airport marker opens a popup with name and ICAO', async ({ page }) => {
await page.goto(APP_URL);
await waitForAirportCount(page);
diff --git a/package.json b/package.json
index 6d92182..bd9650d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "flight-ops",
- "version": "0.3.0",
+ "version": "0.3.1",
"private": true,
"scripts": {
"dev": "next dev -p 3000",