257 Commits

Author SHA1 Message Date
itsrubberduck
70186482f4 fix(classroom): keep footer in document flow 2026-07-30 20:19:40 +02:00
itsrubberduck
2c5c2d5239 feat(classroom): show the full curriculum and raise the mastery bar
The overview rendered four anonymous module tiles and half a page of empty
space, which undersold 63 existing lessons. It now stacks four module
sections that list every lesson as a chip with status and variant dots, and
the header states the real scope: 4 modules, 63 lessons, 262 variations.

Mastery moves from a flat two-variation counter to per-module thresholds
(3/4/4/5) backed by persisted variant signatures — a hash over each roll's
expected answers. That triples the practice runs to full mastery without
authoring a single new lesson, and fixes a dedup hole where the same
variation counted twice after a page reload.

Existing progress is preserved: `done` is sticky, so raising the bar never
revokes a check mark someone already earned.

Also fixes `yarn test`, which passed quoted globs Node 20 does not expand —
it exited 0 without running a single test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 20:08:55 +02:00
itsrubberduck
9685103453 fix(routing): send /bridge to the Bridge download page
/bridge is the address the app hands to people who do not have the desktop
Bridge yet, but the only page under it is /bridge/connect — which pairs an
already-installed Bridge with a code. The bare path resolved to nothing.

It now forwards to opensquawk.de/bridge, where the download lives. The
classroom's experience switcher goes through it as well: picking Live ATC there
without a Bridge previously dropped the user into a cockpit with nothing
attached.

The two links that really do mean the pairing screen — the cockpit's "Connect
simulator" hint and the pairing screen's own post-login redirect — now name
/bridge/connect instead of relying on /bridge to land there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 19:03:10 +02:00
itsrubberduck
67e8d3573c fix(routing): give /bridge a page instead of a 404
Everything links to /bridge — the Live ATC card on the front page, the
cockpit's connect hint, and the post-login redirect the pairing screen builds
for itself — but the only page under it is /bridge/connect. The bare path
resolved to nothing, so the front page's own Live ATC card was a dead link.

Forwards to /bridge/connect from route middleware, keeping the query string so
a ?token= pairing link still works. Middleware rather than setup for the same
reason as /start and /login: an await in setup aborts the first render and
leaves a blank page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:58:09 +02:00
itsrubberduck
3cd754962c fix(routing): redirect before rendering, not while rendering
/start and /login are pure forwarders, and both did their `await navigateTo()`
in `<script setup>` with no template at all. That await suspends the first
render and the navigation aborts it, so the address bar flips to the target
while nothing is ever painted — a dark, empty page until the visitor reloads.

That is what people hit coming from the website: logging in there sends an
already-signed-in user to /start, which redirects here, and here they landed on
nothing. Reloading worked, which is exactly the tell — a cold load of the
target has no suspended render to abort.

Both redirects moved into route middleware, which runs before the page renders,
and both pages keep a placeholder template so a component without one can never
be what is on screen. /start still passes its query string through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 08:55:03 +02:00
itsrubberduck
49090d37f7 feat(classroom): show local speech bridge connection status in settings
Subtle status row (dot + Connected/Not detected) reflecting the same
useLocalSpeechBridge probe already driving TTS routing.
2026-07-29 09:21:08 +02:00
itsrubberduck
3488b54aa4 feat(classroom): use local speech bridge for instructor TTS when available
Mirrors the local-first routing live-atc already has via useRadioSpeech.ts:
requestSayAudio now tries a locally running Speech Bridge (127.0.0.1:8765-8770)
before falling back to the cloud /api/atc/say endpoint.
2026-07-29 08:54:13 +02:00
itsrubberduck
f08e8f5a96 fix(routing): catch the paths the split left pointing at nothing
Three dead ends, one cause: these routes resolved to pages that stayed on the
website, and nothing here answered them any more.

/start — this page became the app's front page in the split (it is index.vue
now), so every pre-split bookmark and the website's own redirect landed on a
404. Caught here so the app's old URLs work without waiting for the website to
be redeployed.

/login — the app has no login by design, identity comes from the issuer. But
five call sites still send people there: logout, the live-atc session guard,
the bridge pairing screen. In the monorepo that path was the website's login
form; since the split it was nothing, so signing out dropped the user on a
blank page. It is a forwarder to the issuer, not a login form.

