mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-05 08:55:54 +08:00
feat(sim-control): wire frequency-sim-control command channel end-to-end
Implements the open items from docs/plans/2026-07-14-frequency-sim-control-design.md §4/§"Offen für die Implementierungsphase": a per-bridge-token in-memory command queue piggybacked on the existing telemetry channel, plus the client-side gate and TTS confirmations. - server/utils/simControlQueue.ts: TTL-based queue keyed by bridge token (enqueue → drainPending → resolve → drainResultsForClient). - server/api/bridge/data.post.ts: response gains a `commands` field the bridge drains on its next telemetry POST. - server/api/bridge/command.post.ts (new): client enqueues a parsed command, re-validated server-side via isValidSimControlCommand. - server/api/bridge/command-result.post.ts (new): bridge reports ok/failed. - server/api/bridge/live.get.ts: response gains `commandResults` so the client can announce outcomes. - shared/utils/simControl.ts: wire types, isValidSimControlCommand, and simControlRejectionSpeech/simControlResultSpeech TTS phrasing. - useLiveAtcSession.ts: parseSimControl() gated on bridgeConnected, wired in right after the local special cases and before the frequency check — matched commands never reach radioBackend.transmit(). - useSimBridgeSync.ts / live-atc.vue: bridgeToken threaded through, command results forwarded from the telemetry poll to TTS. 43 new tests (shared parser/validation/speech + server queue lifecycle/TTL). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
54
server/api/bridge/command.post.ts
Normal file
54
server/api/bridge/command.post.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { createError, readBody } from 'h3'
|
||||
import { BridgeToken } from '../../models/BridgeToken'
|
||||
import { getBridgeTokenFromHeader } from '../../utils/bridge'
|
||||
import { logBridgeEvent } from '../../utils/bridgeLog'
|
||||
import { simControlQueue } from '../../utils/simControlQueue'
|
||||
import { isValidSimControlCommand, type SimControlCommand } from '../../../shared/utils/simControl'
|
||||
|
||||
interface CommandBody {
|
||||
command?: SimControlCommand
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a frequency-sim-control command (design doc §4) for the bridge
|
||||
* behind this token to pick up on its next telemetry POST.
|
||||
*
|
||||
* POST /api/bridge/command
|
||||
* x-bridge-token: <bridge-token>
|
||||
* body: { command: SimControlCommand }
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const token = getBridgeTokenFromHeader(event)
|
||||
if (!token) {
|
||||
throw createError({ statusCode: 401, statusMessage: 'x-bridge-token header fehlt oder ist ungültig.' })
|
||||
}
|
||||
|
||||
// Only relay for a linked token — an unauthenticated caller can't queue commands.
|
||||
const exists = await BridgeToken.exists({ token })
|
||||
if (!exists) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Bridge-Token ist nicht verknüpft.' })
|
||||
}
|
||||
|
||||
const body = await readBody<CommandBody>(event)
|
||||
const command = body?.command
|
||||
if (!isValidSimControlCommand(command)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Ungültiges Kommando.' })
|
||||
}
|
||||
|
||||
const pending = simControlQueue.enqueue(token, command)
|
||||
|
||||
console.info(
|
||||
`\x1b[32m[bridge:command]\x1b[0m token=\x1b[96m${token.slice(0, 6)}...\x1b[0m id=\x1b[96m${pending.id.slice(0, 8)}\x1b[0m type=\x1b[93m${command.type}\x1b[0m`,
|
||||
)
|
||||
|
||||
logBridgeEvent(token, {
|
||||
endpoint: '/api/bridge/command',
|
||||
method: 'POST',
|
||||
statusCode: 200,
|
||||
color: '#22c55e',
|
||||
summary: `queued ${command.type}`,
|
||||
data: { id: pending.id, command },
|
||||
})
|
||||
|
||||
return { ok: true, id: pending.id, issued_at: pending.issued_at }
|
||||
})
|
||||
Reference in New Issue
Block a user