Commit Graph

321 Commits

Author SHA1 Message Date
leubeem
fdc8bbd02d fix(pm): tune radio to the starting position's frequency on session start
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>
2026-06-17 15:24:26 +02:00
leubeem
64ecb564a9 feat(pm): group scenario chooser by journey with a phase flow
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>
2026-06-17 15:09:07 +02:00
leubeem
5d7d74f9e0 feat(pm): add IFR arrival scenarios to the chooser
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>
2026-06-17 15:04:10 +02:00
leubeem
b80feb80d6 feat(stt): seed Whisper prompt with expected readback + per-field debug UI
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>
2026-06-17 14:12:54 +02:00
leubeem
453b04881f test: cover the core engine and auth rotation/JWT hardening (TEST-03, TEST-04)
TEST-03 — communicationsEngine had zero tests. Add tests/shared/
communicationsEngine.test.ts exercising the deterministic core: system load &
ready state, VariableDefinition unwrapping, dual {{}}/{} template rendering,
patchVariables, moveToSilent (cursor advance + state actions + controller log),
unknown-state handling, getStateDetails, and normalizeATCText expansion.

TEST-04 — auth utils were tested but rotation and JWT verification were not.
Extend tests/server/auth.test.ts with refresh-token rotation (valid rotate,
missing cookie, access-token-as-refresh, version mismatch) and JWT hardening
(alg-confusion rejection, tampered signature, expired, malformed).

97 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 12:14:59 +02:00
leubeem
6b5e8b9df8 fix(security): mandatory cron secret + reject placeholder JWT secrets (SEC-07, OPS-02, SEC-09)
SEC-07 — committed secrets:
- Replace real-looking defaults in .env.example (JWT_SECRET/JWT_REFRESH_SECRET
  "changeme", MANUAL_INVITE_PASSWORD "pm.local@zghl.de") with CHANGE_ME
  placeholders, and drop the personal DOME_LIGHT_WEBHOOK_URL default.
- Add a Nitro startup plugin (server/plugins/validate-secrets.ts) that refuses
  to boot in production when JWT_SECRET is unset, looks like a placeholder, or
  is shorter than 32 chars (warns only in development).

OPS-02 / SEC-09 — cron endpoints:
- requireCronSecret now fails closed: when no CRON_SECRET/KPI_CRON_SECRET is
  configured the endpoint returns 503 instead of being publicly callable
  (previously it allowed the request with a warning). Both cron routes already
  call the guard. Prefer the x-cron-secret header over the loggable ?secret=
  query param; document CRON_SECRET in .env.example.

Operational note: production deployments must now set JWT_SECRET (>=32 chars)
and CRON_SECRET, or the server won't start / crons return 503.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:07:27 +02:00
leubeem
3ea297f050 ci: make typecheck a real blocking gate; bump actions to Node 24 majors
The previous `vue-tsc --noEmit` step was a no-op: the root tsconfig uses
`files: []` with project references, so without `--build` it checks zero files
and always passes. Switch to `vue-tsc --build` (new `yarn typecheck` script)
and make the job blocking.

Fix the one error this surfaced: UsageEventDocument extended mongoose.Document,
whose `model` method collides with the `model: string` field. Use the
recommended pattern — a plain attrs interface passed to the Schema/Model
generics (hydrated docs still expose Document methods). Typecheck is now clean.

Bump actions/checkout@v5 and actions/setup-node@v5 to silence the Node.js 20
runtime deprecation (forced to Node 24 from 2026-06-16).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:28:27 +02:00
leubeem
f097141e8a feat(atc): bias Whisper STT toward aviation phraseology
Replace the generic one-line Whisper prompt with a vocabulary-rich bias prompt
(phonetic alphabet, aviation number words, common phraseology, and the airline
telephony names pulled live from DEFAULT_AIRLINE_TELEPHONY) and set
temperature: 0. Aviation R/T is a constrained domain, so seeding the expected
vocabulary improves transcription of callsigns, runways, and readbacks — the
upstream input to the decision engine. Stays within the ~224-token prompt cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:47:22 +02:00
leubeem
13dcee52ab ci: fix red radioSpeech assertion and add a test/typecheck pipeline
The suite was red on main: the SID test still expected the old
letter-by-letter spelling ("Mike Alfa Romeo...") after the pronunciation
change to speak named waypoints as words ("Marun seven Foxtrot"). Update the
stale assertion.

