# Changelog

All notable changes to devinmitchell.com are documented here.
Versions follow [Semantic Versioning](https://semver.org/). Format follows [Common Changelog](https://common-changelog.org/).

---

## 4.9.0 - 2026-05-28

### Added

- Radiation hero panel: secondary line "FEEDING 4 GLOBAL NETWORKS" added below CPM reading
- Radiation popup: new section below chart legend with explanatory text and four clickable network cards — GMCmap (station history link), Radmon.org (homepage only, no username), Safecast (user profile link), OpenSenseMap (sensor explorer link)
- Radiation monitoring station IDs: GMCmap 55351704929, Safecast user 11757, OpenSenseMap box 6a1a13ffb6f94800083a8d55 / sensor 6a1a13ffb6f94800083a8d56

### Notes

- Radmon.org username deliberately omitted from all public-facing pages per owner preference

---

## 4.8.0 - 2026-05-27

### Added

- `localAcHref(hex)` JS function: returns `https://devinmitchell.com/adsb/?icao={HEX}` — links to self-hosted ultrafeeder for live aircraft
- Nginx reverse proxy configured on devinmitchell.com VM: `/adsb/` → `http://10.0.2.16:8078/`

### Changed

- Live Intercepts modal (Aircraft Overhead Right Now): callsign links now open local ADS-B station (`devinmitchell.com/adsb/?icao=`) instead of FR24 — shows live position, not just flight history
- Historical modals (Today's Log, Range leaderboard, Speed leaderboard, Frequent Flights): remain on ADSBexchange with `showTrace={date}` — correct for past flights
- ADS-B Ground Station badge: expanded to show all three tracker IDs (Flightradar24: T-EGXU127, AirNavRadar: EXTRPI709473, FlightAware: Site 274543); badge now links to `devinmitchell.com/adsb/` instead of AirNavRadar

---

## 4.7.0 - 2026-05-27

### Fixed

- **HA automation: MIL_ALT altitude check removed for military trigger.** Two RAF Eurofighter Typhoons (HAVOC042/ZK306 and ZK376) flew directly over the station at 375–500ft without triggering a notification. Root cause: `MIL_ALT: 5000` filtered out HAVOC042 which was at 10,025ft when polled by the 10-second cron — above the threshold despite being on a low-level run. Military aircraft now notify at **any altitude** when heading toward the station (angle ≤ MAX_ANGLE, distance ≤ MAX_DIST, speed ≥ MIN_SPEED). This gives advance warning before a potential low-level pass rather than waiting until the aircraft is already low and possibly below ADS-B horizon.

---

## 4.6.0 - 2026-05-27

### Added

- `getRegLabel()` JS function: detects civil aircraft registrations used directly as callsigns (G-EKJD, N807DC etc.) and displays country origin — e.g. "UK Civil", "US Civil", "German Civil". Covers UK, US, Germany, France, Netherlands, Ireland, Spain, Italy, Switzerland, Austria, Belgium, Sweden, Norway, Denmark, Finland, Canada, Australia. No `?` shown — registration pattern is unambiguous.
- ICAO_PREFIXES expanded from 309 to 326 entries — added Eurowings (EWG), Atlas Air (GTI), Hainan Airlines (CHH), Nouvelair (NOZ), Titan Airways (NSZ/AWC/TCW), PLAY Airlines (FLE), Pegasus (PGT), flydubai (FDB), Wizz Air UK (WUK), Air Transat (TSC), Transavia (TVF/HV), FedEx (FDX), UPS, SkyWest (SKW), Frontier (FFT), JetBlue (JBU), WestJet (WJA), Air Greenland (OAY), Garuda (GIA), Malaysia Airlines (MAS), Vietnam Airlines (HVN), SriLankan (SLK), IndiGo (IGO), Air Arabia (ARE/ABY), Nile Air (GNJ), Kalitta Air (CKS), and others

### Changed

- `isHexConfirmed()` now also returns true for reg-pattern callsigns — suppresses `?` for UK/US/etc civil registrations since they are pattern-confirmed not guesses
- `getMilitaryLabel()` now returns "Royal Air Force" (full), "Royal Navy", "US Air Force", "French Air Force", "German Air Force" (was RAF/USAF/French AF)

### Fixed

- HA automation: removed `url`/`clickAction` template blocks that caused "message malformed" error; FR24 link now embedded directly in message text as `fr24.com/CALLSIGN`
- HA automation: `mode: single` cooldown can be reset by disabling/enabling the automation without HA restart
- Squawk description now sent in MQTT payload from `export-stats.py` via `_squawkMap` lookup on `ac_mqtt_payload()`
- Category-based speed sanity filter now prevents corrupt ADS-B packets entering speed leaderboard

---

## 4.5.0 - 2026-05-27

### Added

- Category badge (`CAT`) column added to all four modals (Live Aircraft, Today's Log, Top 10 Range, Top 10 Speed) — shows ADS-B emitter category code (A1–A7, B1–B7, C1–C3) with hover tooltip explaining what each category means in plain English
- `catLabel()` JS function + `.cat-badge` CSS class with CSS tooltip — same pattern as squawk badge

### Changed

- `export-stats.py`: category-based speed sanity filter added to speed leaderboard. Speeds above the physical maximum for the reported aircraft category are silently discarded before insertion into `speed_log`. Caps: A1=260kt, A2=380kt, A3=620kt, A5=660kt, A7=210kt, A6=uncapped (fast jets). Unknown category capped at 700kt. Eliminates corrupt ADS-B packets like a Piper Tomahawk reporting 1464kt.

### Fixed

- Bogus speed records from corrupt ADS-B packets (transponders occasionally broadcast garbage ground speed data) will no longer enter the leaderboard — no database reset required, existing bad entries can be removed manually via `adsb-manage.sh del-speed CALLSIGN`

---

## 4.4.0 - 2026-05-26

### Added

- Travel and language paragraph added to About Me section: born in Germany, raised in the US, settled in Yorkshire; B1→B2 German; York proximity; travel plans with partner
- `languages:` line added to terminal box: `English (native) · German (B1→B2)`

### Changed

- About Me final paragraph split: "Dual US/UK citizen" moved to preceding paragraph; travel content in new dedicated paragraph

---

## 4.3.0 - 2026-05-26

### Added

- New hero panel: **Top 10 Frequent Flights** — shows the #1 most frequently seen callsign as the stat; click to open full top-10 table with days seen, airline, first and last date
- `freq-overlay` modal: table with rank, days seen, airline · callsign, first date, last date; explanatory footnote for non-technical visitors
- `freq-aircraft.json` polled every 10 seconds alongside other live stats; hero panel updates automatically

### Changed

- Hero panel labels rewritten for plain-English clarity: "Live Aircraft" → "Aircraft Overhead Right Now", "Today's Aircraft" → "Aircraft Seen Today", "Today's Airlines" → "Airlines Seen Today", "Fastest Speed Recorded KT" → "Top 10 Fastest Speed", "Farthest Distance Recorded" → "Top 10 Furthest Distance"
- All panel `title` tooltip text updated to explain what clicking does in plain language
- Speed and Range modal subtitles updated to plain English
- Real-Time Live Stats label updated to "Live Flight Data — North Yorkshire"

---

## 4.2.0 - 2026-05-26

### Added

- `freq_aircraft` table in `stats.db`: one row per unique hex, tracks `callsign`, `days_seen` (increments once per calendar day), `first_date`, `last_date` — concurrent-safe via `ON CONFLICT` with `CASE WHEN last_date <` guard
- One-time backfill from existing `flights_log` data on first run (`freq_aircraft_seeded` kv flag)
- `freq-aircraft.json` written to web root each cron run — top-10 most frequently seen aircraft by days seen
- `adsb/leaderboard/alltime/frequent` MQTT topic — JSON array of top-10
- `freq_aircraft` cleared by `reset-stats.sh` alongside other tables; `freq_aircraft_seeded` flag also cleared so backfill re-runs on next cron tick
- `show_freq` command added to `adsb-manage.sh` (interactive option 9)

---

## 4.1.0 - 2026-05-25

### Changed

- All four aircraft modals (Live Intercepts, Today's Log, Range leaderboard, Speed leaderboard): airline name now displayed inline with callsign on one line (`AIRLINE · CALLSIGN`) rather than as a smaller div above it — removes double-height rows and keeps table compact
- Airline name truncated at 12 characters with ellipsis if longer (e.g. "BRITISH AIRW…") to preserve column width
- Squawk codes now displayed as coloured bordered badges (colour by category: red=EMERGENCY, amber=SPECIAL, purple=MILITARY, cyan=VFR, green=IFR) with CSS tooltip on hover showing full squawk description — replaces the extra sub-row that previously appeared below each aircraft
- Squawk description extra rows removed from all four modals — one line per aircraft throughout

---

## 4.0.1 - 2026-05-24

### Changed

- ADS-B stat panel: "ALL-TIME" secondary now shows two figures — total intercepts (same aircraft on separate days counted each time) and total unique ICAO hex codes ever seen
- ADS-B log modal sub-header: updated to show both "TOTAL SEEN" and "UNIQUE" counts
- `adsb-stats.json` now includes `flights_total` field alongside existing `aircraft_total`

### Added

- `flights_log` table in `stats.db`: primary key `(hex, date)` — one row per aircraft per calendar day (UTC). Concurrent-safe via `INSERT OR IGNORE`. `COUNT(*)` = total intercepts, never drifts

### Fixed

- `export-stats.py`: `init_schema()` now creates `flights_log` table on upgrade; `migrate_legacy()` seeds it from existing `aircraft_log` entries

---

## 4.0.0 - 2026-05-24

### Changed

- `export-stats.py`: replaced `stats-state.json` + `seen-hexes.txt` with SQLite (WAL mode) at `/opt/adsb/stats.db`
- `acc_aircraft` now derived from `COUNT(*) FROM seen_hexes` — was a drifting counter, now always accurate
- `aircraft_log.messages` stored as `MAX(seen)` per contact — was `+= ac['messages']` each run causing 13× overcount (SAS542: 11.3M messages logged vs ~864k maximum possible in 24h)
- All DB writes wrapped in transactions with `busy_timeout=5000ms` — 6 concurrent cron instances now serialise safely
- `range_log` / `speed_log`: `ON CONFLICT ... WHERE` updates only when new value beats existing record
- `squawk_log`, `rad_history`: trimmed in-DB rather than in-memory slicing

### Added

- One-time migration auto-imports `stats-state.json` and `seen-hexes.txt` on first run; sets `kv['migrated']='1'` on completion
- SQLite tables: `kv`, `seen_hexes`, `aircraft_log`, `range_log`, `speed_log`, `squawk_log`, `rad_history`
- Indices on `aircraft_log(first_seen)` and `aircraft_log(last_seen)` for `aircraft_today`/`aircraft_24h` count queries

### Fixed

- Race condition: concurrent cron instances writing `stats-state.json` could corrupt JSON on partial write, silently resetting all accumulators including `acc_aircraft` to 0
- Race condition: two instances discovering different new hexes simultaneously both incremented `acc_aircraft += 1` — one hex silently dropped from counter (old `acc_aircraft` 3,371 vs canonical `seen-hexes.txt` count 2,859 confirms this)
- `messages` accumulation bug: readsb `messages` field is a per-contact cumulative counter, not a delta — was added to log total 360 times/hour
- Speed leaderboard empty: `speed_log` key missing from state files that predate the feature; SQLite table exists from day 1

---

## 3.5.10 - 2026-05-24

### Changed

- York ADS-B project card: updated from RTL-SDR Blog V4 + LPDA to RadarBox FlightStick (1090 MHz dedicated receiver with integrated 20.5 dB amplifier, bandpass filter, ESD protection) and AirNav 1090 MHz outdoor omnidirectional antenna
- Lab modal ADS-B row: updated receiver name and tracker count (6 → 11)
- Live Intercepts modal sub-header: updated from "RTL-SDR BLOG V4" to "RADARBOX FLIGHTSTICK · 1090MHz DEDICATED"
- Foundation Licence project card: generalised "The RTL-SDR for ADS-B" to "The ADS-B station" (receiver has changed)

---

## 3.5.9 - 2026-05-21

### Added

- New project card: "UK Foundation Licence" — Amateur Radio · In Progress. Covers RSGB Foundation exam (June 2026), packet radio motivation (Direwolf/LinBPQ, 9600 baud), and the RTL-SDR ADS-B origin story. Inserted after WWIV BBS Revival card.

---

## 3.5.8 - 2026-05-19

### Changed

- Title, meta, and structured data: "AWS Cloud Architect" → "DevOps Engineer" throughout
- Hero subtitle: "AWS Cloud Architect · Terraform Specialist · Platform Engineer" → "DevOps Engineer · Terraform · AWS · Platform Engineering"
- Terminal widget role field updated to match
- Bio paragraphs rewritten: quieter tone, no quantified claims presented as achievements, honest about automation being primarily a laziness mitigation strategy and AI usage being about efficiency rather than innovation
- All skill card descriptions toned down: removed superlatives, "only practice", "I became the runbook", and similar
- Timeline entries: removed self-congratulatory framing; Spot Award retained but understated; M3 tooling described as "quicker than doing it by hand" rather than a significant achievement
- Project descriptions: "overengineered solution to a problem that did not strictly require solving" for clucks.net; energy project now notes the DNO wait with appropriate resignation
- Contact blurb: removed "guaranteed", softened generally

---

## 3.5.7 - 2026-05-19

### Added

- `acDate(iso)` helper — extracts `YYYY-MM-DD` from ISO timestamp for `showTrace` parameter
- `acHref(hex, date)` helper — builds `https://globe.adsbexchange.com/?icao={HEX}&showTrace={DATE}` URL
- Airline Traffic popup now fetches `aircraft-log.json` in parallel and shows most recent flight per airline: callsign (linked), altitude, speed, distance, time ago

### Changed

- All historical callsign links switched from FR24 (`/data/aircraft/{hex}`) to ADS-B Exchange (`globe.adsbexchange.com/?icao={HEX}&showTrace={DATE}`) — links open the aircraft's actual flight trace for the day it was seen, working whether the flight is active or completed
- `csLink(cs, hex, date)` and `csMuted(cs, hex, date)` updated with optional `hex` and `date` params; when hex provided, links to ADSBexchange with showTrace; callsign-only (live aircraft table) retains FR24 link
- Today's Intercepts: links use `ac.last_seen` date for `showTrace`
- Range leaderboard: links use `ac.recorded` date for `showTrace`
- Speed leaderboard: links use `e.recorded` date for `showTrace`
- Airline Traffic last-flight links: use `ac.last_seen` date

### Fixed

- FR24 `/data/aircraft/{hex}` URL was silently redirecting to a generic aircraft production-list page — no error, just the wrong page entirely

---

## 3.5.6 - 2026-05-17

### Added

- Airline name stacked above callsign in Fastest Speed Recorded leaderboard
- Airline name stacked above callsign in Farthest Distance Recorded leaderboard
- `_airlineMap` now preloaded in both Speed and Range modals on open (was only loaded in Live Intercepts and Today's Intercepts)
- Speed and Range leaderboard thead updated: CALLSIGN → AIRLINE · CALLSIGN

### Changed

- Squawk description rows across all popups: font raised from `.55rem` to `.72rem`, opacity cap removed, coloured left-border accent added (`border-left:3px solid {squawk colour}`) and dark background wash — significantly more visible and visually distinct
- Global readability pass across all popup modals and hero stat panel:
  - Modal sub-headers: `9px / rgba(.45)` → `11px / rgba(.70)` (Live Intercepts, Today's Intercepts, Range, Radiation, Lab)
  - All table headers: `.52rem` → `.60rem`; body cells: `.62rem` → `.68rem`
  - Stat panel labels: `.52rem / var(--muted)` → `.62rem / rgba(255,255,255,.55)`
  - Stat panel secondary: `.50rem` → `.58rem`; arrow: `.50rem / opacity:.50` → `.62rem / opacity:.75`
  - Airline names above callsigns: `.42rem / var(--muted)` → `.55rem / rgba(255,255,255,.65)`
  - Airline ICAO prefix codes: `.50rem / var(--muted)` → `.60rem / rgba(255,255,255,.55)`
  - World clocks: city `.52rem→.60rem`; timezone `.45rem / rgba(.30)→.55rem / rgba(.55)`; date `.50rem→.58rem`
  - OpenHASP popup: section headers `.55rem→.65rem`; rows `.58rem→.65rem`; key labels `var(--muted)→rgba(255,255,255,.65)`; page list `.52rem→.60rem`
  - Graph labels: `.52rem / rgba(.40)` → `.60rem / rgba(.65)`; timeframe buttons `.55rem→.62rem`
  - Log filter controls: SHOW label `.55rem→.62rem`; buttons `.60rem→.65rem`
  - `opacity:.35` placeholder dashes → `.62`; loading/empty states `.40` → `.65`
  - `.no-pos` rows: `opacity:.40` → `.65`
  - Speed/Range recorded and signal columns: `var(--muted)` → `rgba(255,255,255,.6)` with `.65rem`
  - Speed "kt" unit label: `.50rem / opacity:.60` → `.65rem / opacity:.75`
  - Inline speed/airline modal sub-headers: `.52rem / rgba(.40)` → `.60rem / rgba(.65)`

---

## 3.5.5 - 2026-05-17

### Added

- **Airline name in Live Aircraft popup**: the first column of the live aircraft table now shows the airline name (e.g. "KLM", "Ryanair", "Jet2") in a small muted line above the callsign link. Lookup is performed against `/airline-tally.json` using the 3-character ICAO callsign prefix; result is cached for the browser session in `_airlineMap`. Gracefully renders nothing (callsign only) if the prefix is unknown or the fetch fails.

### Changed

- Live Aircraft modal (`#ac-box`) max-width bumped from 820px to 860px to accommodate the stacked airline/callsign cell; `overflow-x:auto` added to `#ac-box` as a safety net
- `#ac-table` header `CALLSIGN` renamed to `AIRLINE · CALLSIGN` to reflect new content; column count unchanged (still 7)
- `#ac-table` cell padding tightened from `5px 6px` to `4px 5px`; header font tracking reduced slightly — all columns remain on one line without scrolling

---

## 3.5.4 - 2026-05-16

### Added

- SQUAWK column and coloured description row added to Today's Intercepts (aircraft log) popup — same `sqLabel()` + `squawk.map` lookup pattern as Live Intercepts table
- SQUAWK column and coloured description row added to Fastest Speed Recorded leaderboard popup
- SQUAWK column and coloured description row added to Farthest Distance Recorded leaderboard popup
- `loadSquawkMap()` preloaded on open of all three modals above (previously only preloaded in live aircraft modal)

### Changed

- Today's Intercepts row selector extended: was `5 / 10 / 20 / 50`, now `5 / 10 / 20 / 50 / 100 / 200 / ALL`
- Default selection changed from 5 to 10 (highlighted on open)
- `renderLog()` now accepts `null` for ALL (previously only accepted integers)

### Fixed (`export-stats.py`)

- `squawk` field now captured per entry in `aircraft-log.json` — was always absent; only updates when aircraft is actively squawking (preserves last-known code for remainder of 24h window if squawk stops)
- `squawk` field now captured at record time in `max-range-log.json` and `max-speed-log.json` — leaderboard entries will carry the squawk active at the moment the record was broken

---

## 3.5.3 - 2026-05-16

### Security

- `rel="noopener noreferrer"` added to all `target="_blank"` links generated by `csLink()`, `csMuted()`, and inline callsign links in the speed modal — previously `noreferrer` was absent, leaving `window.opener` accessible in the new tab

### Fixed

- `openSquawkLog()` no longer calls `window.alert()` — now reuses the speed overlay (consistent with existing modal pattern; `window.alert()` blocked page interaction and was visually jarring)
- Speed stat (`ls-speed-record`) silently stopped updating whenever the squawk-alerts fetch failed, because the speed fetch was nested inside the squawk `.then()` chain — both fetches are now independent parallel calls in the poll loop
- Dead CSS rules `.hero-stats` and `.stat` removed from `@media(max-width:900px)` block — ghost classes from a previous layout iteration with no matching HTML elements

### Added

- `og:image:width` (1200) and `og:image:height` (630) meta tags — missing values caused unreliable image sizing in LinkedIn and other social unfurlers

### Changed

- `#lab-content` padding shorthand simplified from `20px 24px 20px` to `20px 24px` (redundant value)

---

## 3.5.2 - 2026-05-16

### Security

- `marked.parse()` output in changelog modal now sanitised via DOMPurify (cdnjs 3.1.6) — prevents XSS if CHANGELOG.md is ever tampered with at the server layer

### Fixed

- `SQBUILTIN`: `0033` corrected from "Search and rescue" to "UK aircraft paradropping / parachute drop zone active"; `1177` corrected from Military/low-level to IFR Swanwick FIS ORCAM transit code; `7000` category corrected to VFR; `2000` category corrected to IFR
- Squawk prefix range lookup (`xN00`) now guarded by `sqMapLookup()` helper — prevents `7xxx` unknown codes falsely resolving to `7000` (UK VFR conspicuity) and `2xxx` falsely resolving to `2000`
- Duplicate squawk map entries for codes `0025`, `0027`, `2654`, `2677` removed from `squawk.map` — second entry was silently overwriting first
- `openSquawkLog()` now sets `#speed-modal-title` to "EMERGENCY SQUAWK LOG" when hijacking the speed overlay; `openSpeedModal()` restores it — previously the speed overlay header remained "TOP SPEED LEADERBOARD" when showing squawk log data
- `colspan="8"` corrected to `colspan="7"` in aircraft live table empty/error states (table has 7 columns)
- Backdrop click on modal overlays now correctly closes without leaking orphaned `keydown` listeners — all seven individual `escXxx` handler functions removed; single centralised Escape handler installed covering all overlays
- `csMuted()` consolidated into `csLink()` — was identical function with cosmetically different fallback span
- Radiation modal sub-header corrected: "LAST 24 HOURS · 1 SAMPLE PER MINUTE" → "ROLLING 4-HOUR WINDOW · ~10-SECOND SAMPLES" (reflects actual data served)
- Aircraft log modal sub-header corrected: "TODAY (UTC)" → "ALL-TIME SINCE 13 MAY 2026" (log contains full history, not just today)
- Nav logo `href="#"` changed to `href="/"` — previously added `#` to browser history on click

### Changed

- Tokyo removed from world clocks modal (HTML cell and JS zones array)
- `lab-rack` image (`/rack.jpg`) gains `loading="lazy"` — image is behind a modal and never visible on initial page load

---

## 3.5.1 - 2026-05-16

### Changed

- Page `<title>`, `og:title`, and meta description: "AI Prompt Engineer" removed; replaced with "AI-Augmented Engineering" where appropriate
- `whoami` terminal block: `also:` field changed from "AI Prompt Engineer / Agentic AI" to "AI-Augmented Engineering"
- AI skill card renamed from "AI Prompt Engineering & Agentic Systems" to "AI-Augmented Engineering"; description toned down — removed "what used to take a sprint now takes a session"
- Current role timeline: "regularly stepped into scrum master and product owner responsibilities" softened to "pitched in on agile ceremonies when the team needed cover"
- Current role timeline: removed retroactive AI framing of M3 — M3 description now accurately reflects what it was; AI-augmented practices described as a later expansion of the role
- TS/SCI clearance badge: removed pulsing amber animation — clearance has been inactive since 2013 and doesn't need a heartbeat
- RHCE (2009) cert: "(Expired)" label added for consistency with AWS cert treatment

---

## 3.5.0 - 2026-05-15

### Changed

- York ADS-B description updated: now accurately reflects 11 platforms + 6 MLAT networks + Docker stack detail
- Project card order updated: The Lab · clucks.net · York ADS-B · OpenHASP (row 1), The United Benefice · WWIV BBS Revival · Off-Grid-ish · Industrial/EBM (row 2)

---

## 3.4.0 - 2026-05-15

### Added

- UTC clock stat box is now clickable → opens world clocks popup with live ticking times
- Cities: UTC · UK · Washington DC · Colorado Springs · San Jose · Mumbai · Tokyo · Alice Springs
- UTC displayed full-width at top in green; city clocks in 2-column grid with local timezone offset shown
- Clocks update every second; ESC or click-outside to close
- GoAccess visitor stats page live at `/stats.html` — updated by cron every minute on 10.0.2.203
- Stats exclude internal polling IP (10.0.2.20)

---

## 3.2.0 - 2026-05-14

### Added

- OpenHASP Display Network project card: 16 × Lanbon L8 touchscreen switches running OpenHASP, driven by Home Assistant via MQTT across 12 interactive pages per device
- "Live Display Preview ↗" link opens popup with live BMP screenshot from desk unit, auto-refreshing every 10s with animated progress bar
- Popup includes: hardware specs, full 12-page guide (P0–P11 key), status bar legend
- `export-stats.py` fetches `/hasp-screenshot.bmp` from desk display every cron run (every 10s)

---

## 3.1.0 - 2026-05-14

### Changed

- Graph assignments updated: Active aircraft popup → Message Rate; 24h aircraft popup → Aircraft Seen + Tracks Seen side by side; Range popup unchanged
- `local_trailing_rate` graph added to cron copy list
- Live aircraft popup blurb updated to list all 6 feeding platforms with station IDs and links

---

## 3.0.0 - 2026-05-13

### Added

- graphs1090 dark mode enabled (`GRAPHS1090_DARKMODE=true` in ultrafeeder environment)
- Persistent graph data volume mapped: `/opt/adsb/ultrafeeder/graphs1090:/var/lib/collectd`
- Live performance graphs embedded in three ADS-B popups with 24h/7d/30d timeframe switcher:
  - Active aircraft intercepts popup: Tracks Seen graph
  - Aircraft last 24h popup: Aircraft Seen/Tracked graph
  - Max range popup: Range (NM) + Signal Level (dBFS) side by side
- `export-stats.py` copies graph PNGs from ultrafeeder nginx to web root on each cron run
- Graphs served from `/graphs/` directory on devinmitchell.com

---

## 2.9.0 - 2026-05-13

### Added

- York ADS-B now feeding 6 platforms simultaneously: Flightradar24 (T-EGXU127), FlightAware (site 274543), PlaneFinder (receiver 237463), RadarBox (EXTRPI709473), OpenSky Network (serial -1407997523), ADS-B Exchange
- All 6 feed links displayed in project card with station IDs
- Live aircraft popup blurb updated to list all 6 platforms
- The Lab popup ADS-B entry updated to reflect 6 trackers
- Antenna coordinates updated to accurate values (not published)

---

## 2.8.0 - 2026-05-13

### Added

- Snake game easter egg: 🐍 snake link in footer opens full-screen canvas Snake game in site theme (green snake, amber food, dark grid); arrow keys or WASD; space/enter to start or restart; ESC to exit; high score persists per session

---

## 2.7.0 - 2026-05-13

### Changed

- Live stats and now-playing poll every 10 seconds (was 60 seconds), matching cron frequency
- Cron extended to 6 entries (every 10 seconds via sleep offsets)

---

## 2.6.0 - 2026-05-13

### Changed

- "Total local aircraft tracked" stat now shows last 24 hours only (`aircraft_24h`)
- Aircraft log popup retitled "Local Aircraft — Last 24 Hours"
- Aircraft log trimmed to entries with `last_seen` within 24h (max 500), replacing fixed 100-entry limit
- All-time total (`acc_aircraft`) retained in state and output JSON but not displayed

---

## 2.5.0 - 2026-05-13

### Changed

- Callsigns in all three ADS-B popups (live aircraft, aircraft log, range leaderboard) are now clickable links to `https://www.flightradar24.com/CALLSIGN` — opens live map or recent history for that flight in a new tab; hover turns amber

---

## 2.4.0 - 2026-05-13

### Added

- The United Benefice project card: pro bono site for five Church of England village churches in the Vale of York; links to theunitedbenefice.co.uk

---

## 2.3.0 - 2026-05-13

### Added

- The Lab popup: rack photo (full height, no crop), 6-panel spec grid covering Compute, Storage, Networking, Power & Resilience, Self-Hosted Services, Offline Stack
- "Explore the Home Lab ↗" project-link at bottom of The Lab card
- Storage detail: 4×16TB HDD SHR (~42TB usable) + 4×SSD scratch/dev bays, 64TB raw

### Changed

- All internal IP addresses removed from The Lab popup
- "York" removed from entire site — location references now "York, North Yorkshire" at most
- Lab popup subtitle changed to "SELF-HEALING · AUTOMATED · ZERO UNPLANNED OUTAGES"

### Removed

- "Always something broken at 2AM" subtitle

---

## 2.2.0 - 2026-05-13

### Added

- wwiv.uk link added to WWIV BBS Revival project card

---

## 2.1.0 - 2026-05-13

### Added

- Top-10 range leaderboard popup on Log-Periodic Antenna Active Range stat
- Entries show rank (🥇🥈🥉 medals for top 3), distance NM, callsign, hex, altitude, speed, heading, signal, timestamp
- `max-range-log.json` written by cron — persists top-10 furthest aircraft across restarts
- Aircraft log popup on Total Local Aircraft Tracked stat: rolling log of last 100 aircraft, table with 5/10/20/50 row selector, columns: callsign, hex, last heard (relative), altitude, speed, heading, distance, message count, signal
- `aircraft-log.json` written by cron

---

## 2.0.0 - 2026-05-13

### Added

- ADS-B live aircraft popup on Tracking Local Aircraft stat: table of currently decoded aircraft with callsign, hex, altitude, speed, heading with arrow, distance NM, signal strength
- Explanation of FR24/FlightAware feed contribution in popup footer
- `aircraft-live.json` written by cron

---

## 1.10.0 - 2026-05-13

### Fixed

- Radiation history state save moved to after radiation CPM append — previously state was saved before new entry was appended, causing rad-history.json to show only seeded data + current reading on every cron run
- rad_poll.sh added to clucksnet crontab (`* * * * *`) — previously never scheduled, causing gaps in clucks.net radiation history

### Changed

- Hero stats repositioned from top:5.5rem to top:10rem to clear UTC clock overlap
- GitHub link added to nav and JSON-LD sameAs (missed in 1.9.0)


---

## 1.9.0 - 2026-05-12

### Changed

- Now-playing label reformatted: uniform amber color/font, reads "Currently listening to — LIVE @ HH:MM UTC"
- GitHub link added to nav and contact section (amber, alongside LinkedIn)
- JSON-LD `sameAs` updated to include GitHub profile

---

## 1.8.0 - 2026-05-12

### Added

- Plex now-playing widget below hero subtitle, fetching from `/now-playing.json` every 60 seconds
- Music: shows Artist, Album, Song with pulsing amber "CURRENTLY LISTENING TO — LIVE" label and timestamp
- Video: shows Show and Episode with "CURRENTLY WATCHING — LIVE" label
- Hidden when nothing is playing; colour-coded labels switch automatically by media type
- `now-playing.json` written by `export-stats.py` cron — queries Plex API at `10.0.2.8:32400`, music sessions take priority over video

---

## 1.7.0 - 2026-05-12

### Changed

- Hero eyebrow row moved higher (padding-top reduced from 8rem to 5rem)
- Live stats panel repositioned to top-right to align with Real-Time Live Feeds label
- Max range stat converted from kilometres to nautical miles (NM)
- Stat labels: Tracking Local Aircraft, Total Local Aircraft Tracked, Max Local Range Tracked, Local Radiation Level

---

## 1.6.0 - 2026-05-12

### Added

- NORAD-style UTC clock (updates every second) inline with Real-Time Live Feeds label
- Background radiation stat opens 24-hour canvas chart modal (GMC-800 Geiger counter, 1 sample/min, hover tooltip)
- `rad-history.json` served from web root — populated by cron every minute, max 1440 entries

### Changed

- Hero eyebrow restructured as flex row: location left, Real-Time Live Feeds + UTC clock right
- Radiation history popup enlarged to 720px wide, 220px canvas height
- "Dual US/UK National" removed from hero eyebrow
- Stat panel labels updated: Tracking Overhead Aircraft, Total Aircraft Tracked, Local Radiation Level

---

## 1.5.0 - 2026-05-12

### Added

- Real-time live stats panel in hero (right side): Aircraft Overhead, Aircraft Tracked, Max Range, Local Radiation Level
- Live stats fetch from `/adsb-stats.json` every 60 seconds; values flash amber on update
- Radiation CPM colour-coded: green <50, amber 50-100, red >100

### Changed

- "Runbooks unread" career stat removed; replaced with live feed stats

---

## 1.4.0 - 2026-05-12

### Added

- York ADS-B project card (first position): RTL-SDR Blog V4 feeding Flightradar24 (T-EGXU127) and FlightAware (site 274543)
- Links to live FR24 coverage map and FlightAware public feeder stats

---

## 1.3.0 - 2026-05-12

### Added

- LinkedIn profile link in nav, contact section, and JSON-LD `sameAs`
- Changelog footer link opens modal rendering `CHANGELOG.md` via marked.js (cdnjs v9.1.6); ESC and click-outside close

---

## 1.2.0 - 2026-05-08

### Added

- Certifications timeline (2001-2022): Sun Solaris 8 I&II, Red Hat Network Services & Security, RHCE, InfoBlox DNS, AWS Solutions Architect Associate, AWS Security Specialty — expired certs clearly labelled
- Claude Sonnet 4.6 badge on current role
- Conferences nav link
- AI Prompt Engineering, Claude Sonnet 4.6, Agentic AI added to scrolling ticker

### Changed

- Current role expanded: AWS bootcamp training, scrum master / product owner responsibilities, intern hiring for DocOps project
- About section no longer contains employer-specific account and region counts
- Hero name green glow text-shadow added
- TS/SCI clearance badge amber pulse animation added
- Skill and project card hover lift with drop-shadow
- Active timeline marker green pulse animation
- `prefers-reduced-motion` accessibility media query added

---

## 1.1.0 - 2026-05-07

### Added

- AI Prompt Engineering & Agentic Systems skill card (Claude Sonnet 4.6, agentic workflows, tool-use orchestration)
- Conferences & Events section: ArchCon 2021 (presenter), AWS Summit London 2026 / re:Invent 2018-2022 (attendee)
- WWIV BBS Revival project: wwiv.uk, WWIVnet UK node goal
- TS/SCI CI Poly clearance badge: Lockheed Martin, Hewlett-Packard, Sotera Defense Solutions
- `CHANGELOG.md` footer link, visitor tracking, `sitemap.xml`, `robots.txt`

### Changed

- SEO title, meta description, and keywords target DevOps engineer, AI prompt engineer, Terraform consultant
- JSON-LD updated to reflect AI Prompt Engineering specialism
- Nav logo updated to `// devinmitchell.{com,co.uk}`
- Hero subtitle revised: employer-specific stats removed, dry humour added
- Terminal `whoami` block includes `AI Prompt Engineer / Agentic AI`
- Music section reflects listening tastes rather than music production

### Removed

- "Numbers That Matter" section (employer-specific)
- NIS infrastructure references (deprecated technology)
- Specific account and region counts from skill descriptions

---

## 1.0.0 - 2026-05-06

_Initial release._

### Added

- Full site: dark industrial/terminal aesthetic, scanlines, noise overlay, grid background
- Hero with animated stats and typed terminal effect
- Skills grid: AWS, Terraform, IaC, Golden Image Pipelines, CI/CD, Linux/Unix, Networking, Security, Containers, Documentation as Code, Programming, Observability
- Career timeline 1991-present: WWIV BBS through current role
- TS/SCI CI Poly clearance badge
- Projects: clucks.net, Off-Grid-ish, The Lab, Industrial/EBM, WWIV Revival
- Contact section with `public@devinmitchell.com`
- Scroll-reveal animations with per-card stagger
- Google Fonts: Share Tech Mono, Bebas Neue, DM Sans
- Scrolling tech ticker
