import { createLinuxCncIniSdk, createLinuxCncInterpSdk, } from "../../sdk/src/index.js"; import { getOpfsRoot, loadTextFile, saveTextFile, } from "../../opfs/file-service.js"; import { gcodeFilenameFromProgramPath, loadGcodeProgram, loadMachineTextFiles, machineFilePaths, saveGcodeProgram, saveMachineTextFiles, } from "../../opfs/machine-file-store.js"; import { loadMachineSessionFromOpfs, } from "../../opfs/linuxcnc-machine-session-bridge.js"; import { loadMachineSessionSnapshot, saveMachineSessionSnapshot, } from "../../opfs/snapshot-store.js"; import { createMachineFileLoadSummary, createMachineFileSaveSummary, machineFileLoadLogLines, machineFileSaveLogLines, } from "./machine-file-summary.js"; import { createRunSummary, summarizeRunResult, } from "./run-summary.js"; import { createMachineSessionLoadSummary, machineSessionLoadLogLines, } from "./session-load-summary.js"; import { createSessionSnapshotSummary, sessionSnapshotMetadataLogLines, sessionSnapshotSummaryLogLines, } from "./session-summary.js"; const SAMPLE_PATH = "../../../vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-dual-rotary/xyzab-tdr.ini"; const FIVEAXIS_TRT_SOURCE_PATH = "../../../vendor/linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting"; const FIVEAXIS_TRT_WASM_DIR = "/work/ui-fiveaxis/table-rotary-tilting"; const FIVEAXIS_REMAP_FILES = [ "428remap.ngc", "429remap.ngc", "430remap.ngc", "centering.ngc", "helix_ac.ngc", "helix_bc.ngc", "xyzac_switchkins_sub.ngc", "xyzbc_switchkins_sub.ngc", ]; const MACHINE_ID = "xyzab-tdr"; const MACHINE_PATHS = machineFilePaths(MACHINE_ID); const OPFS_FILE = MACHINE_PATHS.ini; const WASM_FILE = "/work/xyzab-tdr.ini"; const SESSION_WASM_FILES = { iniWasmPath: "/work/session-machine.ini", parameterWasmPath: "/work/session-linuxcnc.var", toolTableWasmPath: "/work/session-tool.tbl", }; const GCODE_FILENAME = "ui-session.ngc"; const GCODE_WASM_FILE = "/work/ui-session.ngc"; const UI_SESSION = { snapshotId: "ui-machine-session", snapshotFilename: "ui-machine-session.json", defaultGcodeFilename: GCODE_FILENAME, gcodeWasmPath: GCODE_WASM_FILE, }; const DEFAULT_TOOL_TABLE = "T2 P7 Z3.125 D1.5 I12 J34 Q4 ;ui session tool\n"; const DEFAULT_PARAMETERS = [ "5161 0.0", "5162 0.0", "5220 1", "5221 0.0", "5399 0.0", "", ].join("\n"); const DEFAULT_GCODE = [ "G0 X1.0 Y2.0 (Comment)", "G1 X3.0 Y4.0 F120.0", "", ].join("\n"); const editor = document.getElementById("ini-editor"); const logNode = document.getElementById("log"); const eventNode = document.getElementById("canon-events"); const eventFilterNode = document.getElementById("event-filter"); const eventCountNode = document.getElementById("event-count"); const wasmBadge = document.getElementById("wasm-badge"); const interpBadge = document.getElementById("interp-badge"); const opfsBadge = document.getElementById("opfs-badge"); const runProgressNode = document.getElementById("run-progress"); const runProgressLabelNode = document.getElementById("run-progress-label"); const runMotionNode = document.getElementById("run-motion"); const runLineNode = document.getElementById("run-line"); const runStatementNode = document.getElementById("run-statement"); const axisNodes = { x: document.getElementById("axis-x"), y: document.getElementById("axis-y"), z: document.getElementById("axis-z"), a: document.getElementById("axis-a"), b: document.getElementById("axis-b"), c: document.getElementById("axis-c"), u: document.getElementById("axis-u"), v: document.getElementById("axis-v"), w: document.getElementById("axis-w"), }; const fields = { machine: document.getElementById("field-machine"), display: document.getElementById("field-display"), kinematics: document.getElementById("field-kinematics"), joints: document.getElementById("field-joints"), coordinates: document.getElementById("field-coordinates"), parameterFile: document.getElementById("field-parameter-file"), toolTable: document.getElementById("field-tool-table"), sessionIni: document.getElementById("field-session-ini"), sessionParameters: document.getElementById("field-session-parameters"), sessionToolTable: document.getElementById("field-session-tool-table"), sessionGcode: document.getElementById("field-session-gcode"), sessionSnapshot: document.getElementById("field-session-snapshot"), runStatus: document.getElementById("field-run-status"), runSource: document.getElementById("field-run-source"), runSnapshot: document.getElementById("field-run-snapshot"), runWorkflow: document.getElementById("field-run-workflow"), runOpfsPath: document.getElementById("field-run-opfs-path"), runWasmPath: document.getElementById("field-run-wasm-path"), fiveaxisRemap: document.getElementById("field-fiveaxis-remap"), }; let iniSdk = null; let interpSdk = null; let loadedSession = null; let restoredSessionSnapshot = null; let canonicalEventText = ""; let runPlaybackTimer = null; function setBadge(node, text, className = "badge") { node.className = className; node.textContent = text; } function setLog(message, isError = false) { logNode.textContent = message; logNode.className = `log${isError ? " danger" : ""}`; } function setCanonicalEvents(text) { canonicalEventText = text || ""; renderCanonicalEvents(); } function renderCanonicalEvents() { const lines = canonicalEventText.split("\n").filter(Boolean); const filter = eventFilterNode.value.trim().toLowerCase(); const visibleLines = filter ? lines.filter((line) => line.toLowerCase().includes(filter)) : lines; eventNode.textContent = visibleLines.join("\n"); eventCountNode.textContent = `${visibleLines.length} / ${lines.length}`; } function setField(name, value) { fields[name].textContent = value ?? "-"; } function nextFrame() { return new Promise((resolve) => requestAnimationFrame(resolve)); } function formatAxisValue(value) { return Number.isFinite(value) ? value.toFixed(3) : "0.000"; } function clearRunPlayback() { if (runPlaybackTimer) { clearInterval(runPlaybackTimer); runPlaybackTimer = null; } } function setRunMonitor(progress, label, axes = {}, motionText = "idle", lineText = "line -", statement = "-") { runProgressNode.value = Math.max(0, Math.min(100, progress)); runProgressLabelNode.textContent = label; runMotionNode.textContent = motionText; runLineNode.textContent = lineText; runStatementNode.textContent = statement; for (const [axis, node] of Object.entries(axisNodes)) { node.textContent = formatAxisValue(axes[axis] ?? 0); } } function readCanonicalNumber(line, name) { const match = line.match(new RegExp(`(?:^| )${name}=([-+0-9.eE]+)`)); return match ? Number(match[1]) : null; } function programLineMap(programText) { const lines = new Map(); programText.split(/\r?\n/).forEach((line, index) => { lines.set(index + 1, line.trim() || "(blank)"); }); return lines; } function parseRunMotion(resultText, programText) { const axes = { x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 }; const snapshots = []; const sourceLines = programLineMap(programText); const motionByLine = new Map(); for (const line of resultText.split("\n")) { const motion = line.match(/^canon_event=(STRAIGHT_TRAVERSE|STRAIGHT_FEED|ARC_FEED)\b/); if (!motion) { continue; } const sourceLine = readCanonicalNumber(line, "line"); if (Number.isFinite(sourceLine)) { motionByLine.set(sourceLine, motion[1]); } } for (const line of resultText.split("\n")) { if (!line.startsWith("run_step phase=execute ")) { continue; } for (const axis of Object.keys(axes)) { const value = readCanonicalNumber(line, axis); if (value !== null && Number.isFinite(value)) { axes[axis] = value; } } const sourceLine = readCanonicalNumber(line, "line"); const statementIndex = line.indexOf(" statement_uri="); const statementValue = statementIndex >= 0 ? line.slice(statementIndex + " statement_uri=".length).trim() : ""; let statement = ""; try { statement = statementValue ? decodeURIComponent(statementValue) : ""; } catch { statement = statementValue; } snapshots.push({ type: motionByLine.get(sourceLine) ?? "EXECUTE", line: sourceLine, statement: statement || (Number.isFinite(sourceLine) ? (sourceLines.get(sourceLine) ?? "-") : "-"), axes: { ...axes }, }); } if (snapshots.length > 0) { return snapshots; } for (const line of resultText.split("\n")) { const motion = line.match(/^canon_event=(STRAIGHT_TRAVERSE|STRAIGHT_FEED|ARC_FEED)\b/); if (!motion) { continue; } for (const axis of Object.keys(axes)) { const value = readCanonicalNumber(line, axis); if (value !== null && Number.isFinite(value)) { axes[axis] = value; } } const sourceLine = readCanonicalNumber(line, "line"); snapshots.push({ type: motion[1], line: sourceLine, statement: Number.isFinite(sourceLine) ? (sourceLines.get(sourceLine) ?? "-") : "-", axes: { ...axes }, }); } return snapshots; } function showRunSnapshot(snapshot, index, total) { const progress = total > 0 ? Math.round(((index + 1) / total) * 100) : 0; const lineText = Number.isFinite(snapshot.line) ? `line ${snapshot.line}` : "line -"; setRunMonitor( progress, `${progress}%`, snapshot.axes, `${snapshot.type} ${lineText} (${index + 1}/${total})`, lineText, snapshot.statement, ); } function playRunMotion(resultText, programText, snapshots = parseRunMotion(resultText, programText)) { clearRunPlayback(); if (snapshots.length === 0) { setRunMonitor(100, "100%", {}, "complete; no axis motion"); return; } let index = 0; showRunSnapshot(snapshots[index], index, snapshots.length); runPlaybackTimer = setInterval(() => { index += 1; if (index >= snapshots.length) { clearRunPlayback(); showRunSnapshot(snapshots[snapshots.length - 1], snapshots.length - 1, snapshots.length); return; } showRunSnapshot(snapshots[index], index, snapshots.length); }, 220); } function requireIniSdk() { if (!iniSdk) { throw new Error("INI WASM module is not ready yet."); } return iniSdk; } function requireInterpSdk() { if (!interpSdk) { throw new Error("Interpreter WASM module is not ready yet."); } return interpSdk; } function syncEditorToWasmFs() { requireIniSdk().writeTextFile(WASM_FILE, editor.value); } function sessionSnapshotLabel(snapshot = UI_SESSION) { const sessionId = typeof snapshot === "string" ? snapshot : snapshot.snapshotId ?? snapshot.sessionId; return `${sessionId}/${UI_SESSION.snapshotFilename}`; } function defaultGcodeOpfsPath() { return `linuxcnc/gcode/${UI_SESSION.defaultGcodeFilename}`; } function sessionSnapshotMetadata() { return { source: "ini-panel", workflow: "save-session-snapshot", snapshotLabel: sessionSnapshotLabel(UI_SESSION), defaultGcodeOpfsPath: defaultGcodeOpfsPath(), }; } function sessionMetadataLogLines(metadata = {}) { return sessionSnapshotMetadataLogLines(createSessionSnapshotSummary({ metadata })); } function runSessionContextLogLines(program, snapshot = restoredSessionSnapshot) { return [ `Program: ${program.opfsPath} -> ${program.wasmPath}`, `Program source: ${program.source}`, `Snapshot: ${snapshot ? sessionSnapshotLabel(snapshot.sessionId) : "-"}`, ...sessionMetadataLogLines(snapshot?.metadata), ]; } function clearLastRunSummary() { window.linuxCncIniPanelLastRunSummary = null; setField("runSource", "-"); setField("runSnapshot", "-"); setField("runWorkflow", "-"); setField("runOpfsPath", "-"); setField("runWasmPath", "-"); } function setLastRunSummary(program, runtime = {}, snapshot = restoredSessionSnapshot) { const summary = createRunSummary( program, { ...runtime, snapshotLabel: snapshot ? sessionSnapshotLabel(snapshot.sessionId) : null, }, snapshot, ); window.linuxCncIniPanelLastRunSummary = summary; setField("runSource", summary.programSource); setField("runSnapshot", summary.snapshotLabel ?? "-"); setField("runWorkflow", summary.workflow ?? "-"); setField("runOpfsPath", summary.programOpfsPath); setField("runWasmPath", summary.programWasmPath); return summary; } async function loadSelectedGcodeProgram() { const opfsPath = restoredSessionSnapshot?.payload?.files?.gcode; let filename = UI_SESSION.defaultGcodeFilename; let selectedOpfsPath = defaultGcodeOpfsPath(); let source = "default"; if (typeof opfsPath === "string") { try { filename = gcodeFilenameFromProgramPath(opfsPath); selectedOpfsPath = opfsPath; source = "snapshot"; } catch { filename = UI_SESSION.defaultGcodeFilename; } } return { filename, opfsPath: selectedOpfsPath, source, wasmPath: UI_SESSION.gcodeWasmPath, text: await loadGcodeProgram(filename), }; } async function restoreSessionSnapshotToEditor() { const snapshot = await loadMachineSessionSnapshot( UI_SESSION.snapshotId, MACHINE_ID, { filename: UI_SESSION.snapshotFilename }, ); const files = await loadMachineTextFiles(MACHINE_ID); editor.value = files.ini; setField("sessionIni", snapshot.payload.files.ini); setField("sessionParameters", snapshot.payload.files.parameters); setField("sessionToolTable", snapshot.payload.files.toolTable); setField("sessionGcode", snapshot.payload.files.gcode); setField("sessionSnapshot", sessionSnapshotLabel(snapshot.sessionId)); restoredSessionSnapshot = snapshot; return snapshot; } async function loadMachineSessionIntoWasm() { loadedSession = await loadMachineSessionFromOpfs( requireInterpSdk(), MACHINE_ID, { ...SESSION_WASM_FILES, iniSdk: requireIniSdk(), }, ); setField("sessionIni", loadedSession.ini.wasmPath); setField("sessionParameters", loadedSession.parameters.wasmPath); setField("sessionToolTable", loadedSession.toolTable.wasmPath); return loadedSession; } async function loadSample() { setLog(`Fetching ${SAMPLE_PATH}`); const response = await fetch(SAMPLE_PATH); if (!response.ok) { throw new Error(`Cannot fetch sample INI (${response.status})`); } editor.value = await response.text(); setLog("Loaded LinuxCNC sample INI into the standalone editor."); } async function fetchTextFile(path) { const response = await fetch(path); if (!response.ok) { throw new Error(`Cannot fetch ${path} (${response.status})`); } return response.text(); } async function writeFetchedTextFile(interp, sourcePath, wasmPath) { interp.writeTextFile(wasmPath, await fetchTextFile(sourcePath)); } async function writeFiveAxisTrtMachineFiles(interp) { await writeFetchedTextFile( interp, `${FIVEAXIS_TRT_SOURCE_PATH}/xyzac-trt.ini`, `${FIVEAXIS_TRT_WASM_DIR}/xyzac-trt.ini`, ); await writeFetchedTextFile( interp, `${FIVEAXIS_TRT_SOURCE_PATH}/xyzac-trt.tbl`, `${FIVEAXIS_TRT_WASM_DIR}/xyzac-trt.tbl`, ); for (const filename of FIVEAXIS_REMAP_FILES) { await writeFetchedTextFile( interp, `${FIVEAXIS_TRT_SOURCE_PATH}/remap_subs/${filename}`, `${FIVEAXIS_TRT_WASM_DIR}/remap_subs/${filename}`, ); } await writeFetchedTextFile( interp, `${FIVEAXIS_TRT_SOURCE_PATH}/demos/impeller-7bl-xyzac.ngc`, `${FIVEAXIS_TRT_WASM_DIR}/demos/impeller-7bl-xyzac.ngc`, ); } async function boot() { try { iniSdk = await createLinuxCncIniSdk(); setBadge(wasmBadge, "INI WASM: ready"); } catch (error) { setBadge(wasmBadge, "INI WASM: failed", "badge danger"); setLog(`INI WASM init failed: ${error.message}`, true); throw error; } try { interpSdk = await createLinuxCncInterpSdk(); setBadge(interpBadge, "Interpreter WASM: ready"); } catch (error) { setBadge(interpBadge, "Interpreter WASM: failed", "badge danger"); setLog(`Interpreter WASM init failed: ${error.message}`, true); throw error; } try { await getOpfsRoot(); setBadge(opfsBadge, "OPFS: ready"); } catch (error) { setBadge(opfsBadge, "OPFS: unavailable", "badge danger"); setLog(`OPFS check failed: ${error.message}`, true); } } document.getElementById("load-sample").addEventListener("click", async () => { try { await loadSample(); } catch (error) { setLog(error.message, true); } }); document.getElementById("save-opfs").addEventListener("click", async () => { try { await saveTextFile(OPFS_FILE, editor.value); setLog(`Saved current INI text to OPFS at ${OPFS_FILE}`); } catch (error) { setLog(`OPFS save failed: ${error.message}`, true); } }); document.getElementById("save-machine-files").addEventListener("click", async () => { try { const paths = await saveMachineTextFiles(MACHINE_ID, { ini: editor.value, toolTable: DEFAULT_TOOL_TABLE, parameters: DEFAULT_PARAMETERS, }); const gcodePath = await saveGcodeProgram(UI_SESSION.defaultGcodeFilename, DEFAULT_GCODE); const summary = createMachineFileSaveSummary(paths, gcodePath); setLog( [ "Saved machine text files to OPFS.", ...machineFileSaveLogLines(summary), ].join("\n"), ); } catch (error) { setLog(`Machine file save failed: ${error.message}`, true); } }); document.getElementById("save-session-snapshot").addEventListener("click", async () => { try { await saveMachineTextFiles(MACHINE_ID, { ini: editor.value, toolTable: DEFAULT_TOOL_TABLE, parameters: DEFAULT_PARAMETERS, }); await saveGcodeProgram(UI_SESSION.defaultGcodeFilename, DEFAULT_GCODE); const snapshot = await saveMachineSessionSnapshot( UI_SESSION.snapshotId, MACHINE_ID, { filename: UI_SESSION.snapshotFilename, gcodeFilename: UI_SESSION.defaultGcodeFilename, metadata: sessionSnapshotMetadata(), }, ); setField("sessionIni", snapshot.payload.files.ini); setField("sessionParameters", snapshot.payload.files.parameters); setField("sessionToolTable", snapshot.payload.files.toolTable); setField("sessionGcode", snapshot.payload.files.gcode); setField("sessionSnapshot", sessionSnapshotLabel(UI_SESSION)); const summary = createSessionSnapshotSummary(snapshot, sessionSnapshotLabel(UI_SESSION)); setLog( [ "Saved machine session snapshot to OPFS.", ...sessionSnapshotSummaryLogLines(summary), `INI: ${summary.iniOpfsPath}`, `Parameter file: ${summary.parameterOpfsPath}`, `Tool table file: ${summary.toolTableOpfsPath}`, `G-code: ${summary.gcodeOpfsPath}`, ].join("\n"), ); } catch (error) { setLog(`Machine session snapshot save failed: ${error.message}`, true); } }); document.getElementById("restore-session-snapshot").addEventListener("click", async () => { try { const snapshot = await restoreSessionSnapshotToEditor(); const summary = createSessionSnapshotSummary(snapshot, sessionSnapshotLabel(snapshot.sessionId)); setLog( [ "Restored machine session snapshot from OPFS.", ...sessionSnapshotSummaryLogLines(summary), `INI: ${summary.iniOpfsPath}`, `Parameter file: ${summary.parameterOpfsPath}`, `Tool table file: ${summary.toolTableOpfsPath}`, `G-code: ${summary.gcodeOpfsPath ?? "-"}`, ].join("\n"), ); } catch (error) { setLog(`Machine session snapshot restore failed: ${error.message}`, true); } }); document.getElementById("load-session").addEventListener("click", async () => { try { await loadMachineSessionIntoWasm(); const loadSummary = createMachineSessionLoadSummary(loadedSession); setLog( [ "Loaded machine session into the interpreter WASM filesystem.", ...machineSessionLoadLogLines(loadSummary), ].join("\n"), ); } catch (error) { setLog(`Machine session load failed: ${error.message}`, true); } }); document.getElementById("restore-load-session").addEventListener("click", async () => { try { const snapshot = await restoreSessionSnapshotToEditor(); await loadMachineSessionIntoWasm(); setField("sessionGcode", snapshot.payload.files.gcode); const summary = createSessionSnapshotSummary(snapshot, sessionSnapshotLabel(snapshot.sessionId)); const loadSummary = createMachineSessionLoadSummary(loadedSession); setLog( [ "Restored and loaded machine session snapshot.", ...sessionSnapshotSummaryLogLines(summary), ...machineSessionLoadLogLines(loadSummary), `G-code: ${summary.gcodeOpfsPath ?? "-"}`, ].join("\n"), ); } catch (error) { setLog(`Machine session snapshot restore/load failed: ${error.message}`, true); } }); document.getElementById("run-gcode").addEventListener("click", async () => { try { if (!loadedSession) { throw new Error("Load a machine session before running G-code."); } const interp = requireInterpSdk(); const program = await loadSelectedGcodeProgram(); interp.writeTextFile(program.wasmPath, program.text); clearRunPlayback(); setField("runStatus", "running"); clearLastRunSummary(); setRunMonitor(5, "running", {}, "running"); await nextFrame(); const result = interp.runFileWithIni(program.wasmPath, loadedSession.ini.wasmPath); const trimmedResult = result.trim(); const motionSnapshots = parseRunMotion(trimmedResult, program.text); const runResultSummary = summarizeRunResult(trimmedResult, motionSnapshots); setField("sessionGcode", program.opfsPath); setField("runStatus", "ok"); setLastRunSummary(program, { runStatus: "ok", iniWasmPath: loadedSession.ini.wasmPath, ...runResultSummary, }); setCanonicalEvents(trimmedResult); playRunMotion(trimmedResult, program.text, motionSnapshots); setLog( [ "Ran G-code through LinuxCNC interpreter WASM.", ...runSessionContextLogLines(program), `INI: ${loadedSession.ini.wasmPath}`, trimmedResult, ].join("\n"), ); } catch (error) { setField("runStatus", "failed"); setCanonicalEvents(""); clearRunPlayback(); clearLastRunSummary(); setRunMonitor(0, "failed", {}, "failed"); setLog(`G-code run failed: ${error.message}`, true); } }); document.getElementById("run-fiveaxis-remap").addEventListener("click", async () => { try { const interp = requireInterpSdk(); await writeFiveAxisTrtMachineFiles(interp); const iniPath = `${FIVEAXIS_TRT_WASM_DIR}/xyzac-trt.ini`; const programPath = `${FIVEAXIS_TRT_WASM_DIR}/demos/impeller-7bl-xyzac.ngc`; const result = interp.runFiveAxisRemapFile(programPath, iniPath); setField("fiveaxisRemap", "ok"); setCanonicalEvents(result.trim()); setLog( [ "Ran LinuxCNC 5-axis switchkins remap demo through interpreter WASM.", `Program: ${programPath}`, `INI: ${iniPath}`, result.trim(), ].join("\n"), ); } catch (error) { setField("fiveaxisRemap", "failed"); setCanonicalEvents(""); setLog(`5-axis remap run failed: ${error.message}`, true); } }); document.getElementById("load-opfs").addEventListener("click", async () => { try { editor.value = await loadTextFile(OPFS_FILE); setLog(`Loaded INI text from OPFS path ${OPFS_FILE}`); } catch (error) { setLog(`OPFS load failed: ${error.message}`, true); } }); document.getElementById("sync-wasm").addEventListener("click", () => { try { syncEditorToWasmFs(); setLog(`Synced editor contents to WASM filesystem at ${WASM_FILE}`); } catch (error) { setLog(`WASM sync failed: ${error.message}`, true); } }); document.getElementById("query").addEventListener("click", async () => { try { syncEditorToWasmFs(); const values = requireIniSdk().getFields(WASM_FILE, { machine: { section: "EMC", tag: "MACHINE" }, display: { section: "DISPLAY", tag: "DISPLAY" }, kinematics: { section: "KINS", tag: "KINEMATICS" }, joints: { section: "KINS", tag: "JOINTS" }, coordinates: { section: "TRAJ", tag: "COORDINATES" }, parameterFile: { section: "RS274NGC", tag: "PARAMETER_FILE" }, toolTable: { section: "EMCIO", tag: "TOOL_TABLE" }, }); for (const [name, value] of Object.entries(values)) { setField(name, value); } setLog("Queried LinuxCNC INI fields through the standalone WASM parser."); } catch (error) { setLog(`Query failed: ${error.message}`, true); } }); document.getElementById("load-machine-files").addEventListener("click", async () => { try { const files = await loadMachineTextFiles(MACHINE_ID); editor.value = files.ini; const summary = createMachineFileLoadSummary(files); setLog( [ "Loaded machine text files from OPFS.", ...machineFileLoadLogLines(summary), ].join("\n"), ); } catch (error) { setLog(`Machine file load failed: ${error.message}`, true); } }); eventFilterNode.addEventListener("input", () => { renderCanonicalEvents(); }); boot().catch(() => {});