Add a GitHub Actions workflow: yarn install + yarn test as the required gate,
plus a non-blocking vue-tsc job (TS strict mode is still off — promote to
required after that cleanup). Suite now 80/80.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:47:09 +02:00
leubeem
10fabe67ba docs: align CLAUDE.md/AGENTS.md/README with the two-repo architecture; drop dead LLM routing
The LLM decision routing moved to the external Python backend
(OpenSquawk-LiveATC-api), but the core docs still described the old in-Nuxt
path (routeDecision(), /api/llm/decide, a Nuxt /api/decision-flows/runtime
route) — none of which exist. Every agent session started with a wrong mental
model.

- Rewrite CLAUDE.md to describe the real /pm flow (useRadioBackend -> Python
  backend owns authoritative state; Nuxt owns STT/TTS/audio/auth/editor) and
  fix stale commands (bun -> yarn).
- Fix the two stale AGENTS.md lines (openai.ts is getOpenAIClient() only;
  decisionFlowService is editor-only, no Nuxt runtime route).
- README: note the Python backend is required for /pm; correct server/ desc.
- Remove dead shared/utils/openaiDecision.ts (called the non-existent
  /api/llm/decide) and the now-orphaned LLM decision contract types in
  shared/types/llm.ts. Trace types used by pm.vue are kept.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:46:57 +02:00
leubeem
7cc1ca1a10 Merge branch 'main' of github.com:OpenSquawk/OpenSquawk 2026-06-15 13:00:57 +02:00
itsrubberduck
43a106f3c8 manual frequencies 2026-06-12 10:14:38 +02:00
leubeem
97d6475208 feat(pm): recover gracefully when the backend session expires
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>
2026-06-10 23:17:14 +02:00
leubeem
7367f7cdb7 feat(server): per-user AI usage tracking, cost alerting, and endpoint hardening
Usage tracking:
- new UsageEvent collection records every STT/TTS/LLM call per user with
  provider, model, volume (audio seconds, characters, tokens) and an
  estimated USD cost; self-hosted providers (Speaches/Piper) and cache
  hits record at $0
- pricing table for whisper-1, tts-1, gpt-5-nano & co. in server/utils/usage.ts
- weekly KPI mail gains an "AI-Nutzung & Kosten" section: weekly and
  rolling 30-day cost, per-kind breakdown, top 5 users by cost
- quota alert mail when rolling 30-day cost exceeds USAGE_ALERT_USD
  (default $5), at most once per calendar month (UsageAlertDelivery)

Hardening:
- /api/atc/say now requires an authenticated session (middleware
  exemption removed); useFlightLabAudio sends the bearer token
- /api/service/tools/latency requires auth (was a public LLM endpoint)
- per-user rate limits: PTT 20/min, say 60/min, latency 5/min
- cron endpoints (waitlist-drip, weekly-kpi-report) require a shared
  secret via ?secret= or x-cron-secret (CRON_SECRET, falls back to
  KPI_CRON_SECRET); allowed with a warning while unset so existing
  deployments keep working
- PTT records the actual transcribed audio duration for billing accuracy

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:17:03 +02:00
leubeem
cab7b36d85 feat(atc): accept preNormalized flag on /api/atc/say
Clients that already ran the full radiotelephony normalizer (see
normalizeATCText) pass preNormalized: true so the server skips
normalizeATC — double-normalizing corrupts the text, e.g. expandAirports
spells the city name "MAIN" letter-by-letter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:12:53 +02:00
leubeem
3c6816c44c feat(pm): live ATIS broadcast loop with METAR-slot refresh and multi-station support
- per-airport ATIS loop keyed by station with a virtual start epoch, so
  re-tuning resumes where the broadcast would be instead of restarting
- refetch airport data at :23/:53 to follow VATSIM ATIS regeneration from
  real-world METAR publication, with faster retries while no ATIS is on
  the feed; prefetch audio when the info letter changes
- support separate arrival/departure ATIS stations on different frequencies
- cancel the deferred audio teardown on retune so a fresh broadcast is not
  killed by the previous stop()'s fade-out timer (atisAudioLoop)
