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>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
// Oracle-driven IFPS auto-correction against the public Eurocontrol IFPUV.
|
||||
//
|
||||
// Given a route, repeatedly validate and mechanically fix the tractable error
|
||||
// classes until the plan is IFPS-ACCEPTED (or no further fix applies):
|
||||
// * ROUTE130 UNKNOWN DESIGNATOR X -> drop token X from item 15
|
||||
// * PROF204/205 forbidden FL band -> move the cruise level into the
|
||||
// allowed window (RAD level capping)
|
||||
// One browser session is reused across iterations (fast loop).
|
||||
//
|
||||
// Usage:
|
||||
// node autofix.mjs '{"adep":"LFPG","ades":"EGLL","route":"OPALE DCT KESAX DCT DIMAL DCT ALESO","level":"F360"}'
|
||||
// Prints one JSON line: { accepted, level, route, iterations, log, errors }.
|
||||
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"]';
|
||||
const MAX_ITERS = 8;
|
||||
|
||||
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, route, flNum) {
|
||||
const type = o.actype || "A320";
|
||||
const level = `F${String(flNum).padStart(3, "0")}`;
|
||||
return (
|
||||
`(FPL-TEST01-IS\n` +
|
||||
`-${type}/${o.wtc || "M"}-${o.equip || "SDE1E2E3FGHIRWY/LB1"}\n` +
|
||||
`-${o.adep}${o.eobt || "1200"}\n` +
|
||||
`-${o.speed || "N0450"}${level} ${route.trim()}\n` +
|
||||
`-${o.ades}${o.eet || "0040"}\n` +
|
||||
`-PBN/${o.pbn || "A1B1C1D1O1S2"} REG/${o.reg || "FGKXA"} DOF/${o.dof || tomorrowDOF()})`
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Extract the forbidden FL band(s) from a PROF204/205 message. `Fa..Fb`:
|
||||
// a==0 -> forbidden below b (a floor); b>=600 -> forbidden above a (a ceiling).
|
||||
function bandOf(msg) {
|
||||
const m = msg.match(/F(\d{1,3})\.\.F(\d{3})/);
|
||||
if (!m) return null;
|
||||
const lo = +m[1], hi = +m[2];
|
||||
if (lo === 0) return { floor: hi }; // forbidden 0..hi => must fly above hi
|
||||
return { ceiling: lo }; // Fa..F999 ceiling, or narrow hole Fa..Fb → fly below a
|
||||
}
|
||||
|
||||
// Decide the next fix from the current error set. Returns { route?, fl?, note } or null.
|
||||
function planFix(errors, route, fl) {
|
||||
// 1) unknown designators -> strip the offending token
|
||||
const unknown = errors
|
||||
.filter((e) => e.code === "ROUTE130" && /UNKNOWN DESIGNATOR/i.test(e.msg))
|
||||
.map((e) => e.msg.match(/UNKNOWN DESIGNATOR\s+(\S+)/i)?.[1])
|
||||
.filter(Boolean);
|
||||
if (unknown.length) {
|
||||
const drop = new Set(unknown);
|
||||
const toks = route.split(/\s+/).filter((t) => !drop.has(t));
|
||||
// collapse dangling DCT left by removal
|
||||
const cleaned = toks.filter((t, i) => !(t === "DCT" && (i === 0 || i === toks.length - 1 || toks[i - 1] === "DCT")));
|
||||
return { route: cleaned.join(" "), note: `drop unknown designator(s): ${[...drop].join(", ")}` };
|
||||
}
|
||||
// 2) FL band restrictions -> move into the allowed window
|
||||
let ceiling = Infinity, floor = 0, sawBand = false;
|
||||
for (const e of errors) {
|
||||
if (e.code !== "PROF204" && e.code !== "PROF205") continue;
|
||||
const b = bandOf(e.msg);
|
||||
if (!b) continue;
|
||||
sawBand = true;
|
||||
if (b.ceiling != null) ceiling = Math.min(ceiling, b.ceiling);
|
||||
if (b.floor != null) floor = Math.max(floor, b.floor);
|
||||
}
|
||||
// Lower the level whenever the current FL violates a RAD ceiling — even if
|
||||
// other (non-level) errors remain; clearing the band errors is strict progress
|
||||
// and the loop stops afterwards if nothing else is fixable.
|
||||
if (sawBand && ceiling !== Infinity && fl >= ceiling) {
|
||||
const target = Math.floor((ceiling - 5) / 10) * 10; // highest 10s FL below the cap
|
||||
if (target > floor && target !== fl && target >= 60) {
|
||||
return { fl: target, note: `RAD level cap ${ceiling} -> retry at F${String(target).padStart(3, "0")}` };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── run ────────────────────────────────────────────────────────────────────
|
||||
const o = JSON.parse(process.argv[2] || '{"adep":"LFPG","ades":"EGLL","route":"OPALE DCT KESAX DCT DIMAL DCT ALESO","level":"F360"}');
|
||||
let route = o.route.trim();
|
||||
let fl = parseInt(String(o.level || "F350").replace(/\D/g, ""), 10) || 350;
|
||||
|
||||
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: false, level: null, route: null, iterations: 0, log: [], errors: [], 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 });
|
||||
|
||||
const submit = async (fpl) => {
|
||||
await ed.locator(ROUTE_SEL).fill(fpl);
|
||||
await ed.locator(VALIDATE_SEL).click({ timeout: 15000 });
|
||||
await ed.waitForTimeout(6000);
|
||||
return parseErrors(await ed.locator("body").innerText());
|
||||
};
|
||||
|
||||
// Monotonic auto-correction: only *keep* a change if it strictly reduces the
|
||||
// error count. A fix that makes things worse (e.g. lowering FL into a low-level
|
||||
// TMA full of DCT restrictions) is reverted, and we report the best state seen.
|
||||
// The result is never worse than the input.
|
||||
const fl3 = (n) => `F${String(n).padStart(3, "0")}`;
|
||||
let best = null; // { route, fl, errors }
|
||||
let curRoute = route;
|
||||
let curFl = fl;
|
||||
for (let i = 1; i <= MAX_ITERS; i++) {
|
||||
out.iterations = i;
|
||||
const errors = await submit(buildFpl(o, curRoute, curFl));
|
||||
if (errors.length === 0) {
|
||||
best = { route: curRoute, fl: curFl, errors: [] };
|
||||
out.log.push(`iter ${i}: ${fl3(curFl)} "${curRoute}" -> ACCEPTED`);
|
||||
break;
|
||||
}
|
||||
if (best !== null && errors.length >= best.errors.length) {
|
||||
// The last applied fix didn't help — revert to the best and stop.
|
||||
out.log.push(`iter ${i}: ${fl3(curFl)} -> ${errors.length} err (not better than ${best.errors.length}) — reverting`);
|
||||
break;
|
||||
}
|
||||
best = { route: curRoute, fl: curFl, errors };
|
||||
const fix = planFix(errors, curRoute, curFl);
|
||||
out.log.push(`iter ${i}: ${fl3(curFl)} -> ${errors.length} err [${errors.map((e) => e.code).join(",")}]${fix ? " | fix: " + fix.note : " | no auto-fix"}`);
|
||||
if (!fix) break;
|
||||
if (fix.route != null) curRoute = fix.route;
|
||||
if (fix.fl != null) curFl = fix.fl;
|
||||
}
|
||||
best = best || { route: curRoute, fl: curFl, errors: out.errors };
|
||||
out.accepted = best.errors.length === 0;
|
||||
out.level = fl3(best.fl);
|
||||
out.route = best.route;
|
||||
out.errors = best.errors;
|
||||
} catch (e) {
|
||||
out.error = e.message;
|
||||
} finally {
|
||||
await b.close();
|
||||
}
|
||||
console.log(JSON.stringify(out));
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,38 @@
|
||||
// Explore the public Eurocontrol IFPUV page to find the route input / validate
|
||||
// button / result area. Run: node explore.mjs
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const URL = "https://www.public.nm.eurocontrol.int/PUBPORTAL/gateway/spec/";
|
||||
|
||||
const b = await chromium.launch({ headless: true });
|
||||
const ctx = await b.newContext({ viewport: { width: 1600, height: 1000 } });
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
await page.goto(URL, { waitUntil: "domcontentloaded", timeout: 60000 });
|
||||
await page.waitForTimeout(9000); // let the GWT app render
|
||||
console.log("TITLE:", await page.title());
|
||||
console.log("URL :", page.url());
|
||||
|
||||
const dump = await page.$$eval(
|
||||
"input, textarea, button, a, span, div[role], [class*='gwt'], [class*='Button']",
|
||||
(nodes) =>
|
||||
nodes
|
||||
.map((n) => ({
|
||||
tag: n.tagName,
|
||||
type: n.getAttribute("type") || "",
|
||||
id: n.id || "",
|
||||
cls: (n.className && typeof n.className === "string" ? n.className : "").slice(0, 40),
|
||||
text: (n.innerText || n.value || "").trim().slice(0, 50),
|
||||
}))
|
||||
.filter((e) => e.text && e.text.length > 1)
|
||||
.slice(0, 120),
|
||||
);
|
||||
console.log(JSON.stringify(dump, null, 0));
|
||||
|
||||
await page.screenshot({ path: "ifpuv.png", fullPage: true });
|
||||
console.log("screenshot -> ifpuv.png");
|
||||
} catch (e) {
|
||||
console.error("ERR:", e.message);
|
||||
} finally {
|
||||
await b.close();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Open the IFPUV "Free Text Editor" from the public NOP portal and dump its form.
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const URL = "https://www.public.nm.eurocontrol.int/PUBPORTAL/gateway/spec/";
|
||||
const b = await chromium.launch({ headless: true });
|
||||
const ctx = await b.newContext({ viewport: { width: 1500, height: 950 } });
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
await page.goto(URL, { waitUntil: "domcontentloaded", timeout: 60000 });
|
||||
await page.waitForTimeout(8000);
|
||||
|
||||
// Click the "Free Text Editor" link; it may open a popup.
|
||||
const popupP = ctx.waitForEvent("page", { timeout: 15000 }).catch(() => null);
|
||||
await page.getByText("Free Text Editor", { exact: false }).first().click({ timeout: 15000 });
|
||||
const popup = await popupP;
|
||||
const target = popup || page;
|
||||
await target.waitForTimeout(9000);
|
||||
await target.bringToFront?.();
|
||||
|
||||
console.log("EDITOR TITLE:", await target.title());
|
||||
console.log("EDITOR URL :", target.url());
|
||||
|
||||
const dump = await target.$$eval(
|
||||
"textarea, input, button, [role=button], .gwt-Button, span",
|
||||
(nodes) =>
|
||||
nodes
|
||||
.map((n) => ({
|
||||
tag: n.tagName,
|
||||
type: n.getAttribute("type") || "",
|
||||
id: n.id || "",
|
||||
cls: (typeof n.className === "string" ? n.className : "").slice(0, 45),
|
||||
text: (n.innerText || n.value || n.placeholder || "").trim().slice(0, 45),
|
||||
}))
|
||||
.filter((e) => e.tag === "TEXTAREA" || e.tag === "INPUT" || (e.text && e.text.length > 1))
|
||||
.slice(0, 80),
|
||||
);
|
||||
console.log(JSON.stringify(dump, null, 0));
|
||||
await target.screenshot({ path: "editor.png", fullPage: true });
|
||||
console.log("screenshot -> editor.png");
|
||||
} catch (e) {
|
||||
console.error("ERR:", e.message);
|
||||
} finally {
|
||||
await b.close();
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 744 KiB |
Generated
+59
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "ifpuv-scraper",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ifpuv-scraper",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"playwright": "^1.48.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
||||
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.62.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
||||
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "ifpuv-scraper",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"playwright": "^1.48.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
@@ -0,0 +1,84 @@
|
||||
// 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);
|
||||
@@ -0,0 +1,96 @@
|
||||
// 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));
|
||||
Reference in New Issue
Block a user