Commit Graph

428 Commits

Author SHA1 Message Date
itsrubberduck
034ddbab2e feat(bug-reports): trace reports by code and tell Live ATC from Classroom
Every report now gets a UUID that the notification mail quotes as
"Nenne den Fehlercode <uuid> beim Commit", so a fix can be tied back to
the report it closes. The mail also names the area it came from, and the
comment field is relabelled "Fehlerbeschreibung/Featurewunsch" since it
collects feature wishes just as often as bugs.

Classroom loses its plain feedback link and gets the same reporter as
Live ATC — screenshot, arrow annotation and all. `useBugReport` takes an
options object so the communications-engine snapshot stays optional; the
dialog moves out of `live-atc/cockpit` now that both pages use it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 21:11:43 +02:00
itsrubberduck
4dbd0bda9f fix(live-atc): keep the callsign visible on narrow screens
The cockpit HUD is already a sticky top bar carrying the callsign, but a media
query hid the whole flight-context group below 720px — so on a phone the pilot
had no way to see their own callsign without opening the flight sheet.

Shrink the group there instead of hiding it. Verified at 320/375/1280px: the
callsign stays in the sticky bar while the body scrolls, and the HUD row still
fits without horizontal overflow (318px of 318 at the 320px width).

Also fills two gaps the mock dev harness had after the ATIS work: it now stubs
/api/airports/:icao/atis, and its STT fallback no longer transcribes to
"say again", which the backend now intercepts as a request to repeat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 20:12:34 +02:00
itsrubberduck
9ead29a52a fix(live-atc): match the D-registration to the aircraft class
VFR scenarios drew from a fixed pool of D-E** registrations regardless of type,
so an A320 was assigned "D-ETMO". The letter after "D-" is assigned by weight
and type class (LuftVZO Anlage 1): D-E is single-engine pistons up to 2 t, so
an airliner has to be D-A.

Pick the class from the ICAO type designator — D-A for airliners, D-I for light
twins, D-H for helicopters, D-E for light singles and anything unrecognised,
since VFR scenarios are overwhelmingly light aircraft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 20:05:04 +02:00
itsrubberduck
8b62b8e3aa feat(live-atc): drive ATIS letter, runway, QNH and wind from the resolved report
The broadcast and the flow variables used to read different sources: the audio
came from VATSIM while `information` was genATIS() — a random A-Z letter — and
the runway came from genRunway(), a hard-coded list unrelated to the airport.
So the pilot could hear "information Q" and be expected to call "information T".

Both now read the same AtisReport. The frequency list backfills the letter and
broadcast text from it, so an ATIS station always announces an information
letter even when no VATSIM controller is online. QNH and surface wind come from
the same observation, so the controller stops contradicting the ATIS.

A synthesised ATIS is labelled "Simulated ATIS" in the frequency picker rather
than being credited to VATSIM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 19:40:40 +02:00
itsrubberduck
bcf9a9621c feat(atis): serve the resolved ATIS from /api/airports/:icao/atis
Composes the three live sources into one AtisReport: VATSIM ATIS stations,
the METAR, and the OpenAIP runway ends the frequency endpoint was already
fetching and discarding.

The upstream fetches move into server/utils/airportSources.ts with a TTL cache
matched to each source (VATSIM 20s, METAR 5min, OpenAIP 6h). Without it, adding
this endpoint would have meant pulling the multi-megabyte VATSIM datafeed twice
per session start; the frequency endpoint now shares the same cached copy.

Verified against live data: EDDF (no VATSIM ATIS) synthesises information I with
runway 25C from wind 260/14, while EDDC and EDDM take letter and runway from
their live VATSIM broadcasts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 19:37:16 +02:00
itsrubberduck
aee42e31df feat(atis): resolver core for reliable information letter and runway
VATSIM is the only ATIS source today, so an airport with no controller online
(EDDF, on the day this was reported) falls back to a raw METAR — which carries
neither an information letter nor a runway in use.

Add shared/utils/atisReport.ts, which turns whatever the live sources returned
into one AtisReport:

  vatsim   station broadcasting text; letter from atis_code or parsed from the
           text; runway parsed from the text
  metar    no station: letter derived from the observation time, runway from the
           headwind component over the published runway ends
  fallback no METAR either: letter from the clock, airport's main runway

