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>
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>
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>
Sync both COM_ACTIVE_FREQUENCY and COM_STANDBY_FREQUENCY from bridge
telemetry into the radio, each tracked independently so they only retune
on an actual sim change. Active retune still cuts in-progress ATC speech;
standby has no audio side effects.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When /pm is opened with ?token=<bridge-token>, poll /api/bridge/live for
fresh telemetry. While the bridge keeps posting, mirror the sim's COM1
active frequency into the radio (only on actual sim change, so manual/flow
tuning isn't clobbered) and show a "Bridge connected" badge in the HUD.
Telemetry now carries COM_ACTIVE_FREQUENCY/COM_STANDBY_FREQUENCY from the
bridge's com_active_frequency/com_standby_frequency fields.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Strip the overloaded two-step grid, status cards and elaborate success
overlay down to a single centered card with clear states: ask for code,
prompt login, link (auto), and a state-aware success view. Keep the
background photo (more visible) and the server log as a collapsible panel.
Attempt to auto-close the tab once linked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Explain plainly that testers drag an arrow to the broken spot (mouse or finger),
add an on-image hint until the first arrow is drawn, and switch the annotation
canvas to pointer events so touch dragging works as the instructions promise.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Screenshots: html2canvas 1.4.1 only parses rgb/hsl and throws on color-mix(),
which the app uses app-wide; the throw was swallowed so no screenshot was ever
captured. Swap to modern-screenshot (native SVG foreignObject — supports
color-mix/oklch), capture before opening the dialog, surface failures instead of
hiding them, and show a capture spinner on the Bug button.
State restore: the old handler only patched local vars/flags and loaded the tree
— it never entered the monitor screen or created a backend session, so nothing
happened. New restoreBugReportState() reuses startMonitoring() to spin up a real
session for the same flight/scenario from the snapshot, overlays saved
variables/flags, and replays the captured comm log. The Python backend has no
mid-session resume, so the session restarts at the flow start and the captured
state id is surfaced in a banner.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- New BugReport MongoDB model (comment, contact, userId, screenshot, pmState, status)
- POST /api/bug-reports — authenticated submit; emails emanuel@faktorxmensch.com on receipt
- GET/PATCH /api/admin/bug-reports + /[id] — admin list, detail with screenshot, status toggle
- /pm: "Bug" button in HUD captures viewport screenshot (html2canvas), shows annotation
canvas where testers can draw arrows; submits comment + contact + state snapshot
- /admin: new "Bug Reports" tab with open-count badge, screenshot expand, "Erledigt" toggle,
and "In /pm öffnen" link that restores captured engine state via ?restoreBugReport=<id>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
backendVariables was a departure-shaped subset, so arrival flows (IFR and VFR)
ignored the selected flight and ran on YAML defaults for runway, QNH, surface
wind, aircraft type, cruise level and assigned squawk — even though the engine
generates them. Forward those (with name mapping qnh_hpa→qnh, acf_type→
aircraft_type, cruise_flight_level→cruise_level). Also fixes taxi/tower on
departure, which use runway/qnh/surface_wind.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reverses the auto-tune: tuning to the first controller is part of the exercise.
Each scenario now starts from a fixed baseline frequency (121.900), and the
first call is rejected until the pilot dials the correct one — we do NOT silently
put them on the expected frequency.
The wrong-frequency reply now names the position and frequency to switch to,
e.g. "DLH6RK, you are on the wrong frequency. Contact Approach on 119.900."
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The initial active frequency was only set via a hardcoded EDDF special-case
(121.900); every other airport — and any arrival that begins on Center/Approach
rather than Delivery — kept a stale/default active frequency. That triggered an
immediate "check frequency" on the pilot's very first call (e.g. IFR enroute
start: on 121.900 but Center expects 121.800).
Now the radio is tuned to expectedFrequencyForState() for the start position
when the session begins, so the first call is always on the right frequency and
subsequent handoffs change to the correct next-position frequency.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reworked the scenario picker so it's clear which single-phase practice belongs
to which chain. Scenarios are grouped by Departure / Arrival; each complete
chain renders as a left-to-right flow of its phases with arrows
(e.g. Clearance → Startup & Taxi → Tower → Departure). Tap any step to practise
just that phase, or "Fly full" to run the whole chain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the IFR arrival to the PM scenario picker:
- Complete chain "IFR Arrival" (Enroute → Approach → Landing → Taxi-in),
starting at ifr-enroute-arrival-v1.
- Individual phases: Enroute Descent, IFR Approach, IFR Landing
(taxi-in already present). All use the arrival airport for frequencies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Whisper prompt seeding (per request):
- ptt.post.ts builds the prompt as generic ICAO bias + this state's expected
readback appended LAST (survives the 224-token truncation), in both raw token
form and spoken ICAO form via new radioSpeech.speakToken().
- pm.vue passes the expected phrase + active variable values; classroom.vue
passes the lesson's expected field values.
Per-field readback debug:
- sttMatch.matchTranscriptionToFields returns fields[] (matched/missing + which
view matched) plus normalized/denormalized transcription views.
- useRadioBackend types readback_report on the transmit response.
- pm.vue renders a "Readback check" panel in the right log rail; classroom.vue
renders per-field rows under the STT panel.
Radio-pronunciation fixes (radioSpeech.ts):
- callsign expander handles multi-letter suffixes (DLH6RK -> Lufthansa six Romeo
Kilo).
- toRadioSpeech now expands airports (EDDC -> Echo Delta Delta Charlie).
- bare altitudes >=1000 in a clearance context are spoken ("climb initially
5000" -> "climb initially five thousand feet"); speeds/headings untouched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The radio backend now expires sessions idle for 5+ hours and may restart
without in-memory state. A 404 on transmit previously left the monitor
screen stuck; now it clears the session, returns to the scenario picker,
and shows a dismissible "session expired" alert there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>