// Validate a flight plan against the public Eurocontrol IFPUV (guest access). // // Usage: // node validate.mjs '{"adep":"LFPG","ades":"EGLL","route":"OPALE DCT KESAX DCT DIMAL DCT ALESO","actype":"A320","level":"F360","speed":"N0450"}' // node validate.mjs --raw "(FPL-...-...)" // // Prints one JSON line: { accepted, errors:[{code,msg}], warnings:[...], raw }. import { chromium } from "playwright"; const URL = "https://www.public.nm.eurocontrol.int/PUBPORTAL/gateway/spec/"; const ROUTE_SEL = '[id="FREE_TEXT_EDITOR.FLIGHT_DATA_AREA.GENERAL_DATA_ENTRY.INTRODUCE_FLIGHT_PLAN_FIELD"]'; const VALIDATE_SEL = '[id="FREE_TEXT_EDITOR.FLIGHT_DATA_AREA.VALIDATE_ACTION_LABEL"]'; // --- build the ICAO FPL free-text ----------------------------------------- function tomorrowDOF() { const d = new Date(Date.now() + 24 * 3600 * 1000); const p = (n) => String(n).padStart(2, "0"); return `${String(d.getUTCFullYear()).slice(2)}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}`; } function buildFpl(o) { const cs = o.callsign || o.reg?.replace("-", "") || "TEST01"; const type = o.actype || "A320"; const wtc = o.wtc || "M"; const equip = o.equip || "SDE1E2E3FGHIRWY/LB1"; const reg = o.reg || "FGKXA"; const pbn = o.pbn || "A1B1C1D1O1S2"; const eet = o.eet || "0040"; const altn = o.altn ? ` ${o.altn}` : ""; const item15 = `${o.speed || "N0450"}${o.level || "F360"} ${o.route.trim()}`; return ( `(FPL-${cs}-IS\n` + `-${type}/${wtc}-${equip}\n` + `-${o.adep}${o.eobt || "1200"}\n` + `-${item15}\n` + `-${o.ades}${eet}${altn}\n` + `-PBN/${pbn} REG/${reg} DOF/${o.dof || tomorrowDOF()})` ); } // --- parse the "Validation Results" text ---------------------------------- function parseResults(body) { const start = body.indexOf("Validation Results"); const region = start >= 0 ? body.slice(start) : body; const codeRe = /\b([A-Z]{2,6}\d{1,4}):\s*([^\n\t]+)/g; const all = []; let m; while ((m = codeRe.exec(region))) all.push({ code: m[1], msg: m[2].trim() }); // Filing-form artifacts (REG/EOBD/EET fields we'd fill correctly in a real // filing), not route faults. PROF191 = our fixed EET differs from the calc. const housekeeping = /^(EFPM(51|234|168|166)|PROF191)$/; const errors = all.filter((e) => !housekeeping.test(e.code)); return { errors, all }; } // --- run ------------------------------------------------------------------- const args = process.argv.slice(2); let fpl; if (args[0] === "--raw") { fpl = args.slice(1).join(" "); } else { const payload = JSON.parse(args[0] || '{"adep":"LFPG","ades":"EGLL","route":"OPALE DCT KESAX DCT DIMAL DCT ALESO"}'); fpl = buildFpl(payload); } const b = await chromium.launch({ headless: true }); const ctx = await b.newContext({ viewport: { width: 1400, height: 950 } }); const page = await ctx.newPage(); const out = { accepted: null, errors: [], raw: "", fpl, error: null }; try { await page.goto(URL, { waitUntil: "domcontentloaded", timeout: 60000 }); await page.waitForTimeout(8000); const popupP = ctx.waitForEvent("page", { timeout: 15000 }).catch(() => null); await page.getByText("Free Text Editor", { exact: false }).first().click({ timeout: 15000 }); const ed = (await popupP) || page; await ed.waitForSelector(ROUTE_SEL, { timeout: 20000 }); await ed.locator(ROUTE_SEL).fill(fpl); await ed.locator(VALIDATE_SEL).click({ timeout: 15000 }); await ed.waitForTimeout(7000); const body = await ed.locator("body").innerText(); const { errors, all } = parseResults(body); out.errors = errors; out.accepted = errors.length === 0; const s = body.indexOf("Validated ICAO FPL"); out.raw = (s >= 0 ? body.slice(s) : body).replace(/\n{3,}/g, "\n").trim().slice(0, 2500); await ed.screenshot({ path: "result.png", fullPage: true }); } catch (e) { out.error = e.message; } finally { await b.close(); } console.log(JSON.stringify(out));