diff --git a/text11.txt b/text11.txt index ff3701d..c737b43 100644 --- a/text11.txt +++ b/text11.txt @@ -1203,3 +1203,68 @@ host_wasm_opfs_browser_smokes=ok `control-page.html/js` 的页面消费逻辑抽成更小的页面 controller helper,或者直接加一个 只读导航入口页,把 `index.html` 与 `control-page.html` 作为并列入口统一暴露;仍保持 只读,不新增控制按钮,不解析 G-code。 + +十四、2026-06-14 继续执行记录:INI panel control page controller helper + +本轮按“加快实质性推进”把 control page 的页面消费逻辑抽成可复用 controller helper, +让只读控制页入口不再把 iframe/API 轮询和渲染逻辑内联在页面脚本里。 + +完成内容: + +- 新增 `wasm-port/runtime/ui/ini-panel/control-page-controller.js`; +- 导出 `createMachineSessionControlPageController(input)`; +- controller 负责: + - 读取隐藏 iframe 的 `linuxCncIniPanelApi`; + - 调用 `getMachineSessionControlView()`; + - 渲染 waiting/status; + - 用注入的 renderer 渲染 control view; + - 启动轮询; + - 挂载 panel iframe; +- `wasm-port/runtime/ui/ini-panel/control-page.js` 收敛为 DOM 绑定层; +- 新增 `wasm-port/tests/ui/node/verify_ini_panel_control_page_controller.mjs`; +- 新增 `wasm-port/tests/ui/node/verify_ini_panel_control_page_controller.sh`; +- `wasm-port/tests/ui/node/verify_ui_node_smokes.sh` 纳入 controller node smoke; +- `wasm-port/docs/ui-panel-state-summary.md` 补充 controller helper 说明; +- 未新增控制按钮; +- 未解析 G-code; +- 未解释 canonical events; +- 未改变 OPFS/session persistence、LinuxCNC interpreter、tool、parameter、planner 或 + LinuxCNC-owned runtime semantics。 + +验证已通过: + +```bash +git diff --check +wasm-port/tests/ui/node/verify_ini_panel_control_page_controller.sh +wasm-port/tests/ui/node/verify_ui_node_smokes.sh +wasm-port/tests/browser/verify_ini_panel_browser.sh +wasm-port/tools/verify_vendor_sync.sh +wasm-port/tools/verify_no_standalone_cnc_semantics.sh +SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_interp_wasm.sh +SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_sim_configs_inventory_wasm.sh +wasm-port/tests/host/verify_host_smokes.sh +``` + +关键输出: + +```text +ini_panel_control_page_controller_node_smoke=ok +ui_node_smokes=ok +browser_ini_opfs_smoke=ok +browser_ini_control_page_smoke=ok +vendor sync up to date +standalone CNC semantics guard complete +interp_wasm_node_smoke=ok +sim_configs_wasm_node_inventory_executed=28 +sim_configs_wasm_node_inventory_passed=28 +sim_configs_wasm_node_inventory_skipped=131 +sim_configs_wasm_node_inventory_unexpected_fail=0 +host_wasm_opfs_browser_smokes=ok +``` + +下一步建议: + +继续保持只读边界,下一批建议新增一个极薄的 UI entry index,例如 +`runtime/ui/ini-panel/launch.html`,把编辑/运行面板 `index.html` 与只读控制页 +`control-page.html` 作为并列入口暴露,并用 browser smoke 验证两个入口链接可达;仍不新增 +控制按钮,不解析 G-code。 diff --git a/wasm-port/docs/ui-panel-state-summary.md b/wasm-port/docs/ui-panel-state-summary.md index e94332d..4a25ba6 100644 --- a/wasm-port/docs/ui-panel-state-summary.md +++ b/wasm-port/docs/ui-panel-state-summary.md @@ -29,6 +29,9 @@ renders the current view into a supplied DOM container. For a ready-made read-only page, open `runtime/ui/ini-panel/control-page.html`. It embeds the panel in a hidden iframe and mirrors the same control view into the visible page. +The page uses `createMachineSessionControlPageController(...)` from +`runtime/ui/ini-panel/control-page-controller.js` so other read-only shells can +reuse the same iframe/API polling contract. ## `createIniPanelStateSummary(input)` diff --git a/wasm-port/runtime/ui/ini-panel/control-page-controller.js b/wasm-port/runtime/ui/ini-panel/control-page-controller.js new file mode 100644 index 0000000..933867c --- /dev/null +++ b/wasm-port/runtime/ui/ini-panel/control-page-controller.js @@ -0,0 +1,80 @@ +export function createMachineSessionControlPageController({ + panelFrame, + statusNode, + viewNode, + renderControlView, + pollMs = 200, + setIntervalFn = globalThis.setInterval, +} = {}) { + if (!panelFrame) { + throw new Error("A panel iframe is required."); + } + if (!statusNode) { + throw new Error("A control page status node is required."); + } + if (!viewNode) { + throw new Error("A control page view node is required."); + } + if (typeof renderControlView !== "function") { + throw new Error("A control view renderer is required."); + } + + let refreshTimer = null; + + function getPanelWindow() { + return panelFrame.contentWindow ?? null; + } + + function getPanelApi() { + return getPanelWindow()?.linuxCncIniPanelApi ?? null; + } + + function getControlView() { + return getPanelApi()?.getMachineSessionControlView?.() ?? null; + } + + function render() { + const view = getControlView(); + if (!view) { + statusNode.textContent = "waiting for panel"; + viewNode.textContent = ""; + return null; + } + + statusNode.textContent = [ + view.status.lastAction ?? "-", + view.status.runStatus ?? "-", + view.status.error ?? "-", + ].join(" | "); + return renderControlView(viewNode, view); + } + + function startPolling() { + if (!refreshTimer) { + refreshTimer = setIntervalFn(render, pollMs); + } + return refreshTimer; + } + + function mount({ panelSrc = "./index.html" } = {}) { + panelFrame.addEventListener("load", () => { + render(); + startPolling(); + }); + panelFrame.src = panelSrc; + render(); + startPolling(); + return controller; + } + + const controller = { + getControlView, + getPanelApi, + getPanelWindow, + mount, + render, + startPolling, + }; + + return controller; +} diff --git a/wasm-port/runtime/ui/ini-panel/control-page.js b/wasm-port/runtime/ui/ini-panel/control-page.js index 1840de6..ac888d8 100644 --- a/wasm-port/runtime/ui/ini-panel/control-page.js +++ b/wasm-port/runtime/ui/ini-panel/control-page.js @@ -1,58 +1,25 @@ import { renderMachineSessionControlView, } from "./control-view-renderer.js"; +import { + createMachineSessionControlPageController, +} from "./control-page-controller.js"; const panelFrameNode = document.getElementById("panel-frame"); const statusNode = document.getElementById("control-page-status"); const viewNode = document.getElementById("control-page-view"); -let refreshTimer = null; - -function getPanelWindow() { - return panelFrameNode?.contentWindow ?? null; -} - -function getPanelApi() { - return getPanelWindow()?.linuxCncIniPanelApi ?? null; -} - -function getControlView() { - return getPanelApi()?.getMachineSessionControlView?.() ?? null; -} - -function renderControlPage() { - const view = getControlView(); - if (!view) { - statusNode.textContent = "waiting for panel"; - viewNode.textContent = ""; - return null; - } - - statusNode.textContent = [ - view.status.lastAction ?? "-", - view.status.runStatus ?? "-", - view.status.error ?? "-", - ].join(" | "); - return renderMachineSessionControlView(viewNode, view); -} - -function startPolling() { - if (!refreshTimer) { - refreshTimer = setInterval(renderControlPage, 200); - } -} - -panelFrameNode.addEventListener("load", () => { - renderControlPage(); - startPolling(); +const controller = createMachineSessionControlPageController({ + panelFrame: panelFrameNode, + statusNode, + viewNode, + renderControlView: renderMachineSessionControlView, }); -panelFrameNode.src = "./index.html"; -renderControlPage(); -startPolling(); +controller.mount(); window.linuxCncIniPanelControlPageApi = { - getControlView, - getPanelApi, - renderControlPage, + getControlView: controller.getControlView, + getPanelApi: controller.getPanelApi, + renderControlPage: controller.render, }; diff --git a/wasm-port/tests/ui/node/verify_ini_panel_control_page_controller.mjs b/wasm-port/tests/ui/node/verify_ini_panel_control_page_controller.mjs new file mode 100644 index 0000000..af374a8 --- /dev/null +++ b/wasm-port/tests/ui/node/verify_ini_panel_control_page_controller.mjs @@ -0,0 +1,105 @@ +import assert from "node:assert/strict"; + +import { + createMachineSessionControlPageController, +} from "../../../runtime/ui/ini-panel/control-page-controller.js"; + +function createPanelFrame(panelApi = null) { + const listeners = new Map(); + return { + contentWindow: panelApi ? { linuxCncIniPanelApi: panelApi } : null, + src: null, + addEventListener(type, listener) { + listeners.set(type, listener); + }, + dispatch(type) { + listeners.get(type)?.(); + }, + }; +} + +const rendered = []; +const viewNode = { textContent: "" }; +const statusNode = { textContent: "" }; +const expectedView = { + status: { + lastAction: "run-gcode", + runStatus: "ok", + error: null, + }, + sections: [ + { + id: "run", + rows: [ + { label: "Canonical events", value: 2 }, + ], + }, + ], +}; + +let intervalCallback = null; +const panelFrame = createPanelFrame(); +const controller = createMachineSessionControlPageController({ + panelFrame, + statusNode, + viewNode, + pollMs: 250, + setIntervalFn: (callback, ms) => { + intervalCallback = callback; + return `timer:${ms}`; + }, + renderControlView: (container, view) => { + rendered.push({ container, view }); + return { statusNode: { textContent: "rendered" } }; + }, +}); + +assert.equal(controller.getPanelWindow(), null); +assert.equal(controller.getPanelApi(), null); +assert.equal(controller.getControlView(), null); +assert.equal(controller.render(), null); +assert.equal(statusNode.textContent, "waiting for panel"); +assert.equal(viewNode.textContent, ""); +assert.equal(controller.startPolling(), "timer:250"); +assert.equal(controller.startPolling(), "timer:250"); +assert.equal(typeof intervalCallback, "function"); + +const panelApi = { + getMachineSessionControlView: () => expectedView, +}; +panelFrame.contentWindow = { linuxCncIniPanelApi: panelApi }; + +assert.equal(controller.getPanelApi(), panelApi); +assert.equal(controller.getControlView(), expectedView); +const renderResult = controller.render(); +assert.deepEqual(renderResult, { statusNode: { textContent: "rendered" } }); +assert.equal(statusNode.textContent, "run-gcode | ok | -"); +assert.deepEqual(rendered, [{ container: viewNode, view: expectedView }]); + +const mountedFrame = createPanelFrame(panelApi); +const mountedStatusNode = { textContent: "" }; +const mountedViewNode = { textContent: "" }; +let mountedIntervalMs = null; +const mountedController = createMachineSessionControlPageController({ + panelFrame: mountedFrame, + statusNode: mountedStatusNode, + viewNode: mountedViewNode, + setIntervalFn: (_callback, ms) => { + mountedIntervalMs = ms; + return "mounted-timer"; + }, + renderControlView: () => "mounted-render", +}); + +assert.equal(mountedController.mount({ panelSrc: "./custom-panel.html" }), mountedController); +assert.equal(mountedFrame.src, "./custom-panel.html"); +assert.equal(mountedIntervalMs, 200); +mountedFrame.dispatch("load"); +assert.equal(mountedStatusNode.textContent, "run-gcode | ok | -"); + +assert.throws( + () => createMachineSessionControlPageController(), + /panel iframe/, +); + +console.log("ini_panel_control_page_controller_node_smoke=ok"); diff --git a/wasm-port/tests/ui/node/verify_ini_panel_control_page_controller.sh b/wasm-port/tests/ui/node/verify_ini_panel_control_page_controller.sh new file mode 100755 index 0000000..fcbc108 --- /dev/null +++ b/wasm-port/tests/ui/node/verify_ini_panel_control_page_controller.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)" + +node "$ROOT_DIR/tests/ui/node/verify_ini_panel_control_page_controller.mjs" diff --git a/wasm-port/tests/ui/node/verify_ui_node_smokes.sh b/wasm-port/tests/ui/node/verify_ui_node_smokes.sh index f2aef26..3584290 100755 --- a/wasm-port/tests/ui/node/verify_ui_node_smokes.sh +++ b/wasm-port/tests/ui/node/verify_ui_node_smokes.sh @@ -9,5 +9,6 @@ ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)" "$ROOT_DIR/tests/ui/node/verify_ini_panel_session_workflow.sh" "$ROOT_DIR/tests/ui/node/verify_ini_panel_machine_file_summary.sh" "$ROOT_DIR/tests/ui/node/verify_ini_panel_state_summary.sh" +"$ROOT_DIR/tests/ui/node/verify_ini_panel_control_page_controller.sh" echo "ui_node_smokes=ok"