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>
- Remove the Feedback link from /pm's nav entirely
- Theme (dark/light/system) now lives as a segmented control at the
top of the Settings dialog instead of its own nav menu
- Help and Logout become icon-only and move to the far right,
separated by a divider from Bug/Settings
- Fix missing icon for the free-tier/self-host pricing option
- Make selected cards visually obvious (checkmark badge, stronger border)
- Add Back navigation between onboarding steps
- Network-experience screen: button reads "No, not yet" instead of an
ambiguous "Continue" when nothing is selected
- classroom-introduction reverts to Classroom-only content; the
Classroom/Live ATC chooser moves to a new /start page instead
- Remove the "Skip to Classroom" button in favor of a subtle text link
- New /start page: choose Classroom or Live ATC after login (when
there's no specific redirect target) or right after finishing
onboarding. Shows a "start with Classroom" hint only for first-time
arrivals from pilot-profile-setup. Live ATC is locked for 24h after
registration.
The first light-mode pass only covered Tailwind utility classes in the
template. ~40 hand-written classes (.pm-flow-node, .pm-readback-*,
.freq-*, .pm-seg-*, .pm-bottomnav, signal bars) still hardcoded
rgba(255,255,255,N)/#fff, so scenario chips, the readback panel, and
the frequency display were unreadable in light mode. Switched them to
var(--text/--t2/--t3/--border) and color-mix() so they follow the
theme. Debug panel (dev-only) left as-is.
LiveATC is now in alpha, not closed testing, so the intro offers both
paths up front: Classroom (default, dry radio practice) or Live ATC
alpha test connected to the simulator, with an honest disclaimer that
it's not fully reliable yet and we want bug reports.
Defaults to dark (matching the rest of the app) with a menu to switch
to Light or System. Custom CSS-variable overrides handle the
hand-rolled Tailwind classes; a second Vuetify theme is swapped in
sync for Vuetify components. Scoped entirely to /pm.
Single-question-per-screen wizard (cockpit, radio confidence, stress
point, network experience, hangar tools + paid-tool duration, feature
wish capped at 2, funding preference) plus the reward callsign card
and skip handling. Verified end-to-end against a mocked API layer in
the browser (MongoDB Atlas isn't reachable from this sandbox).
Arm a timer whenever the session lands on a pilot state with
auto_advance_on_silence (from the runtime tree); on expiry call the backend
/timeout endpoint and apply the response through applyBackendDecision. Cleared
on pilot transmission, stale-guarded against session/state changes, and
deferred while PTT is recording so ATC never talks over the pilot.
This makes the tower-v1 airborne handoff work without a sim bridge: telemetry
fires it when connected, the silence timeout covers everyone else.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The VATSIM flight plan carries no runway, so the runway in use is now parsed
from live ATIS text (DEP/ARR/EXPECT...APPROACH phrasing preferred, generic
RWY mention as fallback, "7L" normalised to "07L"). Falls back to the
engine-generated runway when no ATIS text mentions one. Local engine vars
are patched so the HUD matches what ATC says.
- The bridge poller now tracks PLANE_LATITUDE/LONGITUDE; when the bridge is
connected, the aircraft position is sent at session creation so the backend
starts the departure taxi route at the real parking position instead of a
random stand.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The backend computes the real OSM taxi route from airport_icao + stand/runway
and falls back to the flow's YAML default on its own. Sending a placeholder
taxi_route counted as a caller override and suppressed that computation, so no
session ever got a computed route. Drop it from the create payload.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Send live bridge telemetry to the backend so it can drive proactive,
aircraft-state-aware ATC. Purely additive — bridge-less sessions are unchanged.
- NormalizedTelemetry contract + sendTelemetry() + telemetry_fired
(useRadioBackend.ts), sim-agnostic and matched to the backend contract
- extract applyBackendDecision() so telemetry- and pilot-driven ATC responses
land through one shared path
- normalizeBridgeTelemetry() + change-detection + forwardTelemetryToBackend()
wired into the existing bridge poll
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surfaces the info-arrival-v1 flow as a standalone drill and gives it a
German D-registration callsign (same as VFR scenarios).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- useRadioBackend.createSession: forward airport_icao / destination_icao so the
backend can compute the real OSM taxi route for taxi flows.
- pm.vue: pass the resolved airport ICAO from startMonitoring, and show a
full-screen overlay ("Calculating taxi route…") while the session is being
created — the backend computes taxi routes synchronously for taxi-only
training, so the create call can take a few seconds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- "say again" / "repeat" → ATC re-speaks its last transmission locally (#8/#40)
- bare sign-off ("thank you", "tschüss", "good day" with no readback/digits)
gets a polite "Good day" instead of being evaluated (#32)
- positive "Readback correct" confirmation prefixed when every required
readback field matched, not only on the IFR clearance (#10)
- a newer pilot transmission supersedes an older one still awaiting its
backend reply; the stale reply is dropped (#16)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Whisper hallucinates short real words ("Test", "Thank you", "Okay") on
near-silent or noisy audio. Unfiltered these reached the backend as a wrong
readback attempt — counting toward the 3x-skip — and triggered paid
LLM-router calls. Drop voice (PTT) transcripts below NUXT_PUBLIC_PTT_MIN_WORDS
(default 2); text input is exempt so deliberate short commands still work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend counterpart to the Python engine's semantic router.
- POST /api/decision/route: service-secret-guarded endpoint the Python
backend calls on regex-miss. Calls gpt-5-mini (ROUTER_LLM_MODEL),
validates the chosen id against the candidate set, and writes both a
UsageEvent (central cost ledger) and a routing-review record — including
timeouts, with timeoutMs + actual latencyMs — so the budget can be tuned
- LlmRoutingDecision model + GET /api/admin/llm-routing (paginated,
status-filtered, per-status counts)
- admin "LLM Routing" tab: transcript vs expected phrase, candidate chips
with the chosen one highlighted, latency/budget chip, model reason
- serviceAuth util (mirrors CRON_SECRET pattern)
- .env.example: ROUTER_LLM_MODEL, SERVICE_SECRET
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First-run overlay explaining how /pm works (tune frequency, push-to-talk,
read back, emergency, bug button), reopenable via a new "?" button in the
HUD. Shows one language with a DE/EN toggle; the choice and "seen" state
persist in localStorage so it auto-opens only on first use.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- VFR scenarios get a German D-registration (e.g. D-EKLM) and its
abbreviated form (callsign_short, D-EKLM -> D-LM) instead of the airline
callsign; the pilot's first call uses the full registration, ATC uses the
short form thereafter.
- radioSpeech: spell aircraft registrations phonetically for TTS
("D-EKLM" -> "Delta Echo Kilo Lima Mike", "D-LM" -> "Delta Lima Mike").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New standalone "Drills" section in the scenario picker, with the
Rejected Take-Off drill (rto-v1). Drills are surfaced separately from
the journey chains via DRILL_IDS / drillScenarios.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wrong-frequency gate now accepts any of the frequencies a logical
position publishes (e.g. EDDM has two Tower frequencies, 118.700 and
120.500); expectedFrequencyForState() still returns the primary for the
"contact X on <freq>" prompt. Falls back to the single expected value
when no frequency list resolves.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From /pm tester bug reports:
- pre-tune COM1 to the opening pilot state's frequency on scenario start,
so the first call isn't met with a "wrong frequency" rejection (#5/#6/#21)
- taxi-route phonetics no longer stop at the first comma: "via A, V" now
speaks "via Alfa, Victor" (#31)
- barge-in: keying the mic stops any ATC speech still playing
- ignore empty / punctuation-only transmissions (silence, stray PTT taps)
- PTT pad turns green while transmitting (was red)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Standby used change-detection like active, so a locally tuned standby
lingered as "the last" channel. While the bridge is connected, standby now
always reflects COM_STANDBY_FREQUENCY from telemetry on every poll. Active
keeps its change-detection anchor so flow/manual tuning isn't overridden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bridge now sends latitude_deg/longitude_deg/heading_deg; map them to
PLANE_LATITUDE/PLANE_LONGITUDE/PLANE_HEADING_DEGREES_TRUE so position and
course land in the telemetry store and surface in /api/bridge/live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unauthenticated /pm visits pushed to /login without a redirect query, so
after sign-in they landed on the classroom fallback instead of returning
to /pm. Pass route.fullPath as ?redirect= so the bridge link (incl.
?token=…) survives the login round-trip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>