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>
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>
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>
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>
- 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>
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>
- 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>
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>
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>
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>
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>
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>
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>
- 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>
- 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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Moves the bridge-token poller, COM1 active/standby mirroring, telemetry
forwarding to the Python backend, and the remote-PTT WebSocket out of
live-atc.vue.
The bridge's own onMounted/watch(bridgeToken)/onUnmounted wiring moves
with it, so stopBridgeSync/disconnectPttSocket drop out of the page's
onUnmounted; the remaining teardowns there (ATIS loop, pre-rec capture,
airport refresh, silence timer) still belong to other composables.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Moves the screenshot capture, arrow-annotation canvas handling and the
submit call out of live-atc.vue. The state snapshot it posts is read off
the engine directly; the scenario is passed in.
restoreBugReportState stays in the page: it is session orchestration
(it calls startMonitoring and patches the engine), not part of the
report dialog, and it will move with the rest of the session logic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Moves the audio-context/Pizzicato setup, TTS fetch+playback pipeline,
controller/readback speech scheduling, and the radio-check handling
(RADIO_CHECK_RE/SAY_AGAIN_RE/FAREWELL_RE, answerRadioCheck,
performRadioCheck, readability quick-select) out of live-atc.vue.
The interrupt primitives (stopCurrentSpeech/enqueueSpeech and the
audio/abort bookkeeping they tear down) live in a separate
useSpeechInterrupt because useFrequencyPresets needs stopCurrentSpeech
while useRadioSpeech needs frequency state — one composable owning both
would be circular. useSpeechInterrupt is constructed first and threaded
into both.
signalStrength/speechSpeed/radioCheckLoading/radioEffectsEnabled/
readbackEnabled stay page-level refs and are passed in, because
useAtisPlayback already consumes signalStrength before useRadioSpeech
can be constructed (it needs handlePilotTransmission).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Moves mic access, MediaRecorder handling, the pre-recording rolling
PCM buffer, and Whisper transmission processing out of live-atc.vue.
Keeps the internal AudioContext (prerecCtx) properly encapsulated —
the two external call sites that used to reach into it directly
(the lead-in-duration watcher and the SimBridge remote-PTT handler)
now go through small exposed functions instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Moves ATIS carrier/broadcast loop, TTS audio caching, METAR fallback,
and the background airport-data refresh scheduler out of live-atc.vue.
Also extracts the pmLog debug logger to shared/utils/pmLog.ts since it
has no Vue/component dependency and multiple composables need it —
threading it through every composable's parameter list would've been
worse than giving it one shared home. Verified via typecheck plus a
browser mount/redirect smoke test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Moves radio frequency state, airport-frequency fetching, manual tuning,
swap, and hold-select presets out of live-atc.vue into a composable.
atisVariantLabel and FREQ_ROLE_LABEL also moved here since both the
frequency-preset labeling and the (still-inline) ATIS/radio-speech code
need them — avoids a circular dependency once those move out in later
tasks. Verified via typecheck plus a browser mount/redirect smoke test
(no component test harness exists in this repo).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extracted from start.vue's inline .start-bg__* treatment so /live-atc's
setup screens (Milestone 3) can reuse the same navy/radar/glow look
instead of duplicating the gradients/keyframes a third time.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>