Deriving the letter from the observation keeps it stable across background
refetches while still advancing once per METAR cycle. Runway selection compares
METAR true wind against OpenAIP trueHeading (no magnetic correction needed) and
honours takeOffOnly/landingOnly ends.

The synthesised broadcast leaves the METAR groups coded, because
normalizeAtisForSpeech() already expands them for TTS.

Also fixes runway extraction for "RUNWAY IN USE 22" — the phrasing German
VATSIM ATIS actually uses, which the previous pattern missed entirely.

Tests run offline against VATSIM/METAR/OpenAIP responses recorded 2026-07-26,
including the EDDC case where the wind-derived runway matches what the live
controller published.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 19:32:59 +02:00
itsrubberduck
a9f6e6df42 fix(clearance): generate octal squawks and skip reserved codes
genSquawk() drew a decimal number in 1000-8999, so clearances could contain
the digits 8 or 9 — codes no transponder can dial. Replace it with a shared
generateSquawk() that draws four octal digits and re-rolls the reserved codes
(7500/7600/7700, 7000, 2000, 1200, 0000).

shared/learn/scenario.ts had its own octal generator that could still draw an
emergency code; it now uses the same helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 19:27:21 +02:00
itsrubberduck
c828ffe221 Route Live ATC entry through Bridge 2026-07-23 08:31:41 +02:00
itsrubberduck
db74c48b95 Make classroom intro skip action more visible 2026-07-22 21:41:21 +02:00
itsrubberduck
1dd228b5d3 feat(live-atc): route TTS through local bridge with cloud fallback 2026-07-22 20:32:04 +02:00
itsrubberduck
db984c22bb feat(live-atc): route STT through local bridge with cloud fallback 2026-07-22 20:30:24 +02:00
itsrubberduck
0a51fe68ab feat(live-atc): local speech bridge discovery composable 2026-07-22 20:27:43 +02:00
itsrubberduck
990e6059eb bridge connct auferäumt 2026-07-22 17:02:57 +02:00
itsrubberduck
4d8b99ec80 feat(ui): declutter start, classroom header, live-atc & bridge
- start: simplify mode cards — whole card is one click target,
  badges become inline labels, equipment chips become a single
  muted footer line
- classroom: replace back/switch-mode + brand buttons with the
  Classroom↔Live ATC experience dropdown
- live-atc (CockpitShell): drop logo + "OpenSquawk" wordmark next to
  the dropdown; remove the flashy Report-issue coach animation & arrow
- bridge: cut the "How do you want to train?" cards, lead straight
  into a clean header + OS picker; simplify the linking/requirements
  section and drop redundant per-card Alpha badges

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 16:58:14 +02:00
itsrubberduck
9657d38aca feat(classroom): first-run nudge to continue after a readback
After the very first readback is checked, highlight the footer's primary
button (pulse + "Readback checked — continue here" bubble) so learners
realise they advance from there. Shown once, then dismissed permanently
via localStorage on the first advance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:16:29 +02:00
itsrubberduck
38226d3afb feat(ui): clearer onboarding across start, classroom, live-atc & bridge
- index: replace header GitHub link with a "Sign up / Log in" primary button
- start: number the modes (1. Classroom / 2. Live ATC), add headset/mic
  equipment chips, and unlock Live ATC immediately for new accounts
- classroom: replace brand logo + experience dropdown with a clear
  "Back / Switch mode" link back to /start; remove the mic/STT readback
  input so the readback is text-only again
- live-atc: first-run coach mark pointing at the "Report issue" button
- bridge: add a "How do you want to train?" split — flight sim via the
  Bridge vs. a browser-only web version (dry run, no live sim link)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 08:47:21 +02:00
