diff --git a/docs/mission-briefing-images.md b/docs/mission-briefing-images.md new file mode 100644 index 0000000..cdd9b32 --- /dev/null +++ b/docs/mission-briefing-images.md @@ -0,0 +1,13 @@ +# Mission briefing image prompts + +Use these prompts to create cohesive concept art for the Full Flight mission briefing. Paths are relative to `/public`. + +| Image path | Suggested prompt | +| --- | --- | +| `/img/learn/missions/full-flight/briefing-hero.png` | "Cinematic night-time view from an airliner cockpit ready for pushback, glowing avionics, airport ramp lights, teal and navy color palette, ultra wide angle, high fidelity concept art" | +| `/img/learn/missions/full-flight/briefing-route.png` | "Stylized aviation route map overlayed on a glowing nav chart, neon cyan routing line between two hubs, soft depth of field, futuristic UI aesthetic" | +| `/img/learn/missions/full-flight/briefing-departure.png` | "Dynamic scene of an airliner taxiing out at dawn, tower in background, ramp service vehicles with motion blur, cinematic lighting, teal and amber highlights" | +| `/img/learn/missions/full-flight/briefing-arrival.png` | "Airliner on final approach at sunset breaking through clouds, runway lights shimmering, cockpit perspective, warm oranges contrasting with cool blues" | +| `/img/learn/missions/full-flight/briefing-weather.png` | "Weather radar-inspired composition showing cumulonimbus clouds around an airport, lightning in distance, deep blues and violets with cyan HUD overlays" | + +Add the final art assets in `public/img/learn/missions/full-flight/` using the paths above. diff --git a/server/api/learn/simbrief.get.ts b/server/api/learn/simbrief.get.ts new file mode 100644 index 0000000..e59c2ff --- /dev/null +++ b/server/api/learn/simbrief.get.ts @@ -0,0 +1,68 @@ +import { createError, defineEventHandler, getQuery } from 'h3' + +const SIMBRIEF_TIMEOUT_MS = 10000 + +export default defineEventHandler(async event => { + const query = getQuery(event) + const rawId = (query.userId ?? query.userid ?? '').toString().trim() + if (!rawId) { + throw createError({ statusCode: 400, statusMessage: 'userId required' }) + } + + const url = new URL('https://www.simbrief.com/api/xml.fetcher.php') + url.searchParams.set('userid', rawId) + url.searchParams.set('json', '1') + + let response: Response + const controller = new AbortController() + const timeout = setTimeout(() => { + controller.abort() + }, SIMBRIEF_TIMEOUT_MS) + try { + response = await fetch(url.toString(), { + headers: { + Accept: 'application/json', + 'User-Agent': 'OpenSquawk Mission Planner' + }, + cache: 'no-store', + signal: controller.signal + }) + } catch (error: any) { + const aborted = error?.name === 'AbortError' + console.error( + `[simbrief] Request failed for user ${rawId}${aborted ? ' (timeout)' : ''}`, + error + ) + throw createError({ + statusCode: 502, + statusMessage: aborted ? 'SimBrief request timed out' : 'SimBrief request failed', + data: { message: aborted ? 'Request timed out' : error?.message || 'Network error' } + }) + } finally { + clearTimeout(timeout) + } + + if (!response.ok) { + const payload = await response.text().catch(() => '') + console.error( + `[simbrief] Request rejected for user ${rawId}: ${response.status} ${response.statusText}`, + payload + ) + throw createError({ + statusCode: response.status, + statusMessage: 'SimBrief request rejected', + data: { message: payload || response.statusText } + }) + } + + try { + const data = await response.json() + return { data } + } catch (error: any) { + throw createError({ + statusCode: 502, + statusMessage: 'Invalid SimBrief response', + data: { message: error?.message || 'Failed to parse JSON' } + }) + } +})