Compare commits

...

6 Commits

23 changed files with 1556 additions and 77 deletions

View File

@@ -1633,3 +1633,267 @@ host_wasm_opfs_browser_smokes=ok
继续推进实质 UI/API surface。下一批建议把 `ui-shell.js` 中用于 launcher 的入口列表再封装成
`getVisibleIniPanelEntries()`,让页面不需要知道 manifest 复制语义;仍保持只读,不新增控制
按钮,不解析 G-code。
二十一、2026-06-14 继续执行记录INI panel visible entries helper
本轮按“加快实质性推进”在 `ui-shell.js` 增加 launcher-facing 的可见入口 helper让页面和
文档使用 `getVisibleIniPanelEntries()`,不再直接依赖 manifest 复制语义。
完成内容:
- `wasm-port/runtime/ui/ini-panel/ui-shell.js` 新增 `getVisibleIniPanelEntries()`
- `getVisibleIniPanelEntries()` 返回 launcher 可见入口列表;
- `wasm-port/runtime/ui/ini-panel/launch.html` 改为调用 `getVisibleIniPanelEntries()`
- `wasm-port/tests/browser/ini_panel_launch_smoke.html` 改为验证 visible entries
- `wasm-port/tests/ui/node/verify_ini_panel_ui_shell.mjs` 覆盖 visible entries 与复制语义;
- `wasm-port/docs/panel-entry.md` 改为以 `getVisibleIniPanelEntries()` 作为入口列表 API
- `wasm-port/tests/ui/node/verify_ini_panel_entry_docs.mjs` 同步验证文档和 visible entries
- 未新增控制按钮;
- 未解析 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_ui_shell.sh
wasm-port/tests/ui/node/verify_ini_panel_entry_docs.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_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
host_wasm_opfs_browser_smokes=ok
```
下一步建议:
继续推进 UI/API surface但避免纯文档循环。下一批建议把 `getVisibleIniPanelEntries()` 用到
更多外层 shell 消费示例,或新增一个小型 Node helper 生成 launcher link view-model仍保持
只读,不新增控制按钮,不解析 G-code。
二十二、2026-06-15 继续执行记录INI panel launcher link view-model helper
本轮按“加快实质性推进”继续推进 UI/API surface在 shell 层新增 launcher link
view-model helper让 launcher 页面不直接读取 manifest 字段名。
完成内容:
- `wasm-port/runtime/ui/ini-panel/ui-shell.js` 新增
`createIniPanelLauncherLinkViewModel()`
- launcher link view-model 输出 `entryId`、`href`、`label`,作为外层 shell 可直接渲染的
链接模型;
- `wasm-port/runtime/ui/ini-panel/launch.html` 改为调用
`createIniPanelLauncherLinkViewModel()` 渲染链接;
- `wasm-port/tests/browser/ini_panel_launch_smoke.html` 覆盖 launcher link view-model
- `wasm-port/tests/ui/node/verify_ini_panel_ui_shell.mjs` 覆盖 view-model 内容与复制语义;
- `wasm-port/tests/ui/node/verify_ini_panel_entry_docs.mjs` 同步校验文档和 shell helper
- `wasm-port/docs/panel-entry.md` 记录 launcher link view-model 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_ui_shell.sh
wasm-port/tests/ui/node/verify_ini_panel_entry_docs.sh
wasm-port/tests/browser/verify_ini_panel_browser.sh
wasm-port/tests/ui/node/verify_ui_node_smokes.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
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_ini_panel_browser.sh
wasm-port/tests/host/verify_host_smokes.sh
```
关键输出:
```text
ini_panel_ui_shell_node_smoke=ok
ini_panel_entry_docs_node_smoke=ok
browser_ini_opfs_smoke=ok
browser_ini_control_page_smoke=ok
browser_ini_launch_smoke=ok
ui_node_smokes=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
```
下一步建议:
继续推进 UI/API surface 的可复用边界。下一批建议新增一个
`createIniPanelShellViewModel()`,把 launcher links、可用页面和 control-page helper 暴露为
单一 shell model供外层应用一次性挂载仍保持只读不新增控制按钮不解析 G-code。
二十三、2026-06-15 继续执行记录INI panel shell view-model helper
本轮按“第一原则:加快实质性推进”继续推进 UI/API surface新增单一 shell view-model
让外层应用可以一次性读取 launcher links、页面列表和 control-page 挂载 helper。
完成内容:
- `wasm-port/runtime/ui/ini-panel/ui-shell.js` 新增 `createIniPanelShellViewModel()`
- shell view-model 返回:
- `pages`:只读页面列表;
- `launcherLinks`:可直接渲染的 launcher link view-model
- `controlPage.createController`:现有 read-only control-page controller
- `controlPage.renderView`:现有 read-only control-view renderer
- `wasm-port/tests/ui/node/verify_ini_panel_ui_shell.mjs` 覆盖 shell view-model 结构、
helper 引用和复制语义;
- `wasm-port/tests/ui/node/verify_ini_panel_entry_docs.mjs` 同步校验文档和 shell helper
- `wasm-port/docs/panel-entry.md` 记录 `createIniPanelShellViewModel()`
- 未新增控制按钮;
- 未解析 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_ui_shell.sh
wasm-port/tests/ui/node/verify_ini_panel_entry_docs.sh
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_ini_panel_browser.sh
wasm-port/tests/ui/node/verify_ui_node_smokes.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_ui_shell_node_smoke=ok
ini_panel_entry_docs_node_smoke=ok
browser_ini_opfs_smoke=ok
browser_ini_control_page_smoke=ok
browser_ini_launch_smoke=ok
ui_node_smokes=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
```
下一步建议:
继续推进 OPFS/session persistence 或 browser/UI workflow 的实质链路。下一批建议在
`app.js` 外抽出一个 control-page snapshot refresh workflow helper让 read-only 页面能复用
明确的 refresh 状态模型;仍保持只读,不新增控制按钮,不解析 G-code。
二十四、2026-06-15 继续执行记录INI panel control-page refresh workflow helper
本轮按“第一原则:加快实质性推进”推进 browser/UI workflow在 `app.js` 外新增
read-only control-page refresh workflow helper让外层页面可复用明确的 refresh 状态模型。
完成内容:
- 新增 `wasm-port/runtime/ui/ini-panel/control-page-refresh-workflow.js`
- 新增 `controlPageStatusText(status)`,格式化已观察到的 control view status 字段;
- 新增 `createControlPageRefreshState(input)`,固定 refresh 状态结构;
- 新增 `refreshMachineSessionControlPage(input)`,统一处理:
- `waiting`panel API 尚不可用;
- `rendered`:读取 control view 并调用 renderer
- `error`control view getter 或 renderer 抛错;
- `createMachineSessionControlPageController()` 改为通过 refresh workflow 执行 render
- control-page controller 新增 `refresh()`,保留原 `render()` 返回值兼容;
- `control-page.js` 对浏览器页面暴露 `refreshControlPage`
- `ui-shell.js` re-export refresh workflow helper并在 `createIniPanelShellViewModel()` 中暴露
`controlPage.refresh`
- 新增 `wasm-port/tests/ui/node/verify_ini_panel_control_page_refresh_workflow.mjs` 和 `.sh`
- `verify_ui_node_smokes.sh` 纳入 refresh workflow smoke
- browser control-page smoke 验证 `refreshControlPage().phase === "rendered"`
- `panel-entry.md` 和 entry docs gate 同步记录 refresh workflow 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_control_page_refresh_workflow.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_control_page_refresh_workflow_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
```
下一步建议:
继续推进 OPFS/session persistence 的实质链路。下一批建议抽出
`loadMachineSessionForControlPageWorkflow()` 或等价 helper让 read-only control page 能以明确
状态模型描述 embedded panel API readiness 和首帧 session state仍保持只读不新增控制按钮
不解析 G-code。

