2cb633e99d
Clean-room reproduction of PFPX: route generation + IFPS validation + OFP. Independent design — official ICAO/EUROCONTROL data only (RAD, FRA points, IFPUV oracle); no community FPL sources. Workspace crates: core (routing/discover/rad/navdata/perf/export), cli, server, rad (Annex parser), gui (Tauri v2 + React + MapLibre). Route discovery: oracle-in-the-loop repair against Eurocontrol IFPUV. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
85 lines
3.3 KiB
JavaScript
85 lines
3.3 KiB
JavaScript
// 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);
|