/logout — after clearing the app session it sent the user to /login, which
under SSO means an issuer where they are still signed in: they would be handed
a fresh code and land straight back inside. It now signs them out at the issuer
too, which bumps tokenVersion and invalidates every refresh token.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 19:37:27 +02:00
itsrubberduck
676779895d testcommit 2026-07-28 19:29:09 +02:00
itsrubberduck
56d3c991d7 fix(sso): hand the issuer /auth/callback so the code gets redeemed
The auth guard asked the issuer to come back to the page the visitor wanted.
The issuer appends ?code= to whatever URL it is given, but only /auth/callback
redeems a code — so it arrived on the target page, sat there unread, the guard
found no session and bounced back for a fresh code. The browser ping-ponged
between the two hosts until the user gave up.

The guard now hands over /auth/callback and carries the wanted page in its
redirect parameter, which is exactly what that page already expected. A spent
code in the URL is dropped rather than carried along, so a stale link cannot
turn into a redemption error one hop later.

URL building moved into shared/utils/ssoHandoff.ts because the same mistake
existed twice — the retry button on the callback page had it too — and because
a redirect loop deserves a regression test that does not need a browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 19:14:20 +02:00
itsrubberduck
c8c2365c4d refactor(split): make the app self-hosting ready
Remove the transitional website auth, admin hooks, SEO and hosted analytics together, then align the app routes, runtime configuration, tests and dependencies. These changes form one atomic cleanup because the filtered app must switch its identity and runtime surfaces as a unit.
2026-07-27 19:17:27 +02:00
itsrubberduck
3ed3865ebb Merge branch 'codex/classroom-overhaul' 2026-07-27 18:52:53 +02:00
itsrubberduck
2b792eb57f refactor(split): decouple app from website in preparation for the repo split
Phase 0 of the OpenSquawk repo separation. Everything happens inside the
monorepo so that the split itself becomes a mechanical path filter — filtering
first and repairing afterwards would leave two broken repos at once.

AUTH_MODE (0.1)
  New server/utils/authMode.ts, session.ts and jwt.ts (the latter extracted
  from auth.ts). requireUserSession now resolves in three steps: the app's own
  session cookie, an app-minted bearer token, then the website access token.
  Only the last one is transitional; it is marked PHASE 1 and disappears with
  the User collection. The app's session is its own JWT in a host-only cookie
  plus a short-lived bearer, so the existing Authorization call sites are
  unchanged.

  AUTH_MODE defaults to 'sso', not 'open' as the plan proposed: while the admin
  and editor surface still lives here, an unset variable would otherwise serve
  it to everyone as a local admin. requireAdmin additionally refuses in open
  mode. Both are one-line removals in Phase 1 and marked as such.

SSO handoff (0.2)
  Issuer: /api/service/auth/sso/{authorize,exchange}. Codes are stored as
  SHA-256 hashes with a TTL index and claimed by a single atomic update, so
  concurrent redemption cannot succeed twice. redirect_uri is matched against
  SSO_REDIRECT_ORIGINS by exact origin — a prefix check would accept
  app.opensquawk.de.evil.tld. There is no default and no wildcard: an empty
  allowlist disables the handoff rather than opening a redirector.
  Consumer: /api/auth/sso/callback plus app/pages/auth/callback.vue. The
  browser only ever carries the code; it is redeemed server-to-server.

Hardcoded values and leaks (0.3)
  Hotjar ID, the dome-light webhook URL and the bug-report recipient were
  compiled in. All three are env-gated and off by default now, so a foreign
  instance cannot ship analytics, cockpit telemetry or its users' bug reports
  to us. Setting HOTJAR_ID, DOME_LIGHT_WEBHOOK_URL and BUG_REPORT_NOTIFY_EMAIL
  restores the current behaviour on opensquawk.de.

Two databases, no shared Mongo (0.6)
  AppUser mirrors an identity locally. Its _id is deliberately the SSO subject,
  i.e. the website's User._id, so every existing LearnProfile, PilotProfile and
  BridgeToken reference keeps resolving without a migration.
  telemetry.ts mirrors records to the hosted service only when TELEMETRY_URL
  and SERVICE_SECRET are both set — the self-host default is that nothing ever
  leaves the instance. It writes locally first, buffers with a bound, drops on
  overflow and never blocks the request path.
  /api/service/user-deleted purges the app's half on account deletion. Unlike
  telemetry this is deliberately loud: the admin delete aborts with the user
  intact if the purge fails, because their id is the only handle for retrying.
  ?force=true overrides it and says so in the response.

Also here
  /api/service/analytics/product-session was an unauthenticated public write
  endpoint; it moves to /api/analytics/product-session behind the auth guard.
  The bridge no longer populates against User but resolves through the mirror,
  backfilling missing rows so live bridges never have to re-pair.
  .claude/worktrees was tracked and would have reached the public repo.

