Files

243 lines
8.2 KiB
JavaScript

#!/usr/bin/env node
/**
* UI-level diagnosis for motor console (no backend-side health APIs as truth).
* Launches Edge against :2625/pages/motor.html, dumps what the page SHOWS.
* Usage: node scripts/ui_diag_motor.mjs [port]
*/
import { spawn } from "node:child_process";
import { writeFileSync, mkdirSync } from "node:fs";
import path from "node:path";
const PORT = Number(process.argv[2] || 2625);
const PAGE = `http://127.0.0.1:${PORT}/pages/motor.html`;
const CDP = "http://127.0.0.1:9333";
const OUT = path.resolve("output/ui_diag");
mkdirSync(OUT, { recursive: true });
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function findWs() {
for (let i = 0; i < 40; i++) {
try {
const list = await (await fetch(CDP + "/json/list")).json();
const page = list.find((t) => t.type === "page" && t.url.includes("motor"));
if (page && page.webSocketDebuggerUrl) return page.webSocketDebuggerUrl;
const any = list.find((t) => t.type === "page");
if (any && any.webSocketDebuggerUrl) return any.webSocketDebuggerUrl;
} catch (_) {}
await sleep(250);
}
throw new Error("CDP not available on 9333");
}
const edgeCandidates = [
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
];
function launchEdge() {
const exe = edgeCandidates.find((p) => p && require("fs").existsSync(p)) || "msedge.exe";
// prefer spawn without require - use existsSync from fs
return spawn(
edgeCandidates.find((p) => {
try {
return require("node:fs").existsSync(p);
} catch {
return false;
}
}) || "msedge.exe",
[
"--headless=new",
"--disable-gpu",
`--remote-debugging-port=9333`,
"--user-data-dir=" + path.join(OUT, "edge-profile"),
"--no-first-run",
"--no-default-browser-check",
PAGE,
],
{ stdio: "ignore", detached: true }
);
}
import { existsSync } from "node:fs";
function launchEdge2() {
const exe =
edgeCandidates.find((p) => existsSync(p)) || "msedge.exe";
const child = spawn(
exe,
[
"--headless=new",
"--disable-gpu",
"--remote-debugging-port=9333",
`--user-data-dir=${path.join(OUT, "edge-profile")}`,
"--no-first-run",
"--no-default-browser-check",
PAGE,
],
{ stdio: "ignore", detached: true }
);
child.unref();
return child;
}
const url = await (async () => {
try {
await fetch(CDP + "/json/list");
} catch {
console.log("launching Edge headless →", PAGE);
launchEdge2();
await sleep(2000);
}
return findWs();
})();
console.log("CDP", url);
const sock = new WebSocket(url);
await new Promise((r, j) => {
sock.onopen = r;
sock.onerror = j;
});
let id = 0;
const pending = new Map();
const consoleMsgs = [];
sock.onmessage = (ev) => {
const m = JSON.parse(ev.data);
if (m.id && pending.has(m.id)) {
pending.get(m.id)(m);
pending.delete(m.id);
}
if (m.method === "Runtime.consoleAPICalled") {
const args = (m.params.args || [])
.map((a) => a.value ?? a.description ?? a.unserializableValue ?? "")
.join(" ");
consoleMsgs.push(`[${m.params.type}] ${args}`);
}
if (m.method === "Runtime.exceptionThrown") {
const d = m.params.exceptionDetails || {};
consoleMsgs.push(
`[exception] ${d.text || ""} ${d.exception && d.exception.description ? d.exception.description : ""}`
);
}
if (m.method === "Log.entryAdded") {
const e = m.params.entry || {};
consoleMsgs.push(`[log:${e.level}] ${e.text}`);
}
};
function send(method, params = {}) {
const myId = ++id;
return new Promise((res) => {
pending.set(myId, res);
sock.send(JSON.stringify({ id: myId, method, params }));
});
}
async function evaluate(expr) {
const r = await send("Runtime.evaluate", {
expression: expr,
awaitPromise: true,
returnByValue: true,
});
if (r.result && r.result.exceptionDetails) {
return {
__err: JSON.stringify(r.result.exceptionDetails).slice(0, 500),
};
}
return r.result && r.result.result && r.result.result.value;
}
await send("Runtime.enable");
await send("Log.enable");
await send("Page.enable");
await send("Page.navigate", { url: PAGE });
await sleep(4000);
const report = await evaluate(`(() => {
const $ = (s) => document.querySelector(s);
const txt = (s) => { const e=$(s); return e ? (e.textContent||'').trim() : null; };
const cls = (s) => { const e=$(s); return e ? e.className : null; };
const nodes = [...document.querySelectorAll('#nodeList .node-item, #nodeList .node, #nodeList [data-node], #nodeList button, #nodeList .node-row')].map(e => ({
tag: e.tagName,
cls: e.className,
text: (e.textContent||'').trim().slice(0,80),
html: (e.innerHTML||'').slice(0,120)
}));
const statusPanel = $('#statusPanel, .status-panel, #statusBody');
const sb = {};
for (const id of ['sbLastSend','sbRx','sbTx','sbErr','devStatus','ipLabel','connLed','nodeBadge','netBannerText','refreshRate']) {
const e = document.getElementById(id);
sb[id] = e ? {text:(e.textContent||'').trim().slice(0,80), cls:e.className, value:e.value??null, title:e.title||null} : null;
}
let dbg = null;
try {
const s = window.motorDbg && window.motorDbg.state;
if (s) {
dbg = {
connected: s.connected,
connecting: s.connecting,
probing: s.probing,
tpId: s.tpId,
ip: s.ip,
probeErr: s.probeErr,
probeErrFull: (s.probeErrFull||'').slice(0,200),
failCount: s.failCount,
refreshIntervalMs: s.refreshIntervalMs,
motorsLen: (s.motors||[]).length,
motorsSample: (s.motors||[]).slice(0,3).map(m=>({ch:m.ch,node:m.node,online:m.online,dlc:m.dlc,dataLen:(m.data||'').length})),
chOnline: s.chOnline,
chEnabled: s.chEnabled,
selectedNode: s.selectedNode,
decodedSize: s.decoded && s.decoded.size,
recvTsLen: (s.recvTs||[]).length,
tickStuck: s.tickStuck,
txCount: s.txCount,
transportHas: !!(s.tp),
transportId: s.tp && s.tp.id,
lastConnToastAt: s.lastConnToastAt,
};
}
} catch(e) { dbg = {err:String(e)}; }
// overlay / empty states visible to user
const overlays = [...document.querySelectorAll('.offline-overlay,.empty-state,.status-offline,#statusEmpty,.panel-empty')]
.map(e => ({cls:e.className, text:(e.textContent||'').trim().slice(0,100), hidden:e.hidden, display:getComputedStyle(e).display}));
// any toast visible
const toasts = [...document.querySelectorAll('#toastContainer .toast, .toast')]
.map(e => ({cls:e.className, text:(e.textContent||'').trim().slice(0,80)}));
return {
title: document.title,
url: location.href,
readyState: document.readyState,
bodyClass: document.body && document.body.className,
htmlData: document.documentElement.dataset,
toolbar: {
brand: txt('.toolbar-brand span'),
sub: txt('.toolbar-brand .sub'),
devStatus: txt('#devStatus'),
connLedCls: cls('#connLed'),
ipLabel: txt('#ipLabel'),
transportSelect: document.querySelector('#transportSelect, #linkSelect, select[id*=transport], select[id*=link]')?.value,
transportOptions: [...document.querySelectorAll('select option')].map(o=>o.value+':'+o.textContent.trim()).slice(0,12),
},
sb,
nodeListCount: nodes.length,
nodes,
statusPanelText: statusPanel ? (statusPanel.textContent||'').trim().slice(0,400) : null,
overlays,
toasts,
dbg,
scripts: [...document.scripts].map(s=>s.src).filter(Boolean),
};
})()`);
const consoleTail = consoleMsgs.slice(-40);
const dump = { when: new Date().toISOString(), PAGE, report, consoleTail };
const outFile = path.join(OUT, "ui_diag.json");
writeFileSync(outFile, JSON.stringify(dump, null, 2), "utf8");
console.log("WROTE", outFile);
console.log(JSON.stringify({ title: report && report.title, toolbar: report && report.toolbar, sb: report && report.sb, nodeListCount: report && report.nodeListCount, nodes: report && report.nodes, overlays: report && report.overlays, toasts: report && report.toasts, dbg: report && report.dbg, scripts: report && report.scripts, consoleTail }, null, 2));
if (report && report.__err) console.log("EVAL_ERR", report.__err);
sock.close();
process.exit(0);