- comm log shows newest entries first

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:10:35 +02:00
leubeem
2a504885b5 fix(radio): speak SID names and waypoints as words, expand more ATIS elements
- SID basenames and 5-letter waypoints (ANEKI 7S, SULUS) are pronounceable
  by design and are now spoken as words instead of letter-by-letter phonetics
- skip acronyms (ATIS, RNAV, MAIN, ...) when spelling 4-letter ICAO codes
- expand stand/gate designators, ATIS information letter, and surface wind
  groups for TTS
- normalizeATCText now runs full client-side radiotelephony expansion
  (callsigns, airports) since preNormalized texts skip the server normalizer

Note: tests/radioSpeech "normalizes SID suffix and METAR data" still expects
the old spelled-out SID behavior and fails until updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:10:22 +02:00
leubeem
3b16b6f1d6 feat: scenario picker, flow chaining UX, reliable frequency checks
Scenario picker & completion:
- Login → scenario selection screen with complete chains + individual phases
- Completion screen with "fly again / try opposite / back to scenarios"
- Scenario.airport ('dep'|'arr') drives which airport frequencies to fetch;
  arrival scenarios (vfr-arrival, circuit-landing, taxi-in) use arr ICAO

Backend session integration:
- createSession forwards no_chain; response carries active_flow + session_complete
- Pass all six airport frequency variables to session so every chained flow
  has the real airport values from creation
- fetchAirportFrequencies now runs before session creation so resolved
  frequencies are included in backendVariables

Wrong-frequency check:
- airportFreqMap computed (from airportFrequencies, always up-to-date)
  used as primary source in expectedFrequencyForState — immune to flow
  snapshot switches
- setActiveFlow called when response.active_flow changes so local engine
  cursor moves to the correct flow's states after a chain
- Wrong-freq ATC reply appended to communication log (offSchema entry)

Engine fixes (communicationsEngine.ts):
- patchVariables / patchFlags: write directly to the internal reactive
  store, bypassing readonly(ref) which silently blocked all (vars as any)
  .value[k] = v mutations
- appendLogEntry: push ATC speech (and wrong-freq replies) into comm log
- ATC controller_say_rendered appended to comm log after every transmission
2026-06-08 13:03:55 +02:00
leubeem
1ae985fdc2 fix: interrupt ATC speech on frequency change, fix PTT stack overflow
- Stop in-progress ATC speech (HTTP request + audio playback) when the
  active frequency changes via preset select or swap button. Prevents the
  pilot "hearing" a controller on a frequency they have already left.
- Fix RangeError crash for long PTT recordings: btoa(String.fromCharCode
  (...largeArray)) overflows the call stack above ~2 s of audio and
  silently drops the PTT request. Replaced with chunked 8 KB conversion.
- Add 30 s PTT auto-stop timer so runaway holds are submitted rather than
  silently lost.
- Add AbortSignal support to useApi so fetch requests can be cancelled.
2026-06-08 10:14:07 +02:00
itsrubberduck
853418ecdb neue tabbed view 2026-05-31 11:18:45 +02:00
itsrubberduck
f22a79ba00 add Prerec mode for pm 2026-05-29 18:50:24 +02:00
itsrubberduck
3bcbb42479 fix(pm): remove max-width from hud-inner
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:22:49 +02:00
itsrubberduck
2ad47259e1 fix failed deployment 2026-05-28 21:04:09 +02:00
itsrubberduck
fe3d0b3c7b neues navbar design 2026-05-28 14:16:42 +02:00
itsrubberduck
d6aa858ff8 feat(atis): inline METAR expansion, airport name lookup, acronym fix
ATIS text often arrives in raw METAR form (e.g.
"METAR EDDF 281050Z AUTO 02008KT 320V070 CAVOK 24/02 Q1025 NOSIG"),
which TTS reads as letter-by-letter spelling. The normalizer now expands
the full WMO Code Form FM 15-XV vocabulary inline: DDHHMMZ date stamps,
compressed wind (with gusts, VRB, calm), wind variability ranges, RVR
(R25L/1500N), wind shear, slash-form temp/dewpoint, Q/A pressure,
NSC/SKC/CLR/NCD/VV cloud codes, weather phenomena (with intensity and
descriptors), recent-weather RE prefix, BECMG/TEMPO/FM/TL/AT trend
codes, and strips RMK remarks. Plus ATIS/METAR/SPECI get lowercased
so TTS pronounces them as words (pilots SPELL ILS/QNH/VOR so those
stay uppercase).

