feat(flightlab): sidebar, progress bars, skip speech, SimBridge telemetry & auth

- Add collapsible sidebar with phase stepper (jump between phases)
- Add SimBridge conditions panel in sidebar (live values, progress bars, targets)
- Add global progress bar (top edge, glowing) + phase-local TTS progress bar
- Add skip button to skip TTS speech while ATC is speaking
- Add skipSpeech() to audio composable (stops current Pizzicato sound)
- Wire up bridge data.post.ts with user auth (JWT) + example payload
- Add server-side telemetry store with pub/sub for Bridge→WS relay
- Extend WS handler with subscribe-telemetry message + userId tracking
- Extend sync composable with subscribeTelemetry() + onTelemetry() callback
- Add require-auth middleware to all flightlab pages
- Fix instructor station ECONNREFUSED via import.meta.client guard
- Add animations: phase transitions, button lists, fade-scale, check-pop, pulse

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-02-14 10:14:33 +01:00
parent 2efa24f7f5
commit 77ecd49334
8 changed files with 722 additions and 175 deletions

View File

@@ -12,6 +12,7 @@ export function useFlightLabAudio() {
const soundBuffers = ref<Map<string, AudioBuffer>>(new Map())
let speechQueue: Promise<void> = Promise.resolve()
let pizzicato: PizzicatoLiteType | null = null
let currentSpeechReject: (() => void) | null = null
// --- Replay Cache ---
const lastSpokenAudio = ref<{ base64: string; mime: string; readability: number; text: string } | null>(null)
@@ -151,6 +152,9 @@ export function useFlightLabAudio() {
})
}
let currentPizzicatoSound: any = null
let currentNoiseStoppers: Array<() => void> = []
async function playWithRadioEffects(base64: string, _mime: string, readability: number) {
const pz = await loadPizzicato()
if (!pz) return
@@ -158,6 +162,7 @@ export function useFlightLabAudio() {
const profile = getReadabilityProfile(readability)
const sound = await pz.createSoundFromBase64(ctx, base64)
currentPizzicatoSound = sound
// Apply radio filter chain
sound.addEffect(new pz.Effects.HighPassFilter(ctx, { frequency: profile.eq.highpass, q: profile.eq.highpassQ }))
@@ -185,9 +190,28 @@ export function useFlightLabAudio() {
sound.setVolume(profile.gain)
const stopNoise = createNoiseGenerators(ctx, sound.duration, profile, readability)
currentNoiseStoppers = stopNoise
await sound.play()
stopNoise.forEach((fn: () => void) => fn())
currentPizzicatoSound = null
currentNoiseStoppers = []
}
/** Skip currently playing TTS speech immediately */
function skipSpeech() {
if (!isSpeaking.value) return
// Stop the pizzicato sound
if (currentPizzicatoSound) {
try { currentPizzicatoSound.stop() } catch {}
currentPizzicatoSound = null
}
// Stop noise generators
currentNoiseStoppers.forEach((fn) => { try { fn() } catch {} })
currentNoiseStoppers = []
isSpeaking.value = false
// Reset the speech queue so next speech can start fresh
speechQueue = Promise.resolve()
}
async function replayLastMessage(): Promise<void> {
@@ -248,6 +272,7 @@ export function useFlightLabAudio() {
crossfadeSound,
stopAllSounds,
setMasterVolume,
skipSpeech,
dispose,
}
}

View File

@@ -14,6 +14,7 @@ export function useFlightLabSync() {
onInstructorMessage: [] as Array<(text: string, withRadioEffect: boolean) => void>,
onPeerJoined: [] as Array<(peerRole: FlightLabRole) => void>,
onPeerLeft: [] as Array<(peerRole: FlightLabRole) => void>,
onTelemetry: [] as Array<(data: any) => void>,
onError: [] as Array<(msg: string) => void>,
}
@@ -70,6 +71,9 @@ export function useFlightLabSync() {
case 'peer-left':
callbacks.onPeerLeft.forEach(cb => cb(data.role))
break
case 'telemetry':
callbacks.onTelemetry.forEach(cb => cb(data.data))
break
case 'error':
callbacks.onError.forEach(cb => cb(data.message))
break
@@ -92,6 +96,11 @@ export function useFlightLabSync() {
send({ type: 'join-session', code: code.toUpperCase(), role: joinRole })
}
/** Subscribe this session to receive telemetry for the given userId */
function subscribeTelemetry(userId: string) {
send({ type: 'subscribe-telemetry', userId })
}
function sendParticipantAction(phaseId: string, buttonId: string, nextPhaseId: string) {
send({ type: 'participant-action', phaseId, buttonId, nextPhaseId })
}
@@ -117,6 +126,7 @@ export function useFlightLabSync() {
function onInstructorMessage(cb: (text: string, withRadioEffect: boolean) => void) { callbacks.onInstructorMessage.push(cb) }
function onPeerJoined(cb: (role: FlightLabRole) => void) { callbacks.onPeerJoined.push(cb) }
function onPeerLeft(cb: (role: FlightLabRole) => void) { callbacks.onPeerLeft.push(cb) }
function onTelemetry(cb: (data: any) => void) { callbacks.onTelemetry.push(cb) }
function onError(cb: (msg: string) => void) { callbacks.onError.push(cb) }
return {
@@ -126,6 +136,7 @@ export function useFlightLabSync() {
remoteState,
createSession,
joinSession,
subscribeTelemetry,
sendParticipantAction,
sendInstructorCommand,
sendInstructorMessage,
@@ -134,6 +145,7 @@ export function useFlightLabSync() {
onInstructorMessage,
onPeerJoined,
onPeerLeft,
onTelemetry,
onError,
}
}