From 46224dd70d0161e22d6c666bf87c272c9474643c Mon Sep 17 00:00:00 2001 From: itsrubberduck Date: Sat, 11 Jul 2026 09:55:19 +0200 Subject: [PATCH] chore(live-atc): add dev-only, opt-in API mock for driving /live-atc offline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verification aid, not a product feature. Guarded by import.meta.dev (stripped from prod builds) and opt-in via `?mock=1` (`?mock=0` disables). It wraps globalThis.$fetch and stubs only the Nuxt-side endpoints that would otherwise need a login, VATSIM, or OpenAI — auth (/api/auth/me, refresh), airport frequencies, METAR, TTS (silent), STT — and passes everything else through, including the authoritative Python radio backend on :8000, which is expected to run for real. Used to run the full real /live-atc page end-to-end against the real decision engine: setup → scenario picker → cockpit → text transmit → real controller reply in the log → readback → session_complete → completion screen. Confirms the redesign extraction holds on the real page, not just in component harnesses. Co-Authored-By: Claude Opus 4.8 --- app/plugins/mock-live-atc.client.ts | 91 +++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 app/plugins/mock-live-atc.client.ts diff --git a/app/plugins/mock-live-atc.client.ts b/app/plugins/mock-live-atc.client.ts new file mode 100644 index 0000000..d4a4329 --- /dev/null +++ b/app/plugins/mock-live-atc.client.ts @@ -0,0 +1,91 @@ +/** + * DEV-ONLY, OPT-IN mock for driving /live-atc without real auth/OpenAI. + * + * The authoritative decision engine (the Python backend on :8000) is expected to + * be running for real — this plugin does NOT touch those calls. It only stubs the + * Nuxt-side endpoints that would otherwise need a login, VATSIM, or OpenAI: + * auth (/api/auth/me, refresh), airport frequencies, METAR, TTS, STT. + * + * Activate by visiting any URL with `?mock=1` once; the flag persists in + * localStorage for the session. Everything is guarded by import.meta.dev so it + * is stripped from production builds. Not committed as a feature — a throwaway + * verification aid. + */ +export default defineNuxtPlugin(() => { + if (!import.meta.dev) return + + const params = new URLSearchParams(window.location.search) + if (params.get('mock') === '1') localStorage.setItem('mockLiveAtc', '1') + if (params.get('mock') === '0') localStorage.removeItem('mockLiveAtc') + if (localStorage.getItem('mockLiveAtc') !== '1') return + + // Seed a session so the page's auth guard passes (store reads this on init). + localStorage.setItem('os_access_token', 'mock-token') + + const MOCK_USER = { + id: 'mock-pilot', + email: 'mock@opensquawk.dev', + name: 'Mock Pilot', + role: 'dev', + emailVerified: true, + } + + const FREQS = { + airportName: 'Frankfurt am Main (MOCK)', + sources: { vatsim: false, openaip: true }, + frequencies: [ + { type: 'ATIS', label: 'ATIS', frequency: '118.025', source: 'openaip', atisCode: 'K', atisText: 'EDDF ATIS K' }, + { type: 'DEL', label: 'Delivery', frequency: '121.950', source: 'openaip' }, + { type: 'GND', label: 'Ground', frequency: '121.800', source: 'openaip' }, + { type: 'TWR', label: 'Tower', frequency: '119.900', source: 'openaip' }, + { type: 'APP', label: 'Approach', frequency: '120.150', source: 'openaip' }, + ], + } + + const METAR = 'EDDF 101950Z 25008KT 9999 FEW040 18/12 Q1013 NOSIG' + + const json = (data: unknown) => data + + // Return a canned response for a mocked Nuxt path, or the sentinel MISS. + const MISS = Symbol('miss') + function handle(url: string, opts: any): unknown { + const path = url.split('?')[0] + + if (path === '/api/auth/me') return json(MOCK_USER) + if (path === '/api/service/auth/refresh') return json({ accessToken: 'mock-token' }) + if (path === '/api/auth/login') return json({ accessToken: 'mock-token', user: MOCK_USER }) + + if (/^\/api\/airports\/[^/]+\/frequencies$/.test(path)) return json(FREQS) + if (path === '/api/vatsim/metar') return METAR + if (path === '/api/vatsim/flightplans') return json({ flightplans: [] }) + + // TTS: report "no audio" so nothing plays but the text flow (log, + // expected-comm) is unaffected — those come from the backend transmit reply. + if (path === '/api/atc/say') return json({ success: false }) + + // STT: we cannot transcribe here. Echo the expected phrase the caller sent so + // a PTT press behaves as a correct readback against the real backend. + if (path === '/api/atc/ptt') { + const expected = opts?.body?.expected + const transcription = (Array.isArray(expected) ? expected[0] : expected) || 'say again' + return json({ success: true, transcription }) + } + + return MISS + } + + const realFetch = globalThis.$fetch + const patched = ((url: any, opts: any) => { + if (typeof url === 'string') { + const result = handle(url, opts) + if (result !== MISS) return Promise.resolve(result) + } + return realFetch(url, opts) + }) as typeof globalThis.$fetch + // ofetch helpers (.raw/.create) — keep them pointing at the real impl. + Object.assign(patched, realFetch) + globalThis.$fetch = patched + + // eslint-disable-next-line no-console + console.info('[mock-live-atc] active — auth/freq/metar/TTS/STT stubbed; backend :8000 is real') +})