Files
Alexandre 2cb633e99d Initial commit: offline flight planner (Rust, PFPX-class)
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>
2026-08-21 11:10:35 +02:00

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);