itsrubberduck
6dbd9c3ef4 fix(classroom): force English (US) voice for browser TTS default
When no fixed instructor voice is chosen ("Standard · Ryan US"), the
Web Speech path set no voice and fell back to the OS default (German on
a German system). Now prefers an en-US (then en) voice, matching the
online instructor default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 19:42:35 +02:00
itsrubberduck
3cd07e4331 feat(live-atc): manual flight entry on setup screen
Adds a "Manual flight" card alongside VATSIM and demo so a flight can
be entered by hand (callsign, dep, arr, optional aircraft/altitude)
without a VATSIM account. Feeds into the same scenario picker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 19:37:53 +02:00
itsrubberduck
4f367a67aa fix(tts): back to fast Piper controllers, classroom voice setting
Kokoro-82M generates several times slower than Piper — since the controller
pool moved onto it, every ATC reply arrived seconds late. Controllers return
to distinct Piper speakers with the standard US voice (ryan) as the product
default, so replies are fast again and the default sounds like before.

The classroom gets an instructor-voice setting: standard US voice by default,
'random per module' (stable instructor per module), or one of ten named
US/GB voices.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 12:36:19 +02:00
itsrubberduck
f03355daff feat(tts): curated Kokoro+Piper radio voices, classroom instructor from pool
Controllers now speak with the four Kokoro speakers (bm_george, bf_emma,
am_michael, af_heart — heard most, best quality), pilots with six distinct
Piper speakers (alan, alba, joe, amy, bryce, kristin); ATIS moves to the
dedicated ljspeech broadcast voice. The classroom instructor draws a stable
controller-pool voice per module when the user hasn't configured one, and the
TTS cache key follows the effective voice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:50:30 +02:00
itsrubberduck
1272d45bf9 feat(tts): dedicated ATIS broadcast voice on Speaches
The ATIS tag now resolves to en_GB-alba-medium — outside both voice pools —
instead of sharing 'verse' with the user's own readback voice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:22:44 +02:00
itsrubberduck
a60942bfd8 fix(voice-pool): avalanche hash bits before pool modulo
fnv1a's low bits barely mix: for the live-atc persona keys (shared session
prefix, ':TYPE:freq' suffix) hash % 4 was identical for Delivery, Ground and
Tower in 100% of sessions — every position spoke with one voice. A murmur3
fmix32 finalizer before the modulo restores uniform assignment; verified in
the browser (three stations, three voices, three paces).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:09:38 +02:00
itsrubberduck
04eb2f18ff feat(live-atc): squelch open/close transients around transmissions
Carrier-open click at speech start, ~230ms noise tail cut off by a closing
click at speech end. Playback holds the effect chain alive past the voice so
the tail rings out instead of being killed with the noise generators.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 10:49:56 +02:00
itsrubberduck
3d5a0a4e59 feat(live-atc): per-position controller personas with voice and pace
Each ATC position (session + airport + type + frequency) now gets a stable
persona: a controller-pool voice and a base pace of 1.1-1.3x, jittered +/-0.05
per transmission. Anything spoken without an explicit voice — the controller
reply path and AI-traffic ATC lines — uses the persona; pilot voices are
untouched. A new session reshuffles the shift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 10:48:19 +02:00
itsrubberduck
b359670d47 feat(tts): map logical pool voices to distinct Piper speakers on Speaches
Until now the Speaches branch sent every request to the single env-configured
model (de_DE-thorsten — a German voice for English radio), so the voice pool
had no audible effect. A server-side registry now resolves the logical voice
ids to disjoint controller/pilot Piper speakers; unknown ids keep the env
fallback and the cache key follows the resolved pair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 10:45:49 +02:00
itsrubberduck
b87884a608 feat: add complete SEO metadata and indexing controls 2026-07-18 21:29:31 +02:00
itsrubberduck
db1155c8c9 feat(live-atc): forward aircraft position, un-throttle bridge polling
- NormalizedTelemetry carries lat/lon (0/0 no-GPS guard, position in the
  change-detection signature) so the backend can derive distance triggers
- bridge polling moves from setInterval to a Worker tick: with the sim in the
  foreground the /live-atc tab is backgrounded and Chrome throttles plain
  intervals to ~1/min — telemetry-driven ATC lagged behind the silence fallback

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:48:09 +02:00
itsrubberduck
cadc0aefb6 fix(websim): keep bridge connected in background tabs, fix PFD clipping and layout
- Move physics and telemetry/status ticking from requestAnimationFrame/setInterval
  to a Web Worker (new shared/utils/intervalWorker.ts) — the intended usage is
  WebSim in one tab and /live-atc in another, so the WebSim tab is almost always
  backgrounded, and rAF stops entirely / setInterval throttles to ~1/min in that
  state, dropping the bridge connection after a few minutes.