Airport ICAO codes are substituted with their OpenAIP name when the
frequencies endpoint returns one. New `airportName` field added to
the FrequencyResponse for that. Adds 7 test cases covering the user-
reported EDDF sample plus calm/VRB/gust winds, RVR, weather codes,
cloud specials, trend codes, and RMK stripping.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:45:15 +02:00
itsrubberduck
47efee2148 feat(atis): carrier-noise bed and Web-Audio loop seek
Tuning to the ATIS frequency now plays carrier noise immediately so the
pilot gets feedback before the TTS comes back (synthesis takes a moment
on cold cache). When the ATIS audio is ready, the carrier ducks down to
a subtle bed level and stays underneath the announcement — mimicking
how a real radio channel always carries some noise floor.

Switches the loop from HTMLAudioElement (whose seek on data: URLs gets
quantized by some browsers) to a Web-Audio AudioBufferSourceNode.
`source.start(0, offset)` is sample-accurate per spec, so the
virtual-clock entry point lands exactly where computed. `window.__atisDebug`
exposes ctx/source/state for manual inspection, and pm.vue logs the
requestedOffset/duration/epochAge on each loop start.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 09:24:31 +02:00
itsrubberduck
771e22d728 feat(atis): ICAO phraseology normalizer for spoken ATIS
The ATIS loop sent raw VATSIM `text_atis` to TTS, producing "Q-N-H one
thousand twenty-four", "WIND oh thirty degrees", "RUNWAY oh eight L".
New `normalizeAtisForSpeech` applies ATIS-specific transforms — info
letter → phonetic alphabet, wind/temperature/time digit-by-digit, TRL
expansion, NOSIG → "no significant change", cloud layers (BKN030 →
broken three thousand), visibility, bare runway designators — then
hands off to `normalizeRadioPhrase` for QNH/RWY/FL/freq. pm.vue calls
the normalizer before posting to /api/atc/say so the disk cache keys
the spoken form. Adds 4 test cases covering a full real-world EDDM
broadcast plus edge cases (cloud layers, negative temperatures, km/m
visibility).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 09:09:12 +02:00
itsrubberduck
49dd5031bd feat(atis): clock-locked ATIS loop tied to tuned frequency
Real ATIS broadcasts run continuously; tuning in mid-broadcast should
drop the pilot into the current spoken position, then loop. The frontend
now generates the full announcement once via TTS, plays it as a looping
HTMLAudioElement, and seeks to ((Date.now() - lastUpdated) / duration)
on metadata-load so all clients tuned to the same ATIS hear it phase-
synced. The loop starts/stops automatically with frequency tuning and
restarts on info-letter change. say.post.ts now caches tag=atis like
tag=flightlab to avoid re-synthesizing identical announcements.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:43:48 +02:00
itsrubberduck
c958f65b87 Make callsign STT matching far more tolerant
Whisper routinely garbles the airline portion of a callsign while
nailing the flight number ("Loftansa three five niner", "Speed bird
27", "Lufthana 359"). Previously these slipped past the matcher.

Two changes:

1) Whole-string fuzzy distance for callsigns bumped to allowedDistance
   + 3 (was +1), which covers ~1–2 character substitutions in the
   airline name.

2) New `callsignMatches()` splits each candidate into its alphabetic
   airline prefix and trailing digit run and matches each part
   independently:
   - The digits (e.g. "359") are the strong anchor and must appear.
   - The airline portion is matched both verbatim and with whitespace
     stripped ("Speed bird" → "speedbird"), with a generous ~25%
     character-distance allowance.
   - Bare flight number without any airline trigger does NOT match —
     verified by a dedicated false-positive test.

7 new test cases cover the realistic Whisper error modes (misspell,
split words, ICAO letter readout, reordered words, telephony glue).
All 69 tests green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 21:26:54 +02:00
itsrubberduck
e87e6b79ac Make classroom STT production-ready
Last pass fixed the crashes but the UX wasn't trustworthy — you'd hit
the mic, something happened, fields silently changed, and there was no
clear way to see what Whisper actually heard or which fields it touched.
This rebuilds that flow:

UI
- Dedicated transcription panel below the controls row replaces the
  single-line "Heard:" hint. Has explicit states: recording (red,
  pulsing dot, live MM:SS timer), transcribing (spinner), result
  (editable textarea + summary chip), or error (red body text).
