From e50eb66b2a729807f68e6cc5481225dc6aa0860e Mon Sep 17 00:00:00 2001 From: Remi <73385395+itsrubberduck@users.noreply.github.com> Date: Sun, 21 Sep 2025 17:02:47 +0200 Subject: [PATCH 1/2] Add mission planning flow to learn page --- .../missions/full-flight/briefing-arrival.png | 3 + .../full-flight/briefing-departure.png | 3 + .../missions/full-flight/briefing-hero.png | 3 + .../missions/full-flight/briefing-route.png | 3 + .../missions/full-flight/briefing-weather.png | 3 + server/api/learn/simbrief.get.ts | 68 +++++++++++++++++++ shared/data/learnModules.ts | 20 +++++- shared/learn/scenario.ts | 44 ++++++++---- shared/learn/types.ts | 6 ++ 9 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 public/img/learn/missions/full-flight/briefing-arrival.png create mode 100644 public/img/learn/missions/full-flight/briefing-departure.png create mode 100644 public/img/learn/missions/full-flight/briefing-hero.png create mode 100644 public/img/learn/missions/full-flight/briefing-route.png create mode 100644 public/img/learn/missions/full-flight/briefing-weather.png create mode 100644 server/api/learn/simbrief.get.ts diff --git a/public/img/learn/missions/full-flight/briefing-arrival.png b/public/img/learn/missions/full-flight/briefing-arrival.png new file mode 100644 index 0000000..943fa88 --- /dev/null +++ b/public/img/learn/missions/full-flight/briefing-arrival.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffcedec6257e64e91c7f3cbaf37996fbae0a9018058a47ac5d5fa4acbb90c553 +size 68 diff --git a/public/img/learn/missions/full-flight/briefing-departure.png b/public/img/learn/missions/full-flight/briefing-departure.png new file mode 100644 index 0000000..943fa88 --- /dev/null +++ b/public/img/learn/missions/full-flight/briefing-departure.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffcedec6257e64e91c7f3cbaf37996fbae0a9018058a47ac5d5fa4acbb90c553 +size 68 diff --git a/public/img/learn/missions/full-flight/briefing-hero.png b/public/img/learn/missions/full-flight/briefing-hero.png new file mode 100644 index 0000000..943fa88 --- /dev/null +++ b/public/img/learn/missions/full-flight/briefing-hero.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffcedec6257e64e91c7f3cbaf37996fbae0a9018058a47ac5d5fa4acbb90c553 +size 68 diff --git a/public/img/learn/missions/full-flight/briefing-route.png b/public/img/learn/missions/full-flight/briefing-route.png new file mode 100644 index 0000000..943fa88 --- /dev/null +++ b/public/img/learn/missions/full-flight/briefing-route.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffcedec6257e64e91c7f3cbaf37996fbae0a9018058a47ac5d5fa4acbb90c553 +size 68 diff --git a/public/img/learn/missions/full-flight/briefing-weather.png b/public/img/learn/missions/full-flight/briefing-weather.png new file mode 100644 index 0000000..943fa88 --- /dev/null +++ b/public/img/learn/missions/full-flight/briefing-weather.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffcedec6257e64e91c7f3cbaf37996fbae0a9018058a47ac5d5fa4acbb90c553 +size 68 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' } + }) + } +}) diff --git a/shared/data/learnModules.ts b/shared/data/learnModules.ts index 0f40a69..cde91cb 100644 --- a/shared/data/learnModules.ts +++ b/shared/data/learnModules.ts @@ -1,5 +1,5 @@ import { createBaseScenario, createScenarioSeries, formatTemp } from '~~/shared/learn/scenario' -import type { ModuleDef } from '~~/shared/learn/types' +import type { ModuleDef, Scenario } from '~~/shared/learn/types' function gradientArt(colors: string[]): string { const stops = colors @@ -2194,6 +2194,18 @@ const decisionTreeLessons = [ const fullFlightScenario = createScenarioSeries() +export function seedFullFlightScenario(scenario: Scenario | null) { + if (scenario) { + fullFlightScenario.setScenario(scenario) + } else { + fullFlightScenario.setScenario(null) + } +} + +export function peekFullFlightScenario(): Scenario | null { + return fullFlightScenario.peek() +} + const makeFullFlightGenerator = (reset = false) => () => { if (reset) { fullFlightScenario.reset() @@ -2951,7 +2963,11 @@ export const learnModules: ModuleDef[] = [ title: 'Full Flight ยท Gate to Gate', subtitle: 'One linked scenario from clearance to taxi-in', art: '/img/learn/modules/img6.jpeg', - lessons: fullFlightLessons + lessons: fullFlightLessons, + meta: { + flightPlan: true, + briefingArt: '/img/learn/missions/full-flight/briefing-hero.png' + } } ] diff --git a/shared/learn/scenario.ts b/shared/learn/scenario.ts index ed8800c..8adefac 100644 --- a/shared/learn/scenario.ts +++ b/shared/learn/scenario.ts @@ -198,7 +198,7 @@ function generateSquawk(): string { return code } -function digitsToWords(value: string): string { +export function digitsToWords(value: string): string { return value .split('') .map(char => atcNumberWords[char] ?? char) @@ -207,7 +207,7 @@ function digitsToWords(value: string): string { .trim() } -function lettersToNato(value: string): string { +export function lettersToNato(value: string): string { return value .toUpperCase() .split('') @@ -215,7 +215,7 @@ function lettersToNato(value: string): string { .join(' ') } -function runwayToWords(runway: string): string { +export function runwayToWords(runway: string): string { const digits = runway.replace(/[^0-9]/g, '').padStart(2, '0') const suffix = runway.replace(/[0-9]/g, '') const base = digits @@ -274,17 +274,17 @@ function visibilityToWords(vis: string): string { return vis } -function minutesToWords(minutes: number): string { +export function minutesToWords(minutes: number): string { const value = Math.max(1, Math.round(minutes)) const unit = value === 1 ? 'minute' : 'minutes' return `${digitsToWords(value.toString())} ${unit}` } -function speedToWords(speed: number): string { +export function speedToWords(speed: number): string { return `${digitsToWords(Math.round(speed).toString())} knots` } -function altitudeToWords(value: number): string { +export function altitudeToWords(value: number): string { const thousands = Math.floor(value / 1000) const remainder = value % 1000 let words = thousands ? `${digitsToWords(thousands.toString())} thousand` : '' @@ -491,18 +491,38 @@ function cloneScenario(scenario: Scenario): Scenario { return JSON.parse(JSON.stringify(scenario)) as Scenario } -export type ScenarioGenerator = (() => Scenario) & { reset: () => void } +export type ScenarioGenerator = (() => Scenario) & { + reset: () => void + setScenario: (scenario: Scenario | null) => void + peek: () => Scenario | null +} -export function createScenarioSeries(): ScenarioGenerator { +export function createScenarioSeries(source?: () => Scenario): ScenarioGenerator { let cached: Scenario | null = null - const generator = () => { + let override: Scenario | null = null + const resolveSource = () => { + if (override) { + return cloneScenario(override) + } + return source ? source() : createBaseScenario() + } + const generator = (() => { if (!cached) { - cached = createBaseScenario() + cached = resolveSource() } return cloneScenario(cached) - } + }) as ScenarioGenerator generator.reset = () => { - cached = null + cached = override ? cloneScenario(override) : null + } + generator.setScenario = (scenario: Scenario | null) => { + override = scenario ? cloneScenario(scenario) : null + cached = override ? cloneScenario(override) : null + } + generator.peek = () => { + if (cached) return cloneScenario(cached) + if (override) return cloneScenario(override) + return null } return generator } diff --git a/shared/learn/types.ts b/shared/learn/types.ts index e957b84..e5a3aad 100644 --- a/shared/learn/types.ts +++ b/shared/learn/types.ts @@ -161,10 +161,16 @@ export type Lesson = { generate: () => Scenario } +export type ModuleMeta = { + flightPlan?: boolean + briefingArt?: string +} + export type ModuleDef = { id: string title: string subtitle: string art: string lessons: Lesson[] + meta?: ModuleMeta } From cc3c9b6655233a1e979b56d2061d2c6f4a863e2e Mon Sep 17 00:00:00 2001 From: itsrubberduck Date: Sun, 21 Sep 2025 17:42:12 +0200 Subject: [PATCH 2/2] add images --- public/img/learn/missions/full-flight/briefing-arrival.png | 4 ++-- public/img/learn/missions/full-flight/briefing-departure.png | 4 ++-- public/img/learn/missions/full-flight/briefing-hero.png | 4 ++-- public/img/learn/missions/full-flight/briefing-route.png | 4 ++-- public/img/learn/missions/full-flight/briefing-weather.png | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/public/img/learn/missions/full-flight/briefing-arrival.png b/public/img/learn/missions/full-flight/briefing-arrival.png index 943fa88..45799b7 100644 --- a/public/img/learn/missions/full-flight/briefing-arrival.png +++ b/public/img/learn/missions/full-flight/briefing-arrival.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffcedec6257e64e91c7f3cbaf37996fbae0a9018058a47ac5d5fa4acbb90c553 -size 68 +oid sha256:fa09610c1ef12eed0ca70878896f5873308ba1ab34b264d0d04d4eadafcfb89a +size 2146902 diff --git a/public/img/learn/missions/full-flight/briefing-departure.png b/public/img/learn/missions/full-flight/briefing-departure.png index 943fa88..a378df8 100644 --- a/public/img/learn/missions/full-flight/briefing-departure.png +++ b/public/img/learn/missions/full-flight/briefing-departure.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffcedec6257e64e91c7f3cbaf37996fbae0a9018058a47ac5d5fa4acbb90c553 -size 68 +oid sha256:40b406d3b31ded0eff41024e33b94d018e1449a0411ade3237e85b98002747ef +size 2249127 diff --git a/public/img/learn/missions/full-flight/briefing-hero.png b/public/img/learn/missions/full-flight/briefing-hero.png index 943fa88..277ee85 100644 --- a/public/img/learn/missions/full-flight/briefing-hero.png +++ b/public/img/learn/missions/full-flight/briefing-hero.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffcedec6257e64e91c7f3cbaf37996fbae0a9018058a47ac5d5fa4acbb90c553 -size 68 +oid sha256:b0284350316cb391bec945b26d754c59fa378cd79b7d5d977c2498b8537121f2 +size 2005788 diff --git a/public/img/learn/missions/full-flight/briefing-route.png b/public/img/learn/missions/full-flight/briefing-route.png index 943fa88..5070adf 100644 --- a/public/img/learn/missions/full-flight/briefing-route.png +++ b/public/img/learn/missions/full-flight/briefing-route.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffcedec6257e64e91c7f3cbaf37996fbae0a9018058a47ac5d5fa4acbb90c553 -size 68 +oid sha256:ac94774ef63657715069069446592220163ae318eef81163e8a1fb34f2c8302a +size 2186393 diff --git a/public/img/learn/missions/full-flight/briefing-weather.png b/public/img/learn/missions/full-flight/briefing-weather.png index 943fa88..41e1233 100644 --- a/public/img/learn/missions/full-flight/briefing-weather.png +++ b/public/img/learn/missions/full-flight/briefing-weather.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffcedec6257e64e91c7f3cbaf37996fbae0a9018058a47ac5d5fa4acbb90c553 -size 68 +oid sha256:188c73efbd184f9a0f1cf4726e0ec62ba61cc482fd2867f70346dd2e86c5bae9 +size 2260863