- Surface bridge failures in the header badge (connecting/error/connected) instead
  of showing "verbindet…" forever when telemetry POSTs are failing.
- Replace the PFD's hardcoded 0.85 scale with a ResizeObserver-driven fit so it
  no longer clips the VS tape/altitude readout at typical panel widths.
- Give the sidestick pad the same panel shell as the FCU/radio panels and stretch
  all three to equal height.
- ExteriorView: camera height now reacts to altitude, the runway renders at its
  real distance/lateral offset (was a fixed spot only shown under 3 NM) with
  centerline/threshold markings, widened for visibility from the 12 NM range
  it's now shown from, plus fog and a gradient sky.

Verified live: bridge badge stays green, PFD fits inside its panel at 1280x720,
and the runway becomes visible on approach for all four spawn presets.
2026-07-16 23:48:18 +02:00
itsrubberduck
c73fefe409 fix(live-atc): stop ATC from repeating/looping and hanging on silent frequencies
- Neutralize the local engine's autonomous auto-advance (evaluateAutoTransitions/
  evaluateSimpleAutoFlow) — the Python backend now drives state exclusively via
  moveToSilent; the old walker could self-answer pilot states and race the
  backend, producing loops.
- Dedupe applyBackendDecision's ATC log entry + TTS: moveToSilent no longer logs
  say_tpl for backend-driven auto-advanced states (suppressSay), and a
  lastAppliedSay guard drops a repeated decision from overlapping sources
  (transmit reply, telemetry tick, silence timeout).
- Cap consecutive silence-timeout re-fires on the same state at 2 instead of
  re-arming forever.
- Pause telemetry forwarding while a pilot transmission is in flight, and guard
  against overlapping telemetry POSTs.
- Add request timeouts to TTS (20s) and backend transmit/telemetry/timeout
  (30s)/createSession (60s) calls so a hung request can no longer freeze the
  whole session.