- Mic button label shows the elapsed recording time so the pilot knows
  recording is actually running.
- Per-field mic icon appears on every blank that was filled by the
  current transcription, so it's obvious what came from speech vs.
  what was typed.
- Result panel exposes three explicit actions: Apply to fields (re-runs
  the mapping after edits), Record again, Dismiss.
- Hard auto-stop at 45s (well under the server's 2 MB / ~60s cap).
- 503/unreachable responses from the PTT endpoint now flip
  `sttServerAvailable` so the mic button gracefully hides itself.

Matching reliability (shared/utils/sttMatch.ts)
- Process fields longest-expected-first so a 6-char callsign claims its
  substring before a 1-char digit field grabs an overlapping character.
- Short candidates (<3 chars) now require a whole-word boundary match,
  so the digit "5" in callsign "359" no longer auto-fills an unrelated
  readability field.
- Two new test cases cover both false-positive guards.

62 / 62 tests green, vue-tsc clean, dev server starts and serves the
classroom page without TDZ / hydration warnings in the log.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 21:24:41 +02:00
itsrubberduck
1235f7fd2f Fix STT readback — TDZ crash, spoken-form mismatch, hydration
Three bugs in yesterday's STT addition:

1) **TDZ crash** — `sttSupported` referenced `isClient` before its const
   declaration, throwing on setup and breaking the whole classroom page.
   `sttSupported` is now a ref that's populated in `onMounted`.

2) **Spoken vs. written mismatch** — Whisper returns natural ATC speech
   ("runway two five right", "lufthansa three five niner"), but the
   lesson fields hold the canonical written form ("25R", "DLH359"). The
   old `normalized.includes(...)` check never matched. Matching now lives
   in `shared/utils/sttMatch.ts` and searches both the raw normalized
   transcription *and* a denormalized projection that folds spoken
   digits/letters back to written tokens (incl. SID suffix `7S`, runway
   `25R`, scale words `five thousand → 5000`, frequency `decimal` as a
   digit-run boundary).

3) **SSR hydration mismatch** — `sttSupported` evaluated differently on
   server vs. client, causing visible-vs-hidden button divergence on
   hydration. The ref-set-on-mount approach resolves it.

The new helper is fully unit-tested (15 cases covering radio check,
departure clearance, SIDs, squawks, Speedbird telephony, decimal
frequencies and edge cases).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 21:18:39 +02:00
itsrubberduck
df640b0936 Add STT readback input to classroom
Detlef's original "Rückmeldungen sollten gesprochen werden können"
request was about speaking *into* the readback rather than typing every
field — this wires that up using the existing /api/atc/ptt Whisper
endpoint.

- Mic button in the readback control row records via MediaRecorder,
  posts base64 audio to /api/atc/ptt, then maps the transcription onto
  the lesson's input fields via normalized substring matching against
  each field's expected value plus alternatives.
- Callsign-flagged fields additionally allow a sliding-window
  Levenshtein match because Whisper routinely garbles airline names.
- The transcription is shown back to the user so they can verify what
  was heard before pressing Check.
- The button is hidden when the Speaches speech server is configured
  but unreachable — a small "Speech server unavailable" hint takes its
  place. Browsers without MediaRecorder/getUserMedia never see it.
- Recording state animates the button; transcription state shows a
  spinner. Stops cleanly on Reset and on component unmount.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 10:58:38 +02:00
itsrubberduck
acce6ca88c Group readback label and field so they wrap together
Detlef reported that the prompt for a blank (e.g. "runway") often ended
up on the line above the input itself, forcing him to look back up to
remember what he was typing.

Each preceding text segment is now paired with its following input field
into a `.cloze-group`. The group uses `inline-flex; flex-wrap: nowrap`
so its label and input stay on one line, while the outer `.cloze`
container still wraps groups normally. On narrow viewports (<640px) the
group falls back to wrapping internally so it never overflows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 10:37:33 +02:00
itsrubberduck
9082a25cb8 Address classroom tester feedback (Detlef / FSC e.V.)
Fixes the most impactful issues from ~1.5h of testing:

- TTS: spell SID prefixes phonetically (ANEKI → Alfa November Echo Kilo
  India) so unfamiliar waypoint names are intelligible without prior
  briefing context.
