// Persistent IFPUV validator: keeps ONE browser + the Free Text Editor open and // validates on demand. This drops each validation from ~25 s (relaunch + GWT // load) to ~5-6 s (just refill + revalidate). // // Protocol (line-delimited JSON on stdin/stdout): // stdout `{"ready":true}` — editor loaded, send requests // stdin `{"adep","ades","route","level"[,"speed","actype"]}` — a validation // stdout `{"accepted":bool,"errors":[{code,msg}]}` — its result // stdin `QUIT` — close and exit import { chromium } from "playwright"; import readline from "node:readline"; 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"]'; 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 type = o.actype || "A320"; const level = o.level || "F350"; return ( `(FPL-TEST01-IS\n` + `-${type}/${o.wtc || "M"}-${o.equip || "SDE1E2E3FGHIRWY/LB1"}\n` + `-${o.adep}${o.eobt || "1200"}\n` + `-${o.speed || "N0450"}${level} ${String(o.route).trim()}\n` + `-${o.ades}${o.eet || "0040"}\n` + `-PBN/${o.pbn || "A1B1C1D1O1S2"} REG/${o.reg || "FGKXA"} DOF/${o.dof || tomorrowDOF()})` ); } // Filing-form artifacts (REG/EOBD/EET), not route faults. const HOUSEKEEPING = /^(EFPM(51|234|168|166)|PROF191)$/; function parseErrors(body) { const start = body.indexOf("Validation Results"); const region = start >= 0 ? body.slice(start) : body; const re = /\b([A-Z]{2,6}\d{1,4}):\s*([^\n\t]+)/g; const errors = []; let m; while ((m = re.exec(region))) { if (!HOUSEKEEPING.test(m[1])) errors.push({ code: m[1], msg: m[2].trim() }); } return errors; } const b = await chromium.launch({ headless: true }); const ctx = await b.newContext({ viewport: { width: 1400, height: 950 } }); const page = await ctx.newPage(); 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 }); async function submit(payload) { await ed.locator(ROUTE_SEL).fill(buildFpl(payload)); await ed.locator(VALIDATE_SEL).click({ timeout: 15000 }); await ed.waitForTimeout(5000); const errors = parseErrors(await ed.locator("body").innerText()); return { accepted: errors.length === 0, errors }; } process.stdout.write(JSON.stringify({ ready: true }) + "\n"); const rl = readline.createInterface({ input: process.stdin }); for await (const line of rl) { const t = line.trim(); if (!t) continue; if (t === "QUIT") break; try { const res = await submit(JSON.parse(t)); process.stdout.write(JSON.stringify(res) + "\n"); } catch (e) { process.stdout.write(JSON.stringify({ accepted: false, errors: [], error: e.message }) + "\n"); } } await b.close(); process.exit(0);