scripts/split-paths.txt carries the filter list, verified by
scripts/verify-split-paths.mjs: every path exists, nothing website-only is
kept, and no kept file imports a dropped one. That check found real gaps —
tests/ cannot be taken wholesale, and two shared modules were missing. Ten
remaining edges are allowlisted, each annotated PHASE 1 in the code.

Open item, flagged and not resolved: flightlabTelemetryStore is an in-process
singleton written by the bridge (app) and read by FlightLab (website). Two
repos means two processes, so that read breaks regardless of which side it
lands on. FlightLab needs an HTTP path in Phase 2/3.

Verified: 609 tests pass, vue-tsc clean. Ran against two throwaway local
MongoDBs: open mode reaches /classroom and /live-atc with no login and
persists progress; the full SSO loop works and the mirror _id matches the
website User._id; lookalike origins, code reuse, forged codes and wrong
service secrets are all rejected; ingest is idempotent on bug-report code;
deletion purges all five collections; and with the app unreachable the admin
delete fails 502 with the user still present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 18:46:34 +02:00
itsrubberduck
b470d805f8 Improve classroom readback tolerance 2026-07-27 12:29:37 +02:00
itsrubberduck
6ce79145e6 merge: classroom overhaul 2026-07-27 11:55:08 +02:00
itsrubberduck
d6b2b328f6 feat(landing): align homepage with current alpha 2026-07-27 11:46:39 +02:00
itsrubberduck
b299122d88 refactor(tools): forward the airport endpoints to the backend
/api/service/tools/* was a second implementation of the OSM geocoding and taxi
routing the Python backend already owns — its own Overpass client, alias
matching and scoring. It drifted: it still asked Overpass for `out center tags`,
so a runway resolved to the middle of the strip rather than a threshold, which
is the runway-endpoint bug the backend fixed. Teaching the copy about runway
endpoints would mean maintaining the geometry twice, so the copy goes instead.

Forwarding exposes origin_runway_point / dest_runway_point and
include_connectors, and inherits the backend's Overpass cache and its radius
fallback for aerodromes with no generated OSM area (EDDM), which the copy
answered with an empty feature list. Failures now carry an HTTP status as well
as the error code in the body; the old copy answered every error with 200.

The frequencies call sites needed real types: airportGeocode.ts sat under
server/api with no default export, so Nitro registered it as a route whose
return type collapsed to any and poisoned the whole API type map. The typecheck
was green because of it, not despite it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:30:52 +02:00
itsrubberduck
30bf0d51c8 fix(classroom): restore audio-first prompt practice 2026-07-27 11:15:01 +02:00
itsrubberduck
ed8fe7366d feat(live-atc): send the filed route so the clearance can grant it
The route reached the local engine already — genSID() takes it and throws it
away — but never reached the backend, so the clearance could only issue a SID.

The payload mapping moves to shared/utils so it can be tested at all: app/
composables are outside the test glob, and the fallback chains and string
coercion in it were entirely uncovered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 11:12:43 +02:00
itsrubberduck
bc03b3f277 fix(classroom): surface callsign in compact lesson values 2026-07-27 11:06:47 +02:00
itsrubberduck
f949b42eaa fix(classroom): compact mobile lesson footer 2026-07-27 10:56:49 +02:00
itsrubberduck
8fb186ef60 fix(classroom): keep pilot answers text-only 2026-07-27 10:49:27 +02:00
itsrubberduck
dae8b122c9 feat(classroom): simplify guided learning interface 2026-07-27 10:26:52 +02:00
itsrubberduck
a27bd8a07b feat(frequency): offer the destination's stations, and mark what is out of range
The radio only ever held one airport's frequencies, loaded once at the start, so
on a departure the destination's Tower never appeared however close you got —
there was no way to dial ahead.

Both ends of the flight are now listed: the field being flown first and
unlabelled, the far end after it, tagged with its ICAO and drawn with a dashed
border so it reads as somewhere else. The destination's stations are kept in
their own list rather than merged into the primary one, because everything that
resolves *which frequency this phase expects* — the frequency map, the expected
frequency, the wrong-frequency gate, the ATIS wiring — reads that list, and
folding a second airport's Tower into it would let the gate accept the wrong
field's frequency.

Out-of-range stations are dimmed rather than removed: VHF is line-of-sight, so a
station unreachable on the ground is workable from the cruise, and it should come
back as you climb. The distances come from the backend, which already derives
them and now returns them — the browser does not need its own copy of the airport
coordinates for this.

With no bridge connected, nothing is dimmed at all. That is the normal case for
someone practising without a simulator, and a station that cannot be *proven* out
of range must not be taken away.
2026-07-27 10:17:29 +02:00
itsrubberduck
c397ef6f95 test(live-atc): cover the wait auto-tune actually depends on
The decision to tune was tested; the three seconds between announcing it and
doing it were not, and that gap is where the feature can go wrong — it is the
only window in which the radio moves on its own.

Moves the scheduling into a testable unit and wires the composable to it, so
the tested code is the code that runs. Covered: nothing is announced when no
change is due, the change is announced immediately but made only after the
delay, and it is dropped when the pilot tuned the radio themselves, when the
session ended, and when a second handoff superseded it — the last of which would
otherwise have tuned to a stale frequency.

Leaving the flight or unmounting now cancels a pending change explicitly rather
than relying on the fire-time guards to notice.
2026-07-27 10:04:07 +02:00
itsrubberduck
f852c8f2d5 feat(live-atc): teach the rare events on demand
A rejected take-off and a go-around both happen by chance about once in five
hundred flights, which is the point of them — you cannot practise reacting to
something you were told to expect. That also made them impossible to teach.

Two switches under Special scenarios force either on the next flight. They are
read when the session is created rather than watched, so flipping one mid-flight
applies to the following one — which is how an instructor sets it up without the
pilot seeing it coming. Only a switched-on value is sent: the backend reads the
variable's absence as "leave it to chance", so sending false would pin the roll
off instead of leaving it alone.

Styled amber rather than the usual cyan, and the consequence is spelled out
while either is armed: neither scenario continues to the stand.
2026-07-27 09:59:56 +02:00
itsrubberduck
1bd1aac76d feat(live-atc): optional auto-tune after a frequency handoff
Tuning is manual, so after a handoff nothing the pilot says goes through until
they dial the new frequency in. With the setting on, the radio does it for them
three seconds after the handoff was accepted, announcing "OpenSquawk changing
frequency to …" first so it is never a surprise.

Whether a change is due is decided from state rather than from an event, which
is what makes the two must-not-tune cases safe without a special case: a
frequency readback that was wrong and one not yet given both leave the session
on a state that still expects the frequency already dialled in, so nothing is
due. A pending change is dropped if the session ends or the pilot reaches for
the radio themselves — theirs wins.

Off by default: working the radio is part of what is being practised, so
handing it to the aircraft has to be a deliberate choice. The decision itself
is a pure function and covered by tests; the announcement goes through the same
speech and comm-log path as everything else, so the browser sim and the bridge
both see the tuned frequency the way they already do for a manual change.

Also folds the two copies of normalizedFrequencyValue into one in shared/, so
the auto-tune logic can compare frequencies without a composable import.
2026-07-27 00:51:53 +02:00
itsrubberduck
a40ce0a7eb fix(live-atc): arm the silence timer on readback states too
The timer was only armed for states carrying auto_advance_on_silence. Readback
states carry none — they wait for the pilot — so nothing was armed and ATC
never asked again when the readback did not arrive.

Which window applies to a state is now a pure function: the flow-authored one
for an auto-advance state, the much shorter server-published one for a readback
state. That also makes both cases testable instead of inline in the composable.
2026-07-27 00:30:59 +02:00
itsrubberduck
132f5faf9c fix(live-atc): stop swallowing "roger" while still dropping dead air
The PTT gate dropped any spoken transmission under two words, which is exactly
the length of the calls a pilot most often makes: "roger", "wilco", "affirm",
"negative". Those never reached the engine at all.

The gate is now phraseology-aware rather than length-only, and moved into a
pure function so it can be tested: standard short calls pass regardless of
length, while the things push-to-talk actually produces when nothing was said
are still dropped — punctuation from near-silent audio, a syllable clipped by
an early key release, and the phrases Whisper hallucinates on silence
("Thank you.", "Bye."), which are rejected only as a whole transcript so a real
sign-off still passes.

The ignore log now names the reason and the state it happened in, so a
transmission that vanished can be traced.
2026-07-27 00:11:04 +02:00
itsrubberduck
2a798b4452 feat(bug-reports): trace reports by code and tell Live ATC from Classroom
Every report now gets a UUID that the notification mail quotes as
"Nenne den Fehlercode <uuid> beim Commit", so a fix can be tied back to
the report it closes. The mail also names the area it came from, and the
comment field is relabelled "Fehlerbeschreibung/Featurewunsch" since it
collects feature wishes just as often as bugs.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:48:09 +02:00