- TTS: expand standalone uppercase waypoints after via/direct (with a
  skip list for common ATC English tokens like MAYDAY, CLEARED, …).
- TTS: join taxi route tokens with ", " so pauses land between
  taxiways (C5, Z5, U10, …) instead of running together.
- TTS: handle "ILS Z 25C" variant before the runway → "ILS Zulu runway
  two five center" (was previously read as "Zee twentyfive cee").
- Scenarios: derive arrivalRunway from the chosen approach so the
  controller no longer clears a flight for ILS 25C onto runway 18.
- Radio check: accept any readability 1–5 (numeric or spoken), shorten
  placeholder so it fits the sm-width field.
- Line-up readback: clearer hint about the runway-first ICAO order.
- Classroom UI: disable browser autocomplete/autocorrect on readback
  inputs (Edge autofill was injecting unrelated values).
- Classroom UI: "Speak answer" button replays the expected readback as
  TTS so students can hear the correct phrasing.

Tests adjusted for the new SID and taxi-route phonetics.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 10:24:48 +02:00
itsrubberduck
1f6c068f92 Refine PM frequency overview cards 2026-05-21 10:35:44 +02:00
itsrubberduck
8d341fc175 Simplify PM frequency panel 2026-05-21 10:30:23 +02:00
itsrubberduck
37a1e57f78 Unify PM topbar radio controls 2026-05-21 10:24:29 +02:00
itsrubberduck
44add20635 Translate PM tab labels 2026-05-21 10:22:06 +02:00
itsrubberduck
8ed2b7cf2a Polish PM radio settings copy 2026-05-21 10:20:44 +02:00
itsrubberduck
8992ddac0b intercom rausgefactored und ui improvements 2026-05-21 10:18:25 +02:00
itsrubberduck
a3f522daf0 neues pm ui 2026-05-21 10:10:53 +02:00
leubeem
25de89c5f5 fix: correct OpenAIP v2 airport frequency parsing (3 bugs)
The frequency panel was always empty when using OpenAIP because of three
compounding bugs in frequencies.get.ts:

1. Wrong query parameter — ?icao=EDDF performs an unfiltered full-text
   search and returns all 46 000+ airports paginated; EDDF wasn't even
   on the first page.  The correct parameter is ?search=EDDF, which
   returns exactly the one matching airport.

2. Wrong ICAO field name — the code checked airport.icao but the real
   field in the v2 API response is icaoCode.  Even on a correctly
   filtered response the match would always fail.

3. Wrong frequency field names and numeric types — each frequency item
   exposes the MHz value in a value field (not frequency / frequencyMHz),
   and the service type is a numeric code (5=Delivery, 9=Ground,
   14=Tower, 15=ATIS) rather than a string.  Added OPENAIP_TYPE_MAP to
   translate these numeric codes to the internal DEL/GND/TWR/ATIS codes
   the rest of the pipeline expects.

With these fixes EDDF now returns all 8 frequencies (Delivery 122.035,
Ground 121.805, three Tower variants, two ATIS) which are then stored in
delivery_freq / ground_freq / tower_freq / atis_freq and used for both
the frequency overview panel and the per-state frequency validation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 18:16:32 +02:00
leubeem
c21ddc1d52 feat: live expected communication from backend, radio pronunciation toggle, and frequency validation
- Expected Communication card now driven entirely by backend responses:
  controller speech from controller_say_rendered, expected pilot phrase
  from expected_pilot_template — both update after every state transition
  instead of relying on the static COMMUNICATION_STEPS array
- Initial expected pilot phrase seeded on session creation via the new
  expected_pilot_template field in CreateSessionResponse, so the card
  shows the correct text before any transmission
- Radio pronunciation toggle (mdi-radio / mdi-text) on the card applies
  normalizeRadioPhrase() (ICAO alphabet, wun/too/tree, callsign expansion)
  to both ATC speech and expected pilot phrase
- Frequency validation at transmit time: if the pilot's active frequency
  does not match the state's expected frequency (resolved from
  frequency_name → flight-plan variable) a canned ATC reply is played
  and the backend is not called
- Flight-plan variables (callsign, squawk, destination, ATIS, SID, stand,
  initial altitude) now passed to createSession() as variable_overrides
  so backend sessions use real flight-plan data from the first state
- Backend session variables synced into local vars after each response
  so frontend and backend stay consistent