436
text12.txt Normal file
View File

@@ -0,0 +1,436 @@
项目整体完成情况与后续执行计划
生成时间2026-06-15 CST
本文件接替 `text11.txt`。后续工作按 `text12.txt` 执行;除非明确要求审计旧记录,
不要再回到 `text1/text2/text3/text4/text5/text6/text7/text8/text9/text10/text11`
扩展进度。`text11.txt` 作为历史归档保留。
一、第一原则:加快实质性推进
后续批次必须以“实质性能力优先”为第一遵守原则。
1. 优先级从高到低:
- OPFS/session persistence
- SDK/API surface
- browser/UI workflow
- sim config coverage promotion
- host/runtime boundary proof
- 文档化可执行工作流。
2. 每批默认应产出至少一种可直接复用的能力:
- 可调用 helper
- 可复用 API
- 可验证用户流程;
- 可执行 gate
- 可提升覆盖面的 runtime boundary proof
- 能直接指导下一步实现的设计/清单。
3. 暂停低收益推进:
- 不再把“局部常量化、路径变量收敛、断言分组整理”作为主目标;
- 只有当它支撑可复用 workflow/API/gate 时,才允许做测试胶水;
- 禁止为了推进而扩大 CNC 语义边界;
- 禁止启动性能测试,除非已有明确用户流程需要性能基线。
4. LinuxCNC 仍是唯一 CNC 语义来源:
- G-code、remap、tool、parameter、planner、kinematics、user-M、tool DB、Python remap
等语义必须来自 LinuxCNC upstream 或 vendored LinuxCNC C/C++/配置/脚本源码;
- JS/browser 只允许做 build、source sync、runtime shim、filesystem staging、OPFS、
WASM/browser boundary、测试胶水、文档和机器可读 gate/artifact
- 不在 JS 中重写 G-code 或 CNC 语义;
- 不修改 vendored LinuxCNC 源文件来让测试通过;
- 不把 full-process/HAL/UI/Python/tool DB 依赖伪装成 standalone pass
- 不把 expected failure 硬改成 pass除非已有 LinuxCNC-owned runtime proof并且
native、Node、browser gate 都按顺序通过;
- browser 不能依赖目录枚举。
二、续接状态与防重规则
1. 当前续接点。
`text11.txt` 已推进到:
```text
二十四、2026-06-15 继续执行记录INI panel control-page refresh workflow helper
```
该批已提交:
```text
ed5d03e 推进 INI 面板 control-page refresh workflow helper
```
本文件从 `text12.txt` 重新开始记录后续进度。
2. 每轮开始前必须先执行:
```bash
git status --short
git log -5 --oneline
tail -n 120 text12.txt
awk '/^[一二三四五六七八九十百]+、/ {print}' text12.txt | sort | uniq -d
```
执行规则:
- 如果 `git status --short` 是 clean且最新提交已经包含当前建议工作不要重复追加记录
- 每轮只追加一个新的中文编号记录;
- 提交前再次检查标题唯一性;
- `text12.txt` 只记录新的后续进度,不复制 `text11.txt` 的长历史;
- 每批完成后,如果要写记录,先写 `text12.txt`,再提交该批代码和记录;
- 如果发现重复记录,先做 dedupe 清理并提交,再继续新功能或新 guard
- `text11.txt` 不再追加新进度,除非明确要求审计或修正历史记录。
三、当前完成情况摘要
1. 当前工作树状态。
切换到 `text12.txt` 前已确认:
```text
git status --short: clean
```
当前最近提交:
```text
ed5d03e 推进 INI 面板 control-page refresh workflow helper
213514f 推进 INI 面板 shell view-model helper
b330f7b 推进 INI 面板 launcher link view-model helper
ac46345 推进 INI 面板 entry docs strict gate
ef4769b 推进 INI 面板 entry doc
```
2. 关键基线保持稳定。
- `linuxcnc/nc_files` native Layer 1
- `total 107`
- `pass 101`
- `expected_fail 6`
- `unexpected_fail 0`
- `linuxcnc/configs/sim` native Layer 2
- `total 159`
- `pass 151`
- `expected_fail 8`
- `unexpected_fail 0`
- Node sim-config inventory
- `executed=28`
- `passed=28`
- `skipped=131`
- `unexpected_fail=0`
3. blocked runtime family 仍未 promotion。
仍 blocked不允许 promotion
- `L4-USER-M-PROCESS`
- `L4-TOOL-DB`
- `L4-PYTHON-REMAP`
当前 host 仍按缺少 LinuxCNC host runtime 处理,不执行任何:
```bash
ENABLE_MILLTURN_USER_M_RUNTIME_PROBE=1 bash wasm-port/tests/native/probe_millturn_user_m_runtime.sh
ENABLE_TOOL_DB_RUNTIME_PROBE=1 bash wasm-port/tests/native/probe_tool_db_runtime.sh
ENABLE_PYTHON_REMAP_RUNTIME_PROBE=1 bash wasm-port/tests/native/probe_python_remap_runtime.sh
```
四、当前实质能力进展
1. INI panel/UI helper 能力。
当前 UI helper 已覆盖:
- run summary
- run workflow
- session snapshot summary
- session load summary
- machine file save/load summary
- panel state summary
- session snapshot save/restore workflow
- entry manifest、launcher link view-model、shell view-model
- read-only control-page controller
- read-only control-page refresh workflow。
当前聚合验证入口:
```bash
wasm-port/tests/ui/node/verify_ui_node_smokes.sh
```
当前 `verify_ui_node_smokes.sh` 覆盖:
```text
ini_panel_run_summary_node_smoke=ok
ini_panel_run_workflow_node_smoke=ok
ini_panel_session_summary_node_smoke=ok
ini_panel_session_workflow_node_smoke=ok
ini_panel_machine_file_summary_node_smoke=ok
ini_panel_state_summary_node_smoke=ok
ini_panel_control_page_controller_node_smoke=ok
ini_panel_control_page_refresh_workflow_node_smoke=ok
ini_panel_entry_manifest_node_smoke=ok
ini_panel_ui_shell_node_smoke=ok
ini_panel_entry_docs_node_smoke=ok
ui_node_smokes=ok
```
2. Browser/UI workflow 验证。
当前 browser INI panel smoke 覆盖真实 OPFS/session persistence workflow
```bash
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_ini_panel_browser.sh
```
关键输出:
```text
browser_ini_opfs_smoke=ok
browser_ini_control_page_smoke=ok
browser_ini_launch_smoke=ok
```
五、当前必须保持通过的验证入口
每批后续改动至少运行:
```bash
git diff --check
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
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_ini_panel_browser.sh
wasm-port/tests/ui/node/verify_ui_node_smokes.sh
wasm-port/tests/host/verify_host_smokes.sh
```
如果涉及 browser interpreter smoke再运行
```bash
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_interp_browser.sh
```
涉及 native baseline 或大范围 source/staging 改动时,再运行:
```bash
wasm-port/tests/native/verify_nc_files.sh
wasm-port/tests/native/verify_sim_configs.sh
```
六、下一步建议
继续推进 OPFS/session persistence 的实质链路。下一批建议抽出
`loadMachineSessionForControlPageWorkflow()` 或等价 helper让 read-only control page 能以明确
状态模型描述 embedded panel API readiness 和首帧 session state仍保持只读不新增控制按钮
不解析 G-code。
一、2026-06-15 继续执行记录INI panel control-page session readiness workflow helper
本轮按“第一原则:加快实质性推进”切换到 `text12.txt` 续接,并推进 OPFS/session
persistence 相关 browser/UI workflow新增 read-only control-page session readiness workflow
让外层页面能以明确状态模型读取 embedded panel API readiness 和首帧 session state。
完成内容:
- 新建 `text12.txt`,接替 `text11.txt`;后续记录从本文件继续;
- 新增 `wasm-port/runtime/ui/ini-panel/control-page-session-workflow.js`
- 新增 `createControlPageSessionLoadState(input)`,固定 control-page session load 状态结构;
- 新增 `loadMachineSessionForControlPageWorkflow(input)`,统一描述:
- `waiting-panel`embedded panel frame 尚不可读;
- `waiting-api`panel window 可读但 API 尚未暴露;
- `waiting-session`API 已可读但 control view 尚未就绪;
- `ready`state bundle、workflow status 和 control view 可读取;
- `error`:读取 panel window/API/control view 时失败;
- `createMachineSessionControlPageController()` 新增 `loadSessionState()`
- `control-page.js` 对浏览器页面暴露 `loadControlPageSessionState`
- `ui-shell.js` re-export session readiness workflow helper并在
`createIniPanelShellViewModel()` 中暴露 `controlPage.loadSessionState`
- 新增 `wasm-port/tests/ui/node/verify_ini_panel_control_page_session_workflow.mjs` 和 `.sh`
- `verify_ui_node_smokes.sh` 纳入 session readiness workflow smoke
- browser control-page smoke 验证 `loadControlPageSessionState().phase === "ready"`
- `panel-entry.md` 和 entry docs gate 同步记录 session readiness workflow 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_control_page_session_workflow.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_control_page_session_workflow_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
```
下一步建议:
继续推进 OPFS/session persistence 的实质链路。下一批建议把 `load-session` 按钮路径抽成
`loadMachineSessionIntoWasmWorkflow()`,统一返回 loaded session、load summary、field state 和
log lines仍保持由 LinuxCNC/OPFS bridge 执行实际语义,不在 JS 中解释 CNC 行为。
二、2026-06-15 继续执行记录INI panel load-session workflow helper
本轮按“第一原则:加快实质性推进”继续推进 OPFS/session persistence 主链路,抽出
`load-session` 按钮路径的可复用 workflow helper统一返回 loaded session、load summary、
field state 和 log lines。
完成内容:
- `wasm-port/runtime/ui/ini-panel/session-workflow.js` 新增
`createMachineSessionLoadFieldState(summary)`
- `session-workflow.js` 新增 `machineSessionLoadWorkflowLogLines(summary)`
- `session-workflow.js` 新增 `loadMachineSessionIntoWasmWorkflow({ loadSession })`
- workflow 只调用外部传入的 LinuxCNC/OPFS session loader并基于现有
`createMachineSessionLoadSummary()` 生成 UI 状态;
- `wasm-port/runtime/ui/ini-panel/app.js` 的 `load-session` handler 改为调用
`loadMachineSessionIntoWasmWorkflow()`
- `app.js` 仍由 `loadMachineSessionFromOpfs()` 通过 LinuxCNC/OPFS bridge 执行实际 session
staging
- `wasm-port/tests/ui/node/verify_ini_panel_session_workflow.mjs` 覆盖 load workflow 的
loader 调用、summary、field state、log lines 和缺失 loader 错误;
- 未新增控制按钮;
- 未解析 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_session_workflow.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_session_workflow_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
```
下一步建议:
继续推进 OPFS/session persistence 的实质链路。下一批建议将 `restore-load-session` 路径改为复用
`loadMachineSessionIntoWasmWorkflow()`,避免 restore+load 与单独 load 的 field/log 状态模型
分叉;仍保持由 LinuxCNC/OPFS bridge 执行实际语义,不在 JS 中解释 CNC 行为。
三、2026-06-15 继续执行记录INI panel restore-load workflow 复用 load-session helper
本轮按“第一原则:加快实质性推进”继续推进 OPFS/session persistence 主链路,将
`restore-load-session` 路径收敛到已存在的 `loadMachineSessionIntoWasmWorkflow()`,让单独
load 与 restore+load 共享同一套 load summary、field state 和 log lines 模型。
完成内容:
- `restoreMachineSessionSnapshotWorkflow()` 的 restore+load 分支改为调用
`loadMachineSessionIntoWasmWorkflow({ loadSession })`
- restore+load 返回的 `loadedSession`、`loadSummary` 和 INI/parameter/tool-table field state
直接来自 load workflow
- restore+load 日志纳入 load workflow 的统一首行:
`Loaded machine session into the interpreter WASM filesystem.`
- 删除 `app.js` 中已不再使用的 `sessionSnapshotSummaryLogLines` import
- `verify_ini_panel_session_workflow.mjs` 增加 restore+load 复用断言,确认
`loadSummary` 与单独 load workflow 一致,且 `loadedSession` 保持 loader 返回对象;
- `ui-session-summary-helpers.md` 同步说明 restore-load 复用 load workflow
- 未新增控制按钮;
- 未解析 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_session_workflow.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_session_workflow_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
```
下一步建议:
继续推进 OPFS/session persistence 的实质链路。下一批建议抽出 `run-session` 首帧 readiness
或 run 前置状态 helper把“已加载 session、已选择/恢复 snapshot、默认 G-code 路径、run
按钮可执行条件”统一成可复用状态模型;仍保持只描述 UI/host boundary facts不在 JS 中解释
G-code、canonical events 或 CNC 运行语义。

View File

@@ -10,9 +10,15 @@ control view:
```js
import {
INI_PANEL_ENTRIES,
createIniPanelLauncherLinkViewModel,
createIniPanelShellViewModel,
getIniPanelEntryByHref,
getIniPanelEntryManifest,
getVisibleIniPanelEntries,
createMachineSessionControlPageController,
loadMachineSessionForControlPageWorkflow,
createControlPageSessionLoadState,
refreshMachineSessionControlPage,
controlPageStatusText,
renderMachineSessionControlView,
} from "../runtime/ui/ini-panel/ui-shell.js";
```
@@ -28,15 +34,38 @@ controller, and the control-view renderer. It does not implement CNC behavior.
| Edit and run | `runtime/ui/ini-panel/index.html` | Editable INI panel and existing run workflow surface. |
| Read-only control view | `runtime/ui/ini-panel/control-page.html` | Read-only control view backed by the panel API. |
## Entry Manifest
## Visible Entries
The visible launch entries come from `getIniPanelEntryManifest()`:
The visible launch entries come from `getVisibleIniPanelEntries()`:
| Entry id | Href | Title |
| --- | --- | --- |
| `edit-run` | `./index.html` | Open edit and run panel |
| `control-view` | `./control-page.html` | Open read-only control view |
Launcher pages can render links from `createIniPanelLauncherLinkViewModel()`
without reading manifest field names directly. Each item has `entryId`, `href`,
and `label`.
Outer shells can call `createIniPanelShellViewModel()` when they need one
read-only model with `pages`, `launcherLinks`, and `controlPage` helpers for
mounting the existing control-page controller, refresh workflow, and renderer.
The control-page refresh workflow lives in
`runtime/ui/ini-panel/control-page-refresh-workflow.js`. Use
`refreshMachineSessionControlPage()` when an outer shell needs a structured
refresh result with `phase`, `statusText`, `view`, `renderResult`, and `error`.
`controlPageStatusText()` formats the already-observed panel status fields for
the read-only status line, and `createControlPageRefreshState()` is the stable
state-shape helper used by the workflow.
The control-page session workflow lives in
`runtime/ui/ini-panel/control-page-session-workflow.js`. Use
`loadMachineSessionForControlPageWorkflow()` when an outer shell needs a
read-only readiness snapshot for the embedded panel API and first control view.
`createControlPageSessionLoadState()` is the stable state-shape helper for
`waiting-panel`, `waiting-api`, `waiting-session`, `ready`, and `error` phases.
## Boundary
The entry shell only links pages and renders already-observed panel state. It

View File

@@ -9,8 +9,8 @@ for machine-session load state. These helpers are pure so browser UI panels,
SDK-facing wrappers, and Node smoke tests can share the same data shape without
parsing log text.
`runtime/ui/ini-panel/session-workflow.js` contains the reusable UI workflows
for saving, restoring, and restore-loading a machine session snapshot as
host-boundary operations.
for saving, restoring, loading, and restore-loading a machine session snapshot
as host-boundary operations.
The helpers do not implement CNC behavior. G-code execution, canonical events,
tool handling, parameters, kinematics, and planner behavior remain owned by the
@@ -76,7 +76,10 @@ The persistence functions are injected by the caller:
The workflow loads the machine session snapshot, loads the persisted machine
text files so the editor can be restored, and optionally loads the machine
session into the interpreter WASM filesystem.
session into the interpreter WASM filesystem. The optional restore-load path
reuses `loadMachineSessionIntoWasmWorkflow()` so standalone load and
restore-load consumers share the same load summary, field state, and load log
line model.
When `loadSession` is omitted, the returned `fields` contain the OPFS snapshot
paths. When `loadSession` is supplied, the returned `fields` contain the loaded
@@ -101,7 +104,8 @@ The returned object has this shape:
loadedSession: { ... },
logLines: [
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot"
"Workflow: save-session-snapshot",
"Loaded machine session into the interpreter WASM filesystem."
]
}
```

