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

@@ -1,5 +1,6 @@
// server/api/flightlab/ws.ts
import { defineWebSocketHandler } from 'h3'
import { flightlabTelemetryStore } from '../../utils/flightlabTelemetry'
interface FlightLabSession {
code: string
@@ -10,11 +11,25 @@ interface FlightLabSession {
participantConnected: boolean
instructorConnected: boolean
history: Array<{ phaseId: string; buttonId: string; timestamp: number }>
peers: Map<string, { role: 'instructor' | 'participant'; peer: any }>
peers: Map<string, { role: 'instructor' | 'participant'; peer: any; userId?: string }>
/** User IDs subscribed to telemetry for this session */
telemetryUserIds: Set<string>
}
const sessions = new Map<string, FlightLabSession>()
// Map userId → session code for telemetry routing
const userSessionMap = new Map<string, string>()
// Subscribe to telemetry store updates and relay to WebSocket clients
flightlabTelemetryStore.subscribe((userId, data) => {
const sessionCode = userSessionMap.get(userId)
if (!sessionCode) return
const session = sessions.get(sessionCode)
if (!session) return
broadcastToSession(session, { type: 'telemetry', data })
})
function generateCode(): string {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' // No I,O,0,1 for readability
let code = ''
@@ -77,6 +92,7 @@ export default defineWebSocketHandler({
instructorConnected: true,
history: [],
peers: new Map([[peerId, { role: 'instructor', peer }]]),
telemetryUserIds: new Set(),
}
sessions.set(code, session)
peer.send(JSON.stringify({ type: 'session-created', code, state: getSessionState(session) }))
@@ -147,6 +163,17 @@ export default defineWebSocketHandler({
}, peerId)
break
}
case 'subscribe-telemetry': {
// Client sends their userId so bridge data gets routed to their session
const session = findSessionByPeer(peerId)
if (!session || !data.userId) return
const peerInfo = session.peers.get(peerId)
if (peerInfo) peerInfo.userId = data.userId
session.telemetryUserIds.add(data.userId)
userSessionMap.set(data.userId, session.code)
break
}
}
},
@@ -155,6 +182,11 @@ export default defineWebSocketHandler({
for (const [code, session] of sessions) {
const peerInfo = session.peers.get(peerId)
if (peerInfo) {
// Clean up telemetry subscription
if (peerInfo.userId) {
session.telemetryUserIds.delete(peerInfo.userId)
userSessionMap.delete(peerInfo.userId)
}
session.peers.delete(peerId)
if (peerInfo.role === 'participant') session.participantConnected = false
if (peerInfo.role === 'instructor') session.instructorConnected = false