- Removed hardcoded frequency chip from Expected Communication card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 17:51:44 +02:00
leubeem
f2643033f9 feat: wire Python backend session to PTT, add pm.vue debug logger
- Pass  as top-level field in PTT requests so Whisper STT
  results are linked to the correct Python backend session
- Add namespaced  helper in pm.vue (info/warn/error/debug/group)
  controlled by localStorage PM_DEBUG flag; logs transmit/response
  cycles, TTS calls, flag/variable syncs, and fallback warnings
- Log backend session creation context (flow, start state, vars, flags)
  in startMonitoring
- Fix typo in text input hint: STT fails not PTT fails

and

fix: sync backend variables to frontend after each transmission

The ATC say template was rendered using the frontend engine's local
variable defaults (squawk '1234', hardcoded SID, etc.) instead of
the authoritative values from the Python backend session. This caused
the spoken clearance and the readback prompt to show different squawk
codes.

- After each backend transmission response, sync all response.variables
  into vars.value (same pattern already used for flags)
- Prefer controller_say_rendered (pre-rendered by backend) over the raw
  template for TTS scheduling, eliminating any remaining dependency on
  local variable state for the ATC speech text
2026-05-20 16:44:01 +02:00
leubeem
ea4b1e0b24 Cleanup old unused code and add id sessionId to /api/atc/ptt 2026-05-20 14:13:26 +02:00
itsrubberduck
19162e2d77 copilot 2026-05-19 12:46:31 +02:00
itsrubberduck
14d06a67d4 copilot ui improvements 2026-05-10 17:15:41 +02:00
Emanuel Leube
5f2502bed7 Merge pull request #263 from OpenSquawk/feature/pm-python-backend-integration
Wire /pm to Python backend for stateful ATC training sessions
2026-05-09 17:50:38 +02:00
leubeem
d6df3a3ce3 Wire /pm to Python backend for stateful ATC training sessions
Replace the LLM-per-request flow in /pm with a stateful Python backend
(OpenSquawk-LiveATC-api). The backend owns session state, does regex-first
routing with readback evaluation, and returns the next state + ATC speech.
The frontend keeps its local cursor (communicationsEngine) for TTS and
monitoring UI, but no longer calls /api/llm/decide.

Changes:

app/composables/useRadioBackend.ts (new)
  Typed Nuxt composable wrapping the Python REST API:
  createSession, transmit, deleteSession, fetchFlows.
  Base URL read from NUXT_PUBLIC_RADIO_BACKEND_URL (default 127.0.0.1:8000).

nuxt.config.ts
  Expose radioBackendUrl as a public runtime config key so the composable
  and communicationsEngine can both reach the Python backend.

shared/utils/communicationsEngine.ts
  - fetchRuntimeTree now accepts an optional baseUrl so it fetches from the
    Python backend instead of the Nuxt server when a URL is provided.
  - renderTpl handles both {var} (old MongoDB schema) and {{var}} (new YAML
    schema) — double-brace matched first to avoid partial matches.
  - stateSayTpl / stateUtteranceTpl helpers unify say_tpl|say_template and
    utterance_tpl|expected_pilot_template across both schema versions.
  - auto_transitions from the new YAML schema are included when collecting
    eligible transitions in collectAtcStatesUntilPilotTurn.

shared/types/decision.ts
  RuntimeDecisionState extended with say_template and expected_pilot_template
  fields (new YAML schema field names alongside the existing legacy names).

app/pages/pm.vue
  - startMonitoring: loads tree from Python backend, then creates a backend
    session (backendSessionId). Cursor synced to session.current_state.
  - handlePilotTransmission: calls radioBackend.transmit instead of
    /api/llm/decide. Applies auto_advanced_states via moveToSilent, then
    the final state. Speaks controller_say_template via TTS.
  - Both fetchRuntimeTree calls now pass radioBackendUrl so they hit the
    Python backend, not the Nuxt flow-from-MongoDB path.

AGENTS.md (new)
  Project guide updated to document the new two-backend architecture,
  the Python backend session lifecycle, and the dual template schema.

docs/plans/2026-05-06-pm-python-runtime-contract.md (new)
  Implementation plan and API contract written before the work started.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 17:49:28 +02:00
itsrubberduck
d2db4741d2 Wochenreport 2026-05-06 17:38:36 +02:00