View File

@@ -35,13 +35,7 @@ import {
runGcodeSessionWorkflow,
} from "./run-workflow.js";
import {
createMachineSessionLoadSummary,
machineSessionLoadLogLines,
} from "./session-load-summary.js";
import {
sessionSnapshotSummaryLogLines,
} from "./session-summary.js";
import {
loadMachineSessionIntoWasmWorkflow,
restoreMachineSessionSnapshotWorkflow,
saveMachineSessionSnapshotWorkflow,
} from "./session-workflow.js";
@@ -712,24 +706,17 @@ document.getElementById("restore-session-snapshot").addEventListener("click", as
document.getElementById("load-session").addEventListener("click", async () => {
try {
await loadMachineSessionIntoWasm();
const loadSummary = createMachineSessionLoadSummary(loadedSession);
const workflow = await loadMachineSessionIntoWasmWorkflow({
loadSession: () => loadMachineSessionIntoWasm(),
});
loadedSession = workflow.loadedSession;
updatePanelMachineSessionState({
sessionLoad: loadSummary,
fields: {
sessionIni: loadSummary.iniWasmPath,
sessionParameters: loadSummary.parameterWasmPath,
sessionToolTable: loadSummary.toolTableWasmPath,
},
sessionLoad: workflow.loadSummary,
fields: workflow.fields,
lastAction: "load-session",
error: null,
});
setLog(
[
"Loaded machine session into the interpreter WASM filesystem.",
...machineSessionLoadLogLines(loadSummary),
].join("\n"),
);
setLog(workflow.logLines.join("\n"));
} catch (error) {
updatePanelMachineSessionState({
lastAction: "load-session",

View File

@@ -1,3 +1,10 @@
import {
refreshMachineSessionControlPage,
} from "./control-page-refresh-workflow.js";
import {
loadMachineSessionForControlPageWorkflow,
} from "./control-page-session-workflow.js";
export function createMachineSessionControlPageController({
panelFrame,
statusNode,
@@ -34,19 +41,24 @@ export function createMachineSessionControlPageController({
}
function render() {
const view = getControlView();
if (!view) {
statusNode.textContent = "waiting for panel";
viewNode.textContent = "";
return null;
}
return refresh().renderResult;
}
statusNode.textContent = [
view.status.lastAction ?? "-",
view.status.runStatus ?? "-",
view.status.error ?? "-",
].join(" | ");
return renderControlView(viewNode, view);
function refresh() {
return refreshMachineSessionControlPage({
getControlView,
statusNode,
viewNode,
renderControlView,
});
}
function loadSessionState() {
return loadMachineSessionForControlPageWorkflow({
getPanelWindow,
getPanelApi,
getControlView,
});
}
function startPolling() {
@@ -71,7 +83,9 @@ export function createMachineSessionControlPageController({
getControlView,
getPanelApi,
getPanelWindow,
loadSessionState,
mount,
refresh,
render,
startPolling,
};

View File

@@ -0,0 +1,89 @@
export function controlPageStatusText(status = {}) {
return [
status.lastAction ?? "-",
status.runStatus ?? "-",
status.error ?? "-",
].join(" | ");
}
export function createControlPageRefreshState({
phase,
statusText,
view = null,
renderResult = null,
error = null,
} = {}) {
return {
phase,
statusText,
view,
renderResult,
error,
};
}
export function refreshMachineSessionControlPage({
getControlView,
statusNode,
viewNode,
renderControlView,
} = {}) {
if (typeof getControlView !== "function") {
throw new Error("A control view getter 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 view = null;
try {
view = getControlView() ?? null;
} catch (error) {
const statusText = `refresh failed | - | ${error.message}`;
statusNode.textContent = statusText;
viewNode.textContent = "";
return createControlPageRefreshState({
phase: "error",
statusText,
error: error.message,
});
}
if (!view) {
const statusText = "waiting for panel";
statusNode.textContent = statusText;
viewNode.textContent = "";
return createControlPageRefreshState({
phase: "waiting",
statusText,
});
}
const statusText = controlPageStatusText(view.status);
statusNode.textContent = statusText;
try {
const renderResult = renderControlView(viewNode, view);
return createControlPageRefreshState({
phase: "rendered",
statusText,
view,
renderResult,
});
} catch (error) {
const errorStatusText = `refresh failed | - | ${error.message}`;
statusNode.textContent = errorStatusText;
viewNode.textContent = "";
return createControlPageRefreshState({
phase: "error",
statusText: errorStatusText,
view,
error: error.message,
});
}
}

View File

@@ -0,0 +1,84 @@
export function createControlPageSessionLoadState({
phase,
panelReady = false,
apiReady = false,
controlViewReady = false,
stateBundle = null,
workflowStatus = null,
controlView = null,
error = null,
} = {}) {
return {
phase,
panelReady,
apiReady,
controlViewReady,
stateBundle,
workflowStatus,
controlView,
error,
};
}
export function loadMachineSessionForControlPageWorkflow({
getPanelWindow,
getPanelApi,
getControlView,
} = {}) {
if (typeof getPanelWindow !== "function") {
throw new Error("A panel window getter is required.");
}
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.");
}
let panelWindow = null;
let panelApi = null;
let controlView = null;
try {
panelWindow = getPanelWindow();
panelApi = getPanelApi();
controlView = getControlView();
} catch (error) {
return createControlPageSessionLoadState({
phase: "error",
error: error.message,
});
}
if (!panelWindow) {
return createControlPageSessionLoadState({
phase: "waiting-panel",
});
}
if (!panelApi) {
return createControlPageSessionLoadState({
phase: "waiting-api",
panelReady: true,
});
}
if (!controlView) {
return createControlPageSessionLoadState({
phase: "waiting-session",
panelReady: true,
apiReady: true,
stateBundle: panelApi.getMachineSessionStateBundle?.() ?? null,
workflowStatus: panelApi.getMachineSessionWorkflowStatus?.() ?? null,
});
}
return createControlPageSessionLoadState({
phase: "ready",
panelReady: true,
apiReady: true,
controlViewReady: true,
stateBundle: panelApi.getMachineSessionStateBundle?.() ?? null,
workflowStatus: panelApi.getMachineSessionWorkflowStatus?.() ?? null,
controlView,
});
}

View File

@@ -21,5 +21,7 @@ controller.mount();
window.linuxCncIniPanelControlPageApi = {
getControlView: controller.getControlView,
getPanelApi: controller.getPanelApi,
loadControlPageSessionState: controller.loadSessionState,
refreshControlPage: controller.refresh,
renderControlPage: controller.render,
};

View File

@@ -73,16 +73,16 @@
</section>
</main>
<script type="module">
import { getIniPanelEntryManifest } from "./ui-shell.js";
import { createIniPanelLauncherLinkViewModel } from "./ui-shell.js";
const manifest = getIniPanelEntryManifest();
for (const entry of manifest) {
const link = document.querySelector(`a[data-entry-id="${entry.id}"]`);
const links = createIniPanelLauncherLinkViewModel();
for (const linkModel of links) {
const link = document.querySelector(`a[data-entry-id="${linkModel.entryId}"]`);
if (!link) {
throw new Error(`Missing launcher entry ${entry.id}`);
throw new Error(`Missing launcher entry ${linkModel.entryId}`);
}
link.textContent = entry.title;
link.href = entry.href;
link.textContent = linkModel.label;
link.href = linkModel.href;
}
</script>
</body>

View File

@@ -37,6 +37,35 @@ export function sessionSnapshotRestoreLogLines(summary) {
];
}
export function createMachineSessionLoadFieldState(summary) {
return {
sessionIni: summary.iniWasmPath,
sessionParameters: summary.parameterWasmPath,
sessionToolTable: summary.toolTableWasmPath,
};
}
export function machineSessionLoadWorkflowLogLines(summary) {
return [
"Loaded machine session into the interpreter WASM filesystem.",
...machineSessionLoadLogLines(summary),
];
}
export async function loadMachineSessionIntoWasmWorkflow({ loadSession }) {
if (typeof loadSession !== "function") {
throw new Error("A machine session loader is required.");
}
const loadedSession = await loadSession();
const loadSummary = createMachineSessionLoadSummary(loadedSession);
return {
loadedSession,
loadSummary,
fields: createMachineSessionLoadFieldState(loadSummary),
logLines: machineSessionLoadWorkflowLogLines(loadSummary),
};
}
export async function saveMachineSessionSnapshotWorkflow({
machineId,
snapshotId,
@@ -105,20 +134,17 @@ export async function restoreMachineSessionSnapshotWorkflow({
logLines: sessionSnapshotRestoreLogLines(summary),
};
if (loadSession) {
const loadedSession = await loadSession();
const loadSummary = createMachineSessionLoadSummary(loadedSession);
result.loadedSession = loadedSession;
result.loadSummary = loadSummary;
const loadWorkflow = await loadMachineSessionIntoWasmWorkflow({ loadSession });
result.loadedSession = loadWorkflow.loadedSession;
result.loadSummary = loadWorkflow.loadSummary;
result.fields = {
...fields,
sessionIni: loadSummary.iniWasmPath,
sessionParameters: loadSummary.parameterWasmPath,
sessionToolTable: loadSummary.toolTableWasmPath,
...loadWorkflow.fields,
sessionGcode: summary.gcodeOpfsPath,
};
result.logLines = [
...sessionSnapshotSummaryLogLines(summary),
...machineSessionLoadLogLines(loadSummary),
...loadWorkflow.logLines,
`G-code: ${summary.gcodeOpfsPath ?? "-"}`,
];
}

View File

@@ -1,3 +1,32 @@
import {
INI_PANEL_ENTRIES,
getIniPanelEntryByHref,
getIniPanelEntryManifest,
} from "./entry-manifest.js";
import {
createMachineSessionControlPageController,
} from "./control-page-controller.js";
import {
renderMachineSessionControlView,
} from "./control-view-renderer.js";
import {
controlPageStatusText,
createControlPageRefreshState,
refreshMachineSessionControlPage,
} from "./control-page-refresh-workflow.js";
import {
createControlPageSessionLoadState,
loadMachineSessionForControlPageWorkflow,
} from "./control-page-session-workflow.js";
export {
controlPageStatusText,
createControlPageRefreshState,
refreshMachineSessionControlPage,
} from "./control-page-refresh-workflow.js";
export {
createControlPageSessionLoadState,
loadMachineSessionForControlPageWorkflow,
} from "./control-page-session-workflow.js";
export {
INI_PANEL_ENTRIES,
getIniPanelEntryByHref,
@@ -9,3 +38,32 @@ export {
export {
renderMachineSessionControlView,
} from "./control-view-renderer.js";
export function getVisibleIniPanelEntries() {
return getIniPanelEntryManifest();
}
export function createIniPanelLauncherLinkViewModel(entries = getVisibleIniPanelEntries()) {
return entries.map(({ id, href, title }) => ({
entryId: id,
href,
label: title,
}));
}
export function createIniPanelShellViewModel(entries = getVisibleIniPanelEntries()) {
return {
pages: entries.map(({ id, href, title }) => ({
id,
href,
title,
})),
launcherLinks: createIniPanelLauncherLinkViewModel(entries),
controlPage: {
createController: createMachineSessionControlPageController,
loadSessionState: loadMachineSessionForControlPageWorkflow,
refresh: refreshMachineSessionControlPage,
renderView: renderMachineSessionControlView,
},
};
}

View File

@@ -64,6 +64,8 @@ TOOL_TABLE = browser-tool.tbl
if (
!controlPageApi?.getControlView ||
!controlPageApi?.getPanelApi ||
!controlPageApi?.loadControlPageSessionState ||
!controlPageApi?.refreshControlPage ||
!controlPageApi?.renderControlPage
) {
throw new Error("control page API namespace did not expose control helpers");
@@ -82,6 +84,16 @@ TOOL_TABLE = browser-tool.tbl
() => controlPageApi.getControlView(),
"control page API view",
);
assertEqual(
controlPageApi.refreshControlPage().phase,
"rendered",
"control page refresh phase",
);
assertEqual(
controlPageApi.loadControlPageSessionState().phase,
"ready",
"control page session load phase",
);
panelDoc.getElementById("ini-editor").value = iniText;
panelDoc.getElementById("query").click();

View File

@@ -8,8 +8,9 @@
<pre id="status">running</pre>
<script type="module">
import {
createIniPanelLauncherLinkViewModel,
getIniPanelEntryByHref,
getIniPanelEntryManifest,
getVisibleIniPanelEntries,
} from "../../runtime/ui/ini-panel/ui-shell.js";
function assertEqual(actual, expected, label) {
@@ -30,8 +31,13 @@
}
try {
const manifest = getIniPanelEntryManifest();
assertEqual(manifest.length, 2, "launch manifest length");
const visibleEntries = getVisibleIniPanelEntries();
assertEqual(visibleEntries.length, 2, "launch visible entry length");
const launcherLinks = createIniPanelLauncherLinkViewModel();
assertEqual(launcherLinks.length, 2, "launcher link model length");
assertEqual(launcherLinks[0].entryId, "edit-run", "launcher link entry id");
assertEqual(launcherLinks[0].label, "Open edit and run panel", "launcher link label");
assertEqual(launcherLinks[0].href, "./index.html", "launcher link href");
assertEqual(getIniPanelEntryByHref("./index.html").id, "edit-run", "launch manifest edit entry");
assertEqual(
getIniPanelEntryByHref("./control-page.html").title,

View File

@@ -3,6 +3,9 @@ import assert from "node:assert/strict";
import {
createMachineSessionControlPageController,
} from "../../../runtime/ui/ini-panel/control-page-controller.js";
import {
controlPageStatusText,
} from "../../../runtime/ui/ini-panel/control-page-refresh-workflow.js";
function createPanelFrame(panelApi = null) {
const listeners = new Map();
@@ -36,6 +39,17 @@ const expectedView = {
},
],
};
const expectedStateBundle = {
report: {
readiness: {
ini: "INI WASM: ready",
},
},
};
const expectedWorkflowStatus = {
lastAction: "run-gcode",
error: null,
};
let intervalCallback = null;
const panelFrame = createPanelFrame();
@@ -66,15 +80,30 @@ assert.equal(typeof intervalCallback, "function");
const panelApi = {
getMachineSessionControlView: () => expectedView,
getMachineSessionStateBundle: () => expectedStateBundle,
getMachineSessionWorkflowStatus: () => expectedWorkflowStatus,
};
panelFrame.contentWindow = { linuxCncIniPanelApi: panelApi };
assert.equal(controller.getPanelApi(), panelApi);
assert.equal(controller.getControlView(), expectedView);
assert.deepEqual(controller.loadSessionState(), {
phase: "ready",
panelReady: true,
apiReady: true,
controlViewReady: true,
stateBundle: expectedStateBundle,
workflowStatus: expectedWorkflowStatus,
controlView: expectedView,
error: null,
});
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 refreshResult = controller.refresh();
assert.equal(refreshResult.phase, "rendered");
assert.equal(refreshResult.statusText, controlPageStatusText(expectedView.status));
const mountedFrame = createPanelFrame(panelApi);
const mountedStatusNode = { textContent: "" };

View File

@@ -0,0 +1,126 @@
import assert from "node:assert/strict";
import {
controlPageStatusText,
createControlPageRefreshState,
refreshMachineSessionControlPage,
} from "../../../runtime/ui/ini-panel/control-page-refresh-workflow.js";
const expectedView = {
status: {
lastAction: "run-gcode",
runStatus: "ok",
error: null,
},
sections: [
{
id: "run",
rows: [
{ label: "Canonical events", value: 2 },
],
},
],
};
assert.equal(controlPageStatusText(expectedView.status), "run-gcode | ok | -");
assert.deepEqual(createControlPageRefreshState({ phase: "waiting", statusText: "waiting for panel" }), {
phase: "waiting",
statusText: "waiting for panel",
view: null,
renderResult: null,
error: null,
});
const waitingStatusNode = { textContent: "" };
const waitingViewNode = { textContent: "previous" };
assert.deepEqual(
refreshMachineSessionControlPage({
getControlView: () => null,
statusNode: waitingStatusNode,
viewNode: waitingViewNode,
renderControlView: () => "unused",
}),
{
phase: "waiting",
statusText: "waiting for panel",
view: null,
renderResult: null,
error: null,
},
);
assert.equal(waitingStatusNode.textContent, "waiting for panel");
assert.equal(waitingViewNode.textContent, "");
const renderedStatusNode = { textContent: "" };
const renderedViewNode = { textContent: "" };
let renderedInput = null;
assert.deepEqual(
refreshMachineSessionControlPage({
getControlView: () => expectedView,
statusNode: renderedStatusNode,
viewNode: renderedViewNode,
renderControlView: (container, view) => {
renderedInput = { container, view };
return { rendered: true };
},
}),
{
phase: "rendered",
statusText: "run-gcode | ok | -",
view: expectedView,
renderResult: { rendered: true },
error: null,
},
);
assert.deepEqual(renderedInput, { container: renderedViewNode, view: expectedView });
assert.equal(renderedStatusNode.textContent, "run-gcode | ok | -");
const getterErrorStatusNode = { textContent: "" };
const getterErrorViewNode = { textContent: "previous" };
assert.deepEqual(
refreshMachineSessionControlPage({
getControlView: () => {
throw new Error("panel unavailable");
},
statusNode: getterErrorStatusNode,
viewNode: getterErrorViewNode,
renderControlView: () => "unused",
}),
{
phase: "error",
statusText: "refresh failed | - | panel unavailable",
view: null,
renderResult: null,
error: "panel unavailable",
},
);
assert.equal(getterErrorViewNode.textContent, "");
const renderErrorStatusNode = { textContent: "" };
const renderErrorViewNode = { textContent: "previous" };
assert.deepEqual(
refreshMachineSessionControlPage({
getControlView: () => expectedView,
statusNode: renderErrorStatusNode,
viewNode: renderErrorViewNode,
renderControlView: () => {
throw new Error("renderer unavailable");
},
}),
{
phase: "error",
statusText: "refresh failed | - | renderer unavailable",
view: expectedView,
renderResult: null,
error: "renderer unavailable",
},
);
assert.equal(renderErrorStatusNode.textContent, "refresh failed | - | renderer unavailable");
assert.equal(renderErrorViewNode.textContent, "");
assert.throws(
() => refreshMachineSessionControlPage(),
/control view getter/,
);
console.log("ini_panel_control_page_refresh_workflow_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_control_page_refresh_workflow.mjs"

View File

@@ -0,0 +1,121 @@
import assert from "node:assert/strict";
import {
createControlPageSessionLoadState,
loadMachineSessionForControlPageWorkflow,
} from "../../../runtime/ui/ini-panel/control-page-session-workflow.js";
const stateBundle = {
report: {
readiness: {
ini: "INI WASM: ready",
interp: "Interpreter WASM: ready",
opfs: "OPFS: ready",
},
},
};
const workflowStatus = {
lastAction: "load-session",
error: null,
};
const controlView = {
status: {
lastAction: "load-session",
runStatus: null,
error: null,
},
sections: [],
};
assert.deepEqual(createControlPageSessionLoadState({ phase: "waiting-panel" }), {
phase: "waiting-panel",
panelReady: false,
apiReady: false,
controlViewReady: false,
stateBundle: null,
workflowStatus: null,
controlView: null,
error: null,
});
assert.deepEqual(
loadMachineSessionForControlPageWorkflow({
getPanelWindow: () => null,
getPanelApi: () => null,
getControlView: () => null,
}),
createControlPageSessionLoadState({ phase: "waiting-panel" }),
);
assert.deepEqual(
loadMachineSessionForControlPageWorkflow({
getPanelWindow: () => ({}),
getPanelApi: () => null,
getControlView: () => null,
}),
createControlPageSessionLoadState({
phase: "waiting-api",
panelReady: true,
}),
);
const panelApiWithoutControlView = {
getMachineSessionStateBundle: () => stateBundle,
getMachineSessionWorkflowStatus: () => workflowStatus,
};
assert.deepEqual(
loadMachineSessionForControlPageWorkflow({
getPanelWindow: () => ({}),
getPanelApi: () => panelApiWithoutControlView,
getControlView: () => null,
}),
createControlPageSessionLoadState({
phase: "waiting-session",
panelReady: true,
apiReady: true,
stateBundle,
workflowStatus,
}),
);
const panelApi = {
...panelApiWithoutControlView,
getMachineSessionControlView: () => controlView,
};
assert.deepEqual(
loadMachineSessionForControlPageWorkflow({
getPanelWindow: () => ({}),
getPanelApi: () => panelApi,
getControlView: () => controlView,
}),
createControlPageSessionLoadState({
phase: "ready",
panelReady: true,
apiReady: true,
controlViewReady: true,
stateBundle,
workflowStatus,
controlView,
}),
);
assert.deepEqual(
loadMachineSessionForControlPageWorkflow({
getPanelWindow: () => {
throw new Error("cross-origin frame");
},
getPanelApi: () => null,
getControlView: () => null,
}),
createControlPageSessionLoadState({
phase: "error",
error: "cross-origin frame",
}),
);
assert.throws(
() => loadMachineSessionForControlPageWorkflow(),
/panel window getter/,
);
console.log("ini_panel_control_page_session_workflow_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_control_page_session_workflow.mjs"

View File

@@ -5,19 +5,39 @@ import { dirname, resolve } from "node:path";
import {
INI_PANEL_ENTRIES,
getIniPanelEntryManifest,
controlPageStatusText,
createControlPageSessionLoadState,
createIniPanelLauncherLinkViewModel,
createIniPanelShellViewModel,
loadMachineSessionForControlPageWorkflow,
refreshMachineSessionControlPage,
getVisibleIniPanelEntries,
} from "../../../runtime/ui/ini-panel/ui-shell.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, "../../..");
const docText = readFileSync(resolve(root, "docs/panel-entry.md"), "utf8");
const shellText = readFileSync(resolve(root, "runtime/ui/ini-panel/ui-shell.js"), "utf8");
const refreshWorkflowText = readFileSync(
resolve(root, "runtime/ui/ini-panel/control-page-refresh-workflow.js"),
"utf8",
);
const sessionWorkflowText = readFileSync(
resolve(root, "runtime/ui/ini-panel/control-page-session-workflow.js"),
"utf8",
);
const requiredShellExports = [
"INI_PANEL_ENTRIES",
"controlPageStatusText",
"createControlPageSessionLoadState",
"createIniPanelLauncherLinkViewModel",
"createIniPanelShellViewModel",
"getIniPanelEntryByHref",
"getIniPanelEntryManifest",
"getVisibleIniPanelEntries",
"createMachineSessionControlPageController",
"loadMachineSessionForControlPageWorkflow",
"refreshMachineSessionControlPage",
"renderMachineSessionControlView",
];
@@ -26,6 +46,23 @@ for (const exportName of requiredShellExports) {
assert.match(docText, new RegExp(`\\b${exportName}\\b`));
}
for (const exportName of [
"controlPageStatusText",
"createControlPageRefreshState",
"refreshMachineSessionControlPage",
]) {
assert.match(refreshWorkflowText, new RegExp(`\\bexport function ${exportName}\\b`));
assert.match(docText, new RegExp(`\\b${exportName}\\b`));
}
for (const exportName of [
"createControlPageSessionLoadState",
"loadMachineSessionForControlPageWorkflow",
]) {
assert.match(sessionWorkflowText, new RegExp(`\\bexport function ${exportName}\\b`));
assert.match(docText, new RegExp(`\\b${exportName}\\b`));
}
const requiredPages = [
["Launch", "runtime/ui/ini-panel/launch.html"],
["Edit and run", "runtime/ui/ini-panel/index.html"],
@@ -36,7 +73,32 @@ for (const [label, path] of requiredPages) {
assert.match(docText, new RegExp(`\\| ${label} \\| \`${path.replaceAll("/", "\\/")}\``));
}
assert.deepEqual(getIniPanelEntryManifest(), INI_PANEL_ENTRIES);
assert.deepEqual(getVisibleIniPanelEntries(), INI_PANEL_ENTRIES);
assert.deepEqual(
createIniPanelLauncherLinkViewModel(),
INI_PANEL_ENTRIES.map(({ id, href, title }) => ({
entryId: id,
href,
label: title,
})),
);
assert.deepEqual(createIniPanelShellViewModel().pages, getVisibleIniPanelEntries());
assert.deepEqual(createIniPanelShellViewModel().launcherLinks, createIniPanelLauncherLinkViewModel());
assert.equal(controlPageStatusText({ lastAction: "run-gcode", runStatus: "ok", error: null }), "run-gcode | ok | -");
assert.deepEqual(createControlPageSessionLoadState({ phase: "waiting-panel" }), {
phase: "waiting-panel",
panelReady: false,
apiReady: false,
controlViewReady: false,
stateBundle: null,
workflowStatus: null,
controlView: null,
error: null,
});
assert.equal(typeof createIniPanelShellViewModel().controlPage.createController, "function");
assert.equal(createIniPanelShellViewModel().controlPage.loadSessionState, loadMachineSessionForControlPageWorkflow);
assert.equal(createIniPanelShellViewModel().controlPage.refresh, refreshMachineSessionControlPage);
assert.equal(typeof createIniPanelShellViewModel().controlPage.renderView, "function");
for (const entry of INI_PANEL_ENTRIES) {
const escapedHref = entry.href.replace(".", "\\.");

View File

@@ -1,10 +1,68 @@
import assert from "node:assert/strict";
import {
createMachineSessionLoadFieldState,
loadMachineSessionIntoWasmWorkflow,
machineSessionLoadWorkflowLogLines,
restoreMachineSessionSnapshotWorkflow,
saveMachineSessionSnapshotWorkflow,
} from "../../../runtime/ui/ini-panel/session-workflow.js";
const loadedSessionFixture = {
ini: {
opfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
wasmPath: "/work/session-machine.ini",
},
parameters: {
opfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
wasmPath: "/work/session-linuxcnc.var",
result: "restore_parameters=0",
},
toolTable: {
opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
wasmPath: "/work/session-tool.tbl",
result: "tooldata_load=0",
},
};
const loadWorkflowCalls = [];
const loadWorkflow = await loadMachineSessionIntoWasmWorkflow({
loadSession: async () => {
loadWorkflowCalls.push(["loadSession"]);
return loadedSessionFixture;
},
});
assert.deepEqual(loadWorkflowCalls, [["loadSession"]]);
assert.deepEqual(loadWorkflow.loadSummary, {
iniOpfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
iniWasmPath: "/work/session-machine.ini",
parameterOpfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
parameterWasmPath: "/work/session-linuxcnc.var",
parameterResult: "restore_parameters=0",
toolTableOpfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
toolTableWasmPath: "/work/session-tool.tbl",
toolTableResult: "tooldata_load=0",
});
assert.deepEqual(loadWorkflow.fields, {
sessionIni: "/work/session-machine.ini",
sessionParameters: "/work/session-linuxcnc.var",
sessionToolTable: "/work/session-tool.tbl",
});
assert.deepEqual(createMachineSessionLoadFieldState(loadWorkflow.loadSummary), loadWorkflow.fields);
assert.deepEqual(machineSessionLoadWorkflowLogLines(loadWorkflow.loadSummary), [
"Loaded machine session into the interpreter WASM filesystem.",
"INI: linuxcnc/machines/xyzab-tdr/machine.ini -> /work/session-machine.ini",
"Parameter file: linuxcnc/machines/xyzab-tdr/linuxcnc.var -> /work/session-linuxcnc.var",
"Parameters: restore_parameters=0",
"Tool table file: linuxcnc/machines/xyzab-tdr/tool.tbl -> /work/session-tool.tbl",
"Tool table: tooldata_load=0",
]);
await assert.rejects(
() => loadMachineSessionIntoWasmWorkflow({}),
/machine session loader/,
);
const calls = [];
const saveWorkflow = await saveMachineSessionSnapshotWorkflow({
machineId: "xyzab-tdr",
@@ -199,22 +257,7 @@ const restoreLoadWorkflow = await restoreMachineSessionSnapshotWorkflow({
},
loadSession: async () => {
restoreLoadCalls.push(["loadSession"]);
return {
ini: {
opfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
wasmPath: "/work/session-machine.ini",
},
parameters: {
opfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
wasmPath: "/work/session-linuxcnc.var",
result: "restore_parameters=0",
},
toolTable: {
opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
wasmPath: "/work/session-tool.tbl",
result: "tooldata_load=0",
},
};
return loadedSessionFixture;
},
});
@@ -236,11 +279,14 @@ assert.deepEqual(restoreLoadWorkflow.fields, {
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
});
assert.deepEqual(restoreLoadWorkflow.loadSummary, loadWorkflow.loadSummary);
assert.equal(restoreLoadWorkflow.loadedSession, loadedSessionFixture);
assert.deepEqual(restoreLoadWorkflow.logLines, [
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot",
"Default G-code: linuxcnc/gcode/ui-session.ngc",
"Loaded machine session into the interpreter WASM filesystem.",
"INI: linuxcnc/machines/xyzab-tdr/machine.ini -> /work/session-machine.ini",
"Parameter file: linuxcnc/machines/xyzab-tdr/linuxcnc.var -> /work/session-linuxcnc.var",
"Parameters: restore_parameters=0",

View File

@@ -2,20 +2,88 @@ import assert from "node:assert/strict";
import {
INI_PANEL_ENTRIES,
controlPageStatusText,
createIniPanelLauncherLinkViewModel,
createIniPanelShellViewModel,
createControlPageRefreshState,
createControlPageSessionLoadState,
createMachineSessionControlPageController,
getIniPanelEntryByHref,
getIniPanelEntryManifest,
getVisibleIniPanelEntries,
loadMachineSessionForControlPageWorkflow,
refreshMachineSessionControlPage,
renderMachineSessionControlView,
} from "../../../runtime/ui/ini-panel/ui-shell.js";
assert.deepEqual(getIniPanelEntryManifest(), INI_PANEL_ENTRIES);
assert.deepEqual(getVisibleIniPanelEntries(), INI_PANEL_ENTRIES);
assert.equal(controlPageStatusText({ lastAction: "x", runStatus: "ok", error: null }), "x | ok | -");
assert.deepEqual(createControlPageRefreshState({ phase: "waiting", statusText: "waiting for panel" }), {
phase: "waiting",
statusText: "waiting for panel",
view: null,
renderResult: null,
error: null,
});
assert.deepEqual(createControlPageSessionLoadState({ phase: "waiting-panel" }), {
phase: "waiting-panel",
panelReady: false,
apiReady: false,
controlViewReady: false,
stateBundle: null,
workflowStatus: null,
controlView: null,
error: null,
});
assert.equal(getIniPanelEntryByHref("./index.html")?.id, "edit-run");
assert.equal(typeof createMachineSessionControlPageController, "function");
assert.equal(typeof loadMachineSessionForControlPageWorkflow, "function");
assert.equal(typeof refreshMachineSessionControlPage, "function");
assert.equal(typeof renderMachineSessionControlView, "function");
const shellManifest = getIniPanelEntryManifest();
shellManifest[0].title = "changed";
assert.equal(INI_PANEL_ENTRIES[0].title, "Open edit and run panel");
const visibleEntries = getVisibleIniPanelEntries();
visibleEntries[0].title = "changed";
assert.equal(INI_PANEL_ENTRIES[0].title, "Open edit and run panel");
assert.deepEqual(createIniPanelLauncherLinkViewModel(), [
{
entryId: "edit-run",
href: "./index.html",
label: "Open edit and run panel",
},
{
entryId: "control-view",
href: "./control-page.html",
label: "Open read-only control view",
},
]);
const launcherLinks = createIniPanelLauncherLinkViewModel();
launcherLinks[0].label = "changed";
assert.equal(INI_PANEL_ENTRIES[0].title, "Open edit and run panel");
const shellViewModel = createIniPanelShellViewModel();
assert.deepEqual(shellViewModel.pages, [
{
id: "edit-run",
href: "./index.html",
title: "Open edit and run panel",
},
{
id: "control-view",
href: "./control-page.html",
title: "Open read-only control view",
},
]);
assert.deepEqual(shellViewModel.launcherLinks, createIniPanelLauncherLinkViewModel());
assert.equal(shellViewModel.controlPage.createController, createMachineSessionControlPageController);
assert.equal(shellViewModel.controlPage.loadSessionState, loadMachineSessionForControlPageWorkflow);
assert.equal(shellViewModel.controlPage.refresh, refreshMachineSessionControlPage);
assert.equal(shellViewModel.controlPage.renderView, renderMachineSessionControlView);
shellViewModel.pages[0].title = "changed";
shellViewModel.launcherLinks[0].label = "changed";
assert.equal(INI_PANEL_ENTRIES[0].title, "Open edit and run panel");
const panelFrame = {
contentWindow: {
@@ -48,5 +116,7 @@ const controller = createMachineSessionControlPageController({
assert.deepEqual(controller.render(), { rendered: true });
assert.equal(statusNode.textContent, "run-gcode | ok | -");
assert.equal(renderedView.status.runStatus, "ok");
assert.equal(controller.loadSessionState().phase, "ready");
assert.equal(controller.refresh().phase, "rendered");
console.log("ini_panel_ui_shell_node_smoke=ok");

View File

@@ -10,6 +10,8 @@ ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
"$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"
"$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_entry_manifest.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_ui_shell.sh"
"$ROOT_DIR/tests/ui/node/verify_ini_panel_entry_docs.sh"