Verified live against the Python backend: a full clearance→taxi chain now logs
each ATC line exactly once instead of 2-3x.
2026-07-16 23:47:52 +02:00
itsrubberduck
3475e6818d fix(websim): authenticate bridge connection in dev 2026-07-16 19:05:59 +02:00
itsrubberduck
3172516918 feat(websim): browser A320 cockpit to test /live-atc without MSFS (WIP)
Flight model (ground/air physics, SELECTED/NAV/APPR/AUTOLAND autopilot with
STAR sequencing and ILS capture), bridge client that feeds the existing
/api/bridge/* endpoints so /live-atc can't tell it apart from a real bridge,
and the cockpit UI (PFD reuse, FCU, radio panel, Leaflet ND, three.js
exterior, spawn presets at EDDF/EDDS). Design doc:
docs/plans/2026-07-16-websim-design.md.

Also adds a local-dev-only auto-login (/dev-login, server/api/dev/login.post.ts)
that bypasses the invite-only login and MongoDB entirely via a fixed in-memory
user, so require-auth pages are reachable for local testing even when the dev
DB is unreachable. Hard-disabled outside development.

Status: unit tests green (yarn test) and typecheck clean (yarn typecheck).
Browser walkthrough of the actual cockpit (flying a preset, confirming
telemetry reaches /live-atc) is not yet done — picking up from a fresh dev
server + /dev-login?redirect=/flightlab/websim confirmed the spawn screen
renders past auth, but full instrument/map/exterior verification is still
outstanding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 18:49:31 +02:00
itsrubberduck
672ac18ac7 fix(sim-control): narrow SimControlParseResult via type predicates
3fe0ea5 left `nuxt typecheck` failing on main:

  useLiveAtcSession.ts: Property 'reason' does not exist on type
  'SimControlParseResult'.

Cause: the union is discriminated by a boolean, and the project builds
with `strict: false` (nuxt.config.ts). With strictNullChecks off,
TypeScript does not treat `true`/`false` literal types as discriminants,
so `if (r.matched) … else r.reason` never narrows. Nothing about the
sim-control types themselves is wrong — the same three lines with any
boolean-discriminated union fail identically.

Adds isSimControlMatch/isSimControlRejection type predicates, which narrow
regardless of strictNullChecks, and routes the one caller through them.
Turning on strictNullChecks would fix it at the root but is a repo-wide
change, not a bug fix.

The pre-push hook was correctly refusing to push this; origin/main is
clean, so the breakage never escaped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 17:51:09 +02:00
itsrubberduck
7d49f18cc6 feat(live-atc): add simulated AI background traffic on the tuned frequency
Implements the ai-traffic roadmap item per
docs/plans/2026-07-14-ai-traffic-architecture-design.md.

Simulated other aircraft on the user's frequency — callsigns, ATC
instructions, readbacks in their own stable voice, handovers — as pure
scenery. It never touches radioBackend: the Python backend keeps owning
the dialogue *with* the user, useAiTraffic owns the radio *around* the
user. The two share only the speech queue (arbitration) and the log.

Rules live as pure, seeded, framework-free modules under
shared/utils/aiTraffic/ so they run in tsx --test without a browser:
callsign collision rules, wake/in-trail separation, runway slots, the
speed ladder, direct validation, the §3 decision table, and the gating
chain. app/composables/useAiTraffic.ts wires them to Vue (1 Hz tick,
spawner, scheduler).

Gating is evaluated twice — before enqueue and again at playback, since
seconds pass in between. Traffic never keys up while the user holds PTT,
while their transmission is out at the backend, or inside the fresh
readback window. Off by default; the toggle surfaces the feature's v1
limitations rather than burying them in a doc.

Zero LLM calls: variance comes from seeded RNG over template variants.

Deviations from the design, both documented in the design doc:
- Adds SimAircraft.quietUntilSec. The design's rule table says "first
  matching row per tick" but never says an instruction must be allowed to
  take effect before the next one. Without it the planner re-derives the
  same unresolved condition every second and nags one aircraft with the
  same vector: 624 calls/30min measured, vs 90 with the cooldown.
- Airline pool limited to the 14 designators DEFAULT_AIRLINE_TELEPHONY
  already knows; UAE/AUA/WZZ from the design would be spelled out letter
  by letter instead of spoken as airline names.

Verified: 406 tests pass (176 new), no new typecheck errors, /live-atc
compiles and serves. The manual in-session walkthrough (audible traffic,
toggle mid-session) is NOT verified — it needs a login and the Python
backend. The 30-minute deterministic integration run stands in for it and
caught two of the three bugs found during development.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 17:46:52 +02:00
itsrubberduck
3fe0ea5f8a feat(sim-control): wire frequency-sim-control command channel end-to-end
Implements the open items from docs/plans/2026-07-14-frequency-sim-control-design.md
§4/§"Offen für die Implementierungsphase": a per-bridge-token in-memory command
queue piggybacked on the existing telemetry channel, plus the client-side gate
and TTS confirmations.

- server/utils/simControlQueue.ts: TTL-based queue keyed by bridge token
  (enqueue → drainPending → resolve → drainResultsForClient).
- server/api/bridge/data.post.ts: response gains a `commands` field the
  bridge drains on its next telemetry POST.
- server/api/bridge/command.post.ts (new): client enqueues a parsed command,
  re-validated server-side via isValidSimControlCommand.
- server/api/bridge/command-result.post.ts (new): bridge reports ok/failed.
- server/api/bridge/live.get.ts: response gains `commandResults` so the
  client can announce outcomes.
- shared/utils/simControl.ts: wire types, isValidSimControlCommand, and
  simControlRejectionSpeech/simControlResultSpeech TTS phrasing.
- useLiveAtcSession.ts: parseSimControl() gated on bridgeConnected, wired in
  right after the local special cases and before the frequency check —
  matched commands never reach radioBackend.transmit().
- useSimBridgeSync.ts / live-atc.vue: bridgeToken threaded through, command
  results forwarded from the telemetry poll to TTS.

43 new tests (shared parser/validation/speech + server queue lifecycle/TTL).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 09:31:17 +02:00
itsrubberduck
1048b1dbbc feat(sim-control): add fail-closed intent parser for frequency-driven sim setup
Roadmap item "frequency-sim-control": pilot speaks a self-service
sim setup command on frequency ("set me up for an approach from 5000
ft to EDDF 07R", "change my altitude to 8000"). Rule-based, not an
LLM call per transmission — a misparsed command changes the user's
real sim state, and it must never be confused with real ATC
phraseology routed through sttMatch.ts.

parseSimControl() gates on explicit self-service anchors first
("set me up", "put me", "change/set my <param>"); anything without
one of those anchors returns no_intent, so regular readbacks and
clearances can never be misrouted. Matched intents still refuse on
any ambiguous slot (missing unit, out-of-range, invalid runway) with
a typed reason instead of guessing.

Parameter vocabulary follows NormalizedTelemetry (altitude_ft,
ias_kts, heading_deg) since the command travels toward the bridge/sim,
not the decision engine's DecisionNodeAutoTrigger vocabulary.

Design doc covers the still-missing server→bridge write channel
(piggyback on the existing telemetry POST response) — not
implemented yet, this commit is the parser only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 17:47:21 +02:00
itsrubberduck
e4859c418e fix(stt): anchor callsign digits verbatim, fuzzy only the airline name
The whole-string fuzzy fallback in callsignMatches() ran before the
digit-anchored check and let single-digit typos slip through (e.g.
"Lufthansa 350" matched expected "DLH359" at Levenshtein distance 1).
Spoken-digit-word candidates ("three five niner") bypassed the digit
anchor entirely since they contain no digit characters pre-fold.

Fold candidates through denormalizeSpokenAtc before checking for
digits, then require the flight number verbatim (word-boundary regex,
zero tolerance) while keeping the existing ~25% fuzzy tolerance on the
airline-name portion. Single-digit flight numbers (DLH4) now get the
same anchoring as longer ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 17:47:11 +02:00
itsrubberduck
46224dd70d chore(live-atc): add dev-only, opt-in API mock for driving /live-atc offline
Verification aid, not a product feature. Guarded by import.meta.dev (stripped
from prod builds) and opt-in via `?mock=1` (`?mock=0` disables). It wraps
globalThis.$fetch and stubs only the Nuxt-side endpoints that would otherwise
need a login, VATSIM, or OpenAI — auth (/api/auth/me, refresh), airport
frequencies, METAR, TTS (silent), STT — and passes everything else through,
including the authoritative Python radio backend on :8000, which is expected to
run for real.

Used to run the full real /live-atc page end-to-end against the real decision
engine: setup → scenario picker → cockpit → text transmit → real controller
reply in the log → readback → session_complete → completion screen. Confirms
the redesign extraction holds on the real page, not just in component harnesses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 09:55:19 +02:00
itsrubberduck
2d2b4925e3 milstone 5 2026-07-10 17:35:44 +02:00
itsrubberduck
918b2e1c12 feat(live-atc): add CockpitShell — HUD header + body layout
Moves the monitor screen's HUD header, body layout (main column + desktop log
rail), bottom nav and their ~450 lines of scoped CSS out of the page and into
CockpitShell.vue, which composes the cockpit components added in Tasks 17-20.
live-atc.vue drops from 1714 to 744 lines.

The shell owns only presentational state (active tab, HUD menu open/close).
inputMode stays on the page as a v-model because a page watcher drives the
pre-recording buffer off it. debugMode and useBugReport also stay on the page,
so the sheets, dialogs and the debugMode-gated DebugPanel are passed back in
through an #overlays slot — that keeps them inside .learn-theme, whose CSS
variables their styles depend on, without handing the shell ownership of them.

The experience-menu rules stay :global(): Vuetify teleports that menu body to
<body>, out of reach of scoped styles.

Verified by rendering the shell on a throwaway route with fake data — the open
question from the last session was that the reworked HUD had never actually
been seen, since it sits behind the auth guard. It renders: the amber
"Report issue" pill is the visually heaviest button in the cluster
(rgba(245,158,11,.18) fill, #fcd34d text) and collapses to an amber icon at
mobile width. The moved media queries survived the move — the log rail shows
and the bottom nav hides at desktop width, and they swap at 375px, where the
Log tab switches the main column over. All shell emits fire and the inputMode,
pilotInput and manual-frequency v-models round-trip. No Vue warnings.

Typecheck clean, 167/167 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:56:19 +02:00
itsrubberduck
9f45e49f5a feat(live-atc): add TextInputPanel, LastTransmissionCard, CommLogRail
Moves the last three inline cockpit blocks out of the page into components,
re-skinned onto the navy/instrument-panel language the rest of the cockpit
already uses (the v-card bg-white/5 glass treatment is gone).

LastTransmissionCard collapses the old four-button row (Mark as faulty / Edit
issue / Reset / Delete) to two icon buttons, per the design doc. Nothing is
lost: "Reset" was a duplicate of the issue dialog's own "Remove flag" action,
so the amber flag icon opens that dialog and a plain × dismisses the card.
resetLastTransmissionFault therefore has no caller left on the page.

CommLogRail wraps the readback-check block plus CommLog for the desktop rail;
the .pm-readback-* styles move with it. The mobile Log tab keeps using
CommLog directly, as the design doc specifies.

Verified by rendering all three on a throwaway route with fake data: the
suggestion chip fills the text field through v-model, the flag button flips
the card into its faulty skin, and the readback rows render matched/missing.
Typecheck clean, 167/167 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:48:45 +02:00
itsrubberduck
39a3650b4e refactor(live-atc): extract FlightInfoSheet, SettingsSheet, BugReportDialog, DebugPanel
DebugPanel now owns the FAB, the drawer, and useDebugSimulation, and the page
mounts it behind `v-if="debugMode"` — so on the normal cockpit path the whole
simulation machinery is never constructed. That required moving `debugMode`
out of useDebugSimulation and into the page: a flag cannot both live inside the
thing it gates and be the gate. The settings sheet is the one place it is
toggled. runFullSimulation's `debugMode = true` line went with it; nothing
called that function, so the write was already unreachable.

BugReportDialog takes the useBugReport handle as a single prop rather than a
dozen individual ones. The composable stays owned by the page because the HUD's
"Report issue" button starts the screenshot capture before the dialog exists.

SettingsSheet carries its own copy of the .pm-seg segmented-control styles and
routes the theme choice back through the page, so usePmTheme.setPreference
still persists it to the cookie.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:35:34 +02:00
itsrubberduck
e836edd108 feat(live-atc): dock the expected-comm hint onto a round, glowing PTT pad
The PTT pad is the control the pilot uses most, but it read as one card among
several. It is now a circular transmit key that lights up cyan at rest and
green while transmitting, with the active frequency called out underneath.

Expected Communication shrinks from a full card to a two-line strip (ATC line,
pilot line) sharing a frame with the pad, so it reads as guidance for the next
transmission rather than as general reading material. The strip still renders
in text mode when the learning aid has something to say, and disappears with
the learningMode toggle as before.

The pilot line's icon was `mdi-account-pilot`, which is not an MDI icon and
rendered as an empty glyph; it is now `mdi-headset`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:26:30 +02:00
itsrubberduck
4a143de4a8 feat(live-atc): put ACTIVE left of STANDBY, lighten the HUD action cluster
Real avionics (Garmin GTN and friends) place the active frequency window to
the left of standby; the panel now matches, so muscle memory transfers.

In the HUD, "Report issue" is the tool pilots reach for most often during a
session, so it gets a filled amber pill with icon + label while Settings drops
to an icon-only gear. The divider moves down to sit directly above Logout,
grouping the three session tools (report / settings / help) together.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:20:57 +02:00
itsrubberduck
ab9b52ad4c feat(live-atc): add RadioPanel as the cockpit's primary frequency control
Moves frequency handling out of the HUD header — where it lived as two
compact chips behind a dropdown — into a first-class module at the top of
the main column, styled like a physical radio stack: STANDBY and ACTIVE
windows flanking a circular swap button, the published-channel row below,
and the readability control in the panel's own corner rather than as a
separate global chip.

Behaviour is unchanged: HoldSelect stays the interaction primitive for
preset selection and manual free-tune entry, and swap/preset/readability
all route to the same useFrequencyPresets and useRadioSpeech handlers.
Tapping a channel chip stages it in standby (setStandbyFrequencyFromList),
matching the old preset behaviour.

The header's hud-center wrapper and the freq-chip/freq-stack/signal-chip
styles it carried are removed; the freq-option/freq-manual menu styles
move into the panel. The active frequency scales down via clamp() so
seven monospace digits cannot overflow the window at 375px.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:13:27 +02:00
itsrubberduck
8159bff0a5 refactor(live-atc): extract setup steps to components, apply navy/radar skin
Moves the four non-monitor screens out of live-atc.vue into
FlightSourceStep, FlightSelectStep, ScenarioPickerStep and
SessionCompleteStep, and replaces the bg-white/5 glassmorphism cards with
the navy/glow treatment from the design doc.

The card skin lives in one shared SetupCard so the four steps cannot
drift apart and the light-theme overrides stay in one place; it renders
as a real <button> when interactive, which the flight-plan list needs.
RadarBackdrop is mounted once around the setup container rather than per
screen. The scenario picker keeps its phase-chain structure verbatim —
only the card treatment changed — and its .pm-flow styles move with it.

Instrument readouts (callsign, route, flight level, squawk) now render in
font-mono per the design doc's convention.

Verified all four screens render in light and dark on a throwaway route,
since the real screens sit behind an auth guard and MongoDB is not
running locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:58:01 +02:00
itsrubberduck
1d6490d4d9 refactor(live-atc): extract session controller to useLiveAtcSession
Moves applyBackendDecision, handlePilotTransmission, the silence
auto-advance timer, startMonitoring, loadFlightPlans, the screen
transitions (startDemoFlight/launchScenario/flyAgain/backToSetup),
sendPilotText and restoreBugReportState out of live-atc.vue.

The dependency is genuinely circular: the controller drives speech, PTT
and the bridge, while all three need to call back into it. It is
constructed last, and the page hands the other composables two thin
forwarding closures that resolve `session` at call time. The engine,
frequency and speech destructures in the page shrink accordingly —
most of what they exposed is now only read inside the controller.

Also drops normalizeExpectedText, which had no callers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:51:11 +02:00
itsrubberduck
71d148e156 refactor(live-atc): move scenario catalogue to shared/constants/scenarios.ts
SCENARIOS, CHAIN_DEFS and the Scenario type are pure data with no
reactive inputs, but the session controller needs them — leaving them in
the page would mean threading the catalogue through as a dependency.

drillScenarios/chainGroups were computed() over module constants only, so
they can never invalidate; they become plain DRILL_SCENARIOS/CHAIN_GROUPS
consts, evaluated once at module load. Verified the derived shape is
unchanged: 15 scenarios, 2 drills, 3 chains with 4/4/3 segments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:45:11 +02:00
itsrubberduck
1398433cce refactor(live-atc): extract session state to useSessionState
First half of the session extraction: the screen/loading/error refs, the
backend session identity (backendSessionId, lastControllerSay,
backendExpectedPhrase, readback report) and the last-transmission strip
with its fault-reporting dialog.

These are pulled out ahead of the controller because the dependency runs
both ways: useAtisPlayback needs currentScreen, usePttRecording needs
backendSessionId and useRadioSpeech needs setLastTransmission, while the
controller needs all three composables to already exist. State first,
controller last.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:43:42 +02:00
itsrubberduck
33d7aec1dd refactor(live-atc): extract debug simulation and trace inspectors to useDebugSimulation
Moves debugMode, the scripted end-to-end simulation (simulationPilotSteps,
simulationDecisions, runFullSimulation and its helpers), the decision-trace
computeds the debug drawer renders, and debugState/debugNextStates out of
live-atc.vue.

The page keeps only the destructured bindings its debug template still
reads; once DebugPanel.vue exists it constructs this composable itself and
the page stops touching it entirely.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:37:20 +02:00
itsrubberduck
7a1f75152a refactor(live-atc): drop unreachable decision-trace normalization
normalizeDecisionTraceResult had no callers — it was the only root of
ensureTraceCalls, normalizeTraceFallback/AutoSelection, the whole
normalizeTimeline* chain, VALID_TRACE_STAGES and isPlainObject, so that
entire block was unreachable. Same for activeFlowInfo.

Leftovers from the LLM decision routing that moved to the Python backend
(see CLAUDE.md): the trace the debug panel renders now comes straight
from the engine's lastDecisionTrace, already shaped. cloneForTrace stays
— runFullSimulation still uses it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:34:50 +02:00