推进 INI 面板 readonly status API

This commit is contained in:
2026-06-15 05:40:41 +08:00
parent deb8b7cdd3
commit 3280f3a53e
11 changed files with 302 additions and 1 deletions

View File

@@ -587,3 +587,75 @@ host_wasm_opfs_browser_smokes=ok
和 `run` 的状态入口再向上收拢成一个稳定的只读状态 API优先服务 control page 和 shell 的 和 `run` 的状态入口再向上收拢成一个稳定的只读状态 API优先服务 control page 和 shell 的
统一读取;仍保持只描述 UI/host boundary facts不在 JS 中解释 G-code、canonical events 或 统一读取;仍保持只描述 UI/host boundary facts不在 JS 中解释 G-code、canonical events 或
CNC 运行语义。 CNC 运行语义。
六、2026-06-15 继续执行记录INI panel readonly status API
本轮按“第一原则:加快实质性推进”继续把 control page / shell 的状态入口向上收拢,新增稳定的
read-only status API让外层 UI 能一次读取 state bundle、workflow status、overview、control view、
session、run readiness 和 run 结果。
完成内容:
- 新增 `wasm-port/runtime/ui/ini-panel/readonly-status-api.js`
- 新增 `createMachineSessionReadonlyStatus(input)`,固定只读状态对象 shape
- 新增 `readMachineSessionReadonlyStatus({ getPanelApi, getControlView })`,通过现有 panel API
读取 `stateBundle`、`workflowStatus` 和 `controlView`
- `createMachineSessionControlPageController()` 新增 `readStatus()`
- `ui-shell.js` re-export readonly status API
- `createIniPanelShellViewModel()` 的 `controlPage` 新增 `readStatus`
- 新增 `wasm-port/tests/ui/node/verify_ini_panel_readonly_status_api.mjs` 和 `.sh`
- `verify_ui_node_smokes.sh` 纳入 readonly status API smoke
- `verify_ini_panel_control_page_controller.mjs` 覆盖 controller `readStatus()`
- `verify_ini_panel_ui_shell.mjs` 覆盖 shell export、view-model 和 controller 读取路径;
- `verify_ini_panel_entry_docs.mjs` 与 `docs/panel-entry.md` 同步记录新 API
- 未新增控制按钮;
- 未解析 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_readonly_status_api.sh
wasm-port/tests/ui/node/verify_ini_panel_control_page_controller.sh
wasm-port/tests/ui/node/verify_ini_panel_ui_shell.sh
wasm-port/tests/ui/node/verify_ini_panel_entry_docs.sh
wasm-port/tests/ui/node/verify_ui_node_smokes.sh
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 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_readonly_status_api_node_smoke=ok
ini_panel_control_page_controller_node_smoke=ok
ini_panel_ui_shell_node_smoke=ok
ini_panel_entry_docs_node_smoke=ok
ui_node_smokes=ok
browser_ini_opfs_smoke=ok
browser_ini_control_page_smoke=ok
browser_ini_launch_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
browser_interp_smoke=ok
host_wasm_opfs_browser_smokes=ok
```
下一步建议:
继续推进 SDK/API surface。下一批建议把 readonly status API 的 shape 写入一个轻量 SDK-facing
入口或 manifest方便外部页面稳定发现 `readStatus()`、`loadSessionState()`、`refresh()` 和
`renderView()`;仍保持只描述 UI/host boundary facts不在 JS 中解释 G-code、canonical events
或 CNC 运行语义。

View File

@@ -15,6 +15,8 @@ import {
getIniPanelEntryByHref, getIniPanelEntryByHref,
getVisibleIniPanelEntries, getVisibleIniPanelEntries,
createMachineSessionControlPageController, createMachineSessionControlPageController,
createMachineSessionReadonlyStatus,
readMachineSessionReadonlyStatus,
loadMachineSessionForControlPageWorkflow, loadMachineSessionForControlPageWorkflow,
createControlPageSessionLoadState, createControlPageSessionLoadState,
refreshMachineSessionControlPage, refreshMachineSessionControlPage,
@@ -24,7 +26,8 @@ import {
``` ```
`ui-shell.js` re-exports the entry manifest, the read-only control-page `ui-shell.js` re-exports the entry manifest, the read-only control-page
controller, and the control-view renderer. It does not implement CNC behavior. controller, the unified read-only status API, and the control-view renderer. It
does not implement CNC behavior.
## Pages ## Pages
@@ -50,6 +53,9 @@ and `label`.
Outer shells can call `createIniPanelShellViewModel()` when they need one Outer shells can call `createIniPanelShellViewModel()` when they need one
read-only model with `pages`, `launcherLinks`, and `controlPage` helpers for read-only model with `pages`, `launcherLinks`, and `controlPage` helpers for
mounting the existing control-page controller, refresh workflow, and renderer. mounting the existing control-page controller, refresh workflow, and renderer.
Use `controlPage.readStatus` or `readMachineSessionReadonlyStatus()` when an
outer shell needs one stable read of `stateBundle`, `workflowStatus`,
`overview`, `controlView`, `session`, `runReadiness`, and `run`.
The control-page refresh workflow lives in The control-page refresh workflow lives in
`runtime/ui/ini-panel/control-page-refresh-workflow.js`. Use `runtime/ui/ini-panel/control-page-refresh-workflow.js`. Use

View File

@@ -4,6 +4,9 @@ import {
import { import {
loadMachineSessionForControlPageWorkflow, loadMachineSessionForControlPageWorkflow,
} from "./control-page-session-workflow.js"; } from "./control-page-session-workflow.js";
import {
readMachineSessionReadonlyStatus,
} from "./readonly-status-api.js";
export function createMachineSessionControlPageController({ export function createMachineSessionControlPageController({
panelFrame, panelFrame,
@@ -61,6 +64,13 @@ export function createMachineSessionControlPageController({
}); });
} }
function readStatus() {
return readMachineSessionReadonlyStatus({
getPanelApi,
getControlView,
});
}
function startPolling() { function startPolling() {
if (!refreshTimer) { if (!refreshTimer) {
refreshTimer = setIntervalFn(render, pollMs); refreshTimer = setIntervalFn(render, pollMs);
@@ -85,6 +95,7 @@ export function createMachineSessionControlPageController({
getPanelWindow, getPanelWindow,
loadSessionState, loadSessionState,
mount, mount,
readStatus,
refresh, refresh,
render, render,
startPolling, startPolling,

View File

@@ -0,0 +1,41 @@
export function createMachineSessionReadonlyStatus({
stateBundle = null,
workflowStatus = null,
controlView = null,
} = {}) {
const overview = stateBundle?.overview ?? workflowStatus?.overview ?? null;
return {
stateBundle,
workflowStatus,
overview,
controlView,
report: stateBundle?.report ?? null,
runReadiness: workflowStatus?.runReadiness ?? stateBundle?.report?.runReadiness ?? null,
run: workflowStatus?.run ?? stateBundle?.report?.run ?? null,
session: workflowStatus?.session ?? stateBundle?.report?.session ?? null,
status: {
lastAction: workflowStatus?.lastAction ?? overview?.status?.lastAction ?? null,
error: workflowStatus?.error ?? overview?.status?.error ?? null,
runStatus: workflowStatus?.run?.status ?? stateBundle?.report?.run?.status ?? null,
},
};
}
export function readMachineSessionReadonlyStatus({ getPanelApi, getControlView } = {}) {
if (typeof getPanelApi !== "function") {
throw new Error("A panel API getter is required.");
}
if (typeof getControlView !== "function") {
throw new Error("A control view getter is required.");
}
const panelApi = getPanelApi();
if (!panelApi) {
return createMachineSessionReadonlyStatus();
}
return createMachineSessionReadonlyStatus({
stateBundle: panelApi.getMachineSessionStateBundle?.() ?? null,
workflowStatus: panelApi.getMachineSessionWorkflowStatus?.() ?? null,
controlView: getControlView(),
});
}

View File

@@ -18,6 +18,10 @@ import {
createControlPageSessionLoadState, createControlPageSessionLoadState,
loadMachineSessionForControlPageWorkflow, loadMachineSessionForControlPageWorkflow,
} from "./control-page-session-workflow.js"; } from "./control-page-session-workflow.js";
import {
createMachineSessionReadonlyStatus,
readMachineSessionReadonlyStatus,
} from "./readonly-status-api.js";
export { export {
controlPageStatusText, controlPageStatusText,
createControlPageRefreshState, createControlPageRefreshState,
@@ -27,6 +31,10 @@ export {
createControlPageSessionLoadState, createControlPageSessionLoadState,
loadMachineSessionForControlPageWorkflow, loadMachineSessionForControlPageWorkflow,
} from "./control-page-session-workflow.js"; } from "./control-page-session-workflow.js";
export {
createMachineSessionReadonlyStatus,
readMachineSessionReadonlyStatus,
} from "./readonly-status-api.js";
export { export {
INI_PANEL_ENTRIES, INI_PANEL_ENTRIES,
getIniPanelEntryByHref, getIniPanelEntryByHref,
@@ -62,6 +70,7 @@ export function createIniPanelShellViewModel(entries = getVisibleIniPanelEntries
controlPage: { controlPage: {
createController: createMachineSessionControlPageController, createController: createMachineSessionControlPageController,
loadSessionState: loadMachineSessionForControlPageWorkflow, loadSessionState: loadMachineSessionForControlPageWorkflow,
readStatus: readMachineSessionReadonlyStatus,
refresh: refreshMachineSessionControlPage, refresh: refreshMachineSessionControlPage,
renderView: renderMachineSessionControlView, renderView: renderMachineSessionControlView,
}, },

View File

@@ -87,6 +87,21 @@ panelFrame.contentWindow = { linuxCncIniPanelApi: panelApi };
assert.equal(controller.getPanelApi(), panelApi); assert.equal(controller.getPanelApi(), panelApi);
assert.equal(controller.getControlView(), expectedView); assert.equal(controller.getControlView(), expectedView);
assert.deepEqual(controller.readStatus(), {
stateBundle: expectedStateBundle,
workflowStatus: expectedWorkflowStatus,
overview: null,
controlView: expectedView,
report: expectedStateBundle.report,
runReadiness: null,
run: null,
session: null,
status: {
lastAction: "run-gcode",
error: null,
runStatus: null,
},
});
assert.deepEqual(controller.loadSessionState(), { assert.deepEqual(controller.loadSessionState(), {
phase: "ready", phase: "ready",
panelReady: true, panelReady: true,

View File

@@ -9,7 +9,9 @@ import {
createControlPageSessionLoadState, createControlPageSessionLoadState,
createIniPanelLauncherLinkViewModel, createIniPanelLauncherLinkViewModel,
createIniPanelShellViewModel, createIniPanelShellViewModel,
createMachineSessionReadonlyStatus,
loadMachineSessionForControlPageWorkflow, loadMachineSessionForControlPageWorkflow,
readMachineSessionReadonlyStatus,
refreshMachineSessionControlPage, refreshMachineSessionControlPage,
getVisibleIniPanelEntries, getVisibleIniPanelEntries,
} from "../../../runtime/ui/ini-panel/ui-shell.js"; } from "../../../runtime/ui/ini-panel/ui-shell.js";
@@ -36,7 +38,9 @@ const requiredShellExports = [
"getIniPanelEntryByHref", "getIniPanelEntryByHref",
"getVisibleIniPanelEntries", "getVisibleIniPanelEntries",
"createMachineSessionControlPageController", "createMachineSessionControlPageController",
"createMachineSessionReadonlyStatus",
"loadMachineSessionForControlPageWorkflow", "loadMachineSessionForControlPageWorkflow",
"readMachineSessionReadonlyStatus",
"refreshMachineSessionControlPage", "refreshMachineSessionControlPage",
"renderMachineSessionControlView", "renderMachineSessionControlView",
]; ];
@@ -96,7 +100,9 @@ assert.deepEqual(createControlPageSessionLoadState({ phase: "waiting-panel" }),
error: null, error: null,
}); });
assert.equal(typeof createIniPanelShellViewModel().controlPage.createController, "function"); assert.equal(typeof createIniPanelShellViewModel().controlPage.createController, "function");
assert.equal(typeof createMachineSessionReadonlyStatus, "function");
assert.equal(createIniPanelShellViewModel().controlPage.loadSessionState, loadMachineSessionForControlPageWorkflow); assert.equal(createIniPanelShellViewModel().controlPage.loadSessionState, loadMachineSessionForControlPageWorkflow);
assert.equal(createIniPanelShellViewModel().controlPage.readStatus, readMachineSessionReadonlyStatus);
assert.equal(createIniPanelShellViewModel().controlPage.refresh, refreshMachineSessionControlPage); assert.equal(createIniPanelShellViewModel().controlPage.refresh, refreshMachineSessionControlPage);
assert.equal(typeof createIniPanelShellViewModel().controlPage.renderView, "function"); assert.equal(typeof createIniPanelShellViewModel().controlPage.renderView, "function");

View File

@@ -0,0 +1,116 @@
import assert from "node:assert/strict";
import {
createMachineSessionReadonlyStatus,
readMachineSessionReadonlyStatus,
} from "../../../runtime/ui/ini-panel/readonly-status-api.js";
const stateBundle = {
overview: {
phase: "ran",
status: {
lastAction: "run-gcode",
error: null,
},
},
report: {
session: {
snapshotLabel: "ui-machine-session/ui-machine-session.json",
},
runReadiness: {
phase: "ready",
canRun: true,
},
run: {
status: "ok",
canonicalEventCount: 2,
},
},
};
const workflowStatus = {
overview: stateBundle.overview,
runReadiness: stateBundle.report.runReadiness,
session: stateBundle.report.session,
run: stateBundle.report.run,
lastAction: "run-gcode",
error: null,
};
const controlView = {
status: {
lastAction: "run-gcode",
runStatus: "ok",
error: null,
},
};
assert.deepEqual(
createMachineSessionReadonlyStatus({
stateBundle,
workflowStatus,
controlView,
}),
{
stateBundle,
workflowStatus,
overview: stateBundle.overview,
controlView,
report: stateBundle.report,
runReadiness: stateBundle.report.runReadiness,
run: stateBundle.report.run,
session: stateBundle.report.session,
status: {
lastAction: "run-gcode",
error: null,
runStatus: "ok",
},
},
);
assert.deepEqual(createMachineSessionReadonlyStatus(), {
stateBundle: null,
workflowStatus: null,
overview: null,
controlView: null,
report: null,
runReadiness: null,
run: null,
session: null,
status: {
lastAction: null,
error: null,
runStatus: null,
},
});
const panelApi = {
getMachineSessionStateBundle: () => stateBundle,
getMachineSessionWorkflowStatus: () => workflowStatus,
};
assert.deepEqual(
readMachineSessionReadonlyStatus({
getPanelApi: () => panelApi,
getControlView: () => controlView,
}),
createMachineSessionReadonlyStatus({
stateBundle,
workflowStatus,
controlView,
}),
);
assert.deepEqual(
readMachineSessionReadonlyStatus({
getPanelApi: () => null,
getControlView: () => null,
}),
createMachineSessionReadonlyStatus(),
);
assert.throws(
() => readMachineSessionReadonlyStatus({ getControlView: () => null }),
/panel API getter/,
);
assert.throws(
() => readMachineSessionReadonlyStatus({ getPanelApi: () => panelApi }),
/control view getter/,
);
console.log("ini_panel_readonly_status_api_node_smoke=ok");

View File

@@ -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_readonly_status_api.mjs"

View File

@@ -5,6 +5,7 @@ import {
controlPageStatusText, controlPageStatusText,
createIniPanelLauncherLinkViewModel, createIniPanelLauncherLinkViewModel,
createIniPanelShellViewModel, createIniPanelShellViewModel,
createMachineSessionReadonlyStatus,
createControlPageRefreshState, createControlPageRefreshState,
createControlPageSessionLoadState, createControlPageSessionLoadState,
createMachineSessionControlPageController, createMachineSessionControlPageController,
@@ -12,6 +13,7 @@ import {
getIniPanelEntryManifest, getIniPanelEntryManifest,
getVisibleIniPanelEntries, getVisibleIniPanelEntries,
loadMachineSessionForControlPageWorkflow, loadMachineSessionForControlPageWorkflow,
readMachineSessionReadonlyStatus,
refreshMachineSessionControlPage, refreshMachineSessionControlPage,
renderMachineSessionControlView, renderMachineSessionControlView,
} from "../../../runtime/ui/ini-panel/ui-shell.js"; } from "../../../runtime/ui/ini-panel/ui-shell.js";
@@ -38,7 +40,9 @@ assert.deepEqual(createControlPageSessionLoadState({ phase: "waiting-panel" }),
}); });
assert.equal(getIniPanelEntryByHref("./index.html")?.id, "edit-run"); assert.equal(getIniPanelEntryByHref("./index.html")?.id, "edit-run");
assert.equal(typeof createMachineSessionControlPageController, "function"); assert.equal(typeof createMachineSessionControlPageController, "function");
assert.equal(typeof createMachineSessionReadonlyStatus, "function");
assert.equal(typeof loadMachineSessionForControlPageWorkflow, "function"); assert.equal(typeof loadMachineSessionForControlPageWorkflow, "function");
assert.equal(typeof readMachineSessionReadonlyStatus, "function");
assert.equal(typeof refreshMachineSessionControlPage, "function"); assert.equal(typeof refreshMachineSessionControlPage, "function");
assert.equal(typeof renderMachineSessionControlView, "function"); assert.equal(typeof renderMachineSessionControlView, "function");
@@ -79,6 +83,7 @@ assert.deepEqual(shellViewModel.pages, [
assert.deepEqual(shellViewModel.launcherLinks, createIniPanelLauncherLinkViewModel()); assert.deepEqual(shellViewModel.launcherLinks, createIniPanelLauncherLinkViewModel());
assert.equal(shellViewModel.controlPage.createController, createMachineSessionControlPageController); assert.equal(shellViewModel.controlPage.createController, createMachineSessionControlPageController);
assert.equal(shellViewModel.controlPage.loadSessionState, loadMachineSessionForControlPageWorkflow); assert.equal(shellViewModel.controlPage.loadSessionState, loadMachineSessionForControlPageWorkflow);
assert.equal(shellViewModel.controlPage.readStatus, readMachineSessionReadonlyStatus);
assert.equal(shellViewModel.controlPage.refresh, refreshMachineSessionControlPage); assert.equal(shellViewModel.controlPage.refresh, refreshMachineSessionControlPage);
assert.equal(shellViewModel.controlPage.renderView, renderMachineSessionControlView); assert.equal(shellViewModel.controlPage.renderView, renderMachineSessionControlView);
shellViewModel.pages[0].title = "changed"; shellViewModel.pages[0].title = "changed";
@@ -96,6 +101,18 @@ const panelFrame = {
}, },
sections: [], sections: [],
}), }),
getMachineSessionStateBundle: () => ({
overview: {
phase: "ran",
},
}),
getMachineSessionWorkflowStatus: () => ({
overview: {
phase: "ran",
},
lastAction: "run-gcode",
error: null,
}),
}, },
}, },
addEventListener() {}, addEventListener() {},
@@ -116,6 +133,7 @@ const controller = createMachineSessionControlPageController({
assert.deepEqual(controller.render(), { rendered: true }); assert.deepEqual(controller.render(), { rendered: true });
assert.equal(statusNode.textContent, "run-gcode | ok | -"); assert.equal(statusNode.textContent, "run-gcode | ok | -");
assert.equal(renderedView.status.runStatus, "ok"); assert.equal(renderedView.status.runStatus, "ok");
assert.equal(controller.readStatus().overview.phase, "ran");
assert.equal(controller.loadSessionState().phase, "ready"); assert.equal(controller.loadSessionState().phase, "ready");
assert.equal(controller.refresh().phase, "rendered"); assert.equal(controller.refresh().phase, "rendered");

View File

@@ -12,6 +12,7 @@ ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_control_page_controller.sh" "$ROOT_DIR/tests/ui/node/verify_ini_panel_control_page_controller.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_control_page_refresh_workflow.sh" "$ROOT_DIR/tests/ui/node/verify_ini_panel_control_page_refresh_workflow.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_control_page_session_workflow.sh" "$ROOT_DIR/tests/ui/node/verify_ini_panel_control_page_session_workflow.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_readonly_status_api.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_entry_manifest.sh" "$ROOT_DIR/tests/ui/node/verify_ini_panel_entry_manifest.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_ui_shell.sh" "$ROOT_DIR/tests/ui/node/verify_ini_panel_ui_shell.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_entry_docs.sh" "$ROOT_DIR/tests/ui/node/verify_ini_panel_entry_docs.sh"