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 af2eed2d6e
commit acb79cdb2b
2 changed files with 86 additions and 7 deletions

View File

@@ -1,12 +1,56 @@
import { defineEventHandler, readBody } from 'h3'
import { defineEventHandler, readBody, getHeader } from 'h3'
import { resolveUserFromToken } from '../../utils/auth'
import { flightlabTelemetryStore } from '../../utils/flightlabTelemetry'
/**
* Receives MSFS SimConnect telemetry data from an external bridge application.
*
* The bridge should POST telemetry data with an Authorization header so we
* can route the data to the correct FlightLab WebSocket session.
*
* ──────────────────────────────────────────
* EXAMPLE REQUEST (from SimBridge app):
* ──────────────────────────────────────────
*
* POST /api/bridge/data
* Authorization: Bearer <user-jwt-token>
* Content-Type: application/json
*
* {
* "AIRSPEED_INDICATED": 145.2,
* "GROUND_VELOCITY": 142.8,
* "VERTICAL_SPEED": 0,
* "PLANE_ALTITUDE": 364,
* "PLANE_PITCH_DEGREES": 0.5,
* "TURB_ENG_N1_1": 87.3,
* "TURB_ENG_N1_2": 86.9,
* "SIM_ON_GROUND": true,
* "GEAR_HANDLE_POSITION": true,
* "FLAPS_HANDLE_INDEX": 2,
* "BRAKE_PARKING_POSITION": false,
* "AUTOPILOT_MASTER": false
* }
*
* ──────────────────────────────────────────
* RESPONSE: 204 No Content (success)
* 401 Unauthorized (missing/invalid token)
* ──────────────────────────────────────────
*/
export default defineEventHandler(async (event) => {
// Body einlesen
const body = await readBody(event)
// Resolve user from Bearer token (optional — also works with ?userId query param)
const user = await resolveUserFromToken(event)
const userId = user?._id?.toString()
?? new URL(event.node.req.url ?? '', 'http://localhost').searchParams.get('userId')
// Alles in der Server-Konsole ausgeben
console.log('MSFS Telemetry:', body)
if (!userId) {
event.node.res.statusCode = 401
return { error: 'Authorization required — send Bearer token or ?userId query param' }
}
// Leere 204-Antwort zurückgeben
event.node.res.statusCode = 204
const body = await readBody(event)
// Store telemetry and broadcast to WebSocket subscribers
flightlabTelemetryStore.update(userId, body)
event.node.res.statusCode = 204
})

View File

@@ -0,0 +1,35 @@
/**
* In-memory telemetry store that bridges the HTTP POST endpoint
* with FlightLab WebSocket sessions.
*
* Flow: SimBridge → POST /api/bridge/data → telemetryStore → WS → Client
*/
type TelemetryListener = (userId: string, data: any) => void
class FlightLabTelemetryStore {
/** Latest telemetry per userId */
private data = new Map<string, any>()
/** WebSocket listeners that get notified on new data */
private listeners = new Set<TelemetryListener>()
update(userId: string, telemetry: any) {
this.data.set(userId, { ...telemetry, timestamp: Date.now() })
// Notify all listeners (WebSocket handler subscribes here)
for (const listener of this.listeners) {
try { listener(userId, this.data.get(userId)) } catch {}
}
}
get(userId: string) {
return this.data.get(userId) ?? null
}
subscribe(listener: TelemetryListener) {
this.listeners.add(listener)
return () => { this.listeners.delete(listener) }
}
}
// Singleton — shared across all server handlers
export const flightlabTelemetryStore = new FlightLabTelemetryStore()