diff --git a/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md b/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
index 51867a3..ba41ba3 100644
--- a/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
+++ b/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md
@@ -10852,3 +10852,30 @@ nginx: configuration file /etc/nginx/nginx.conf test is successful
17. 执行 `git status --short`,确认提交推送后仅剩未跟踪生成目录 `app/dist/` 和 `tools/__pycache__/`。
18. 执行 `git log -1 --oneline` 和 `git ls-remote origin refs/heads/master`,确认本地最新提交和远端 `master` 均为 `8fd9952`。
19. 本轮结论:T-034 tool-offset 闭环相关源码、测试、证据和工作文档已提交并推送到云仓库;构建产物和 Python 缓存未提交。
+
+## 2026-07-02 执行日志:xyzbc-trt Web OPFS 与 AXIS 首屏等效
+
+本轮接续 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working` 开发,用户明确要求“Web 的文件使用 OPFS 系统”。执行过程如下:
+
+1. 读取并确认当前项目状态:`working/03-推进台账.md`、`working/04-任务矩阵.md`、`app/src/runtime/linuxcnc-machine-file-staging.js`、`app/src/runtime/five-axis-session.js`、`app/src/state/store.js`、`tests/browser/xyzbc_trt_browser_smoke.html` 等文件。
+2. 发现现有浏览器环境具备 OPFS 时会使用 `navigator.storage.getDirectory()`,但仍允许 memory fallback;Node evidence 与 Node smoke 显式使用 `createMemorySessionStorage()`,不能代表真实浏览器 OPFS。
+3. 修改 `app/src/runtime/linuxcnc-machine-file-staging.js`,为 machine file staging 增加 `requireOpfs` 选项:当要求 OPFS 而浏览器 OPFS 不可用时直接报错,不再静默回退到 memory。
+4. 修改 `app/src/runtime/five-axis-session.js`,为 session snapshot 保存/恢复增加 `requireOpfs` 选项。
+5. 修改 `app/src/runtime/tool-db-simulation.js`,为 tool table 保存增加 `requireOpfs` 选项。
+6. 修改 `app/src/state/store.js`,增加 `withDefaultWebOpfsRequirement()`:浏览器 UI 默认要求 OPFS;显式传入 storage 或 Node 环境保持原测试能力。
+7. 修改 `app/src/main.js`,默认启动的 machine file seed 调用 `store.stageMachineFiles({ requireOpfs: true })`。
+8. 扩展 `tests/browser/xyzbc_trt_browser_smoke.html`:断言 machine file seed 和 store staging 的 `storageMode=opfs`,并从 OPFS 读回 `xyzbc_switchkins.ngc`,确认包含 `M428/M429`;调用 `api.saveSession()` 并从 OPFS 读回 session JSON,确认 `machineProfile=xyzbc-trt`;调用 `api.saveToolDb()` 并从 OPFS 读回 tool table,确认包含 `T2` 和 `Z10`。
+9. 同步完成 T-026 AXIS 首屏等效:修改 `app/src/ui/gmoccapy-shell.js` 和 `app/src/styles/gmoccapy.css`,增加首屏状态带和 `data-axis-ui-*` 验收标记,覆盖程序、坐标、状态、MDI、switchkins、override、tool、preview、execution、axis-buttons。
+10. 修改 `tools/collect-web-xyzbc-trt-evidence.mjs`,新增 `axisMainUi` evidence 与 `coverage.axisMainUiEquivalent`。说明:该 Node evidence 仍为内存采集,真实浏览器 OPFS 由 browser smoke 验证。
+11. 修改 `tools/compare-xyzbc-trt-evidence.mjs`,新增 AXIS 首屏等效检查。
+12. 运行验证:`npm --prefix app run smoke:node` 通过,输出 `xyzbc_trt_web_app_smoke=ok`。
+13. 运行验证:`npm --prefix app run build` 通过,输出 `gmoccapy_static_build=ok`。
+14. 运行验证:`npm --prefix app run smoke:browser` 通过,输出 `xyzbc_trt_browser_smoke=ok`;此验证包含真实浏览器 OPFS 写入和读回。
+15. 运行验证:`npm --prefix app run evidence:web` 通过,生成 `working/evidence/web-xyzbc-trt-evidence.json`。
+16. 运行验证:`npm --prefix app run evidence:compare` 生成真实失败报告,输出 `compare_xyzbc_trt_status=fail fail_count=5`;当前为 20/25 通过,剩余 5 项仍来自 native preview/execution path 样本缺失。
+17. 更新 `working/03-推进台账.md`,记录本轮 OPFS 强制与 AXIS 首屏等效实现。
+18. 更新 `working/04-任务矩阵.md`:T-006/T-014/T-016 增加真实 OPFS 验收描述;T-017 更新为 20/25;T-026 标记完成。
+19. 更新 `working/05-验收证据.md`,新增“Web OPFS 与 AXIS 首屏等效验收”章节。
+20. 保留既有未跟踪输出 `app/dist/` 和 `tools/__pycache__/`,未清理或回退用户/构建产物。
+
+本轮结论:目标 Web app 的浏览器文件链路已经默认强制使用 OPFS,browser smoke 已证明 machine files、session snapshot、tool table 均写入 OPFS 并可读回。Node evidence 仍使用内存存储,仅作为非浏览器数据采集,不作为真实 Web 文件系统证明。
diff --git a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/main.js b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/main.js
index 23c6fea..64ef0f8 100644
--- a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/main.js
+++ b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/main.js
@@ -93,7 +93,7 @@ async function ensureMachineFilesForProfile(store) {
if (state.machineFileStaging?.status === "staged" && state.machineFileStaging?.profileId === state.machineProfile) {
return state.machineFileStaging;
}
- return await store.stageMachineFiles();
+ return await store.stageMachineFiles({ requireOpfs: true });
} catch (error) {
return {
apiName: "web-rtcp-5axis-machine-file-staging-seed",
diff --git a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/five-axis-session.js b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/five-axis-session.js
index c134765..070f8b1 100644
--- a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/five-axis-session.js
+++ b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/five-axis-session.js
@@ -184,18 +184,22 @@ export function detectBrowserStorageCapability(globalScope = globalThis) {
function resolveSessionStorage(options = {}) {
if (options.storage) {
+ const mode = options.storageMode || storageModeFor(options.storage);
+ if (options.requireOpfs === true && mode !== "opfs") {
+ throw new Error(`OPFS storage required for session snapshots, got ${mode}`);
+ }
return {
storage: options.storage,
- mode: options.storageMode || storageModeFor(options.storage),
+ mode,
capability: {
apiName: "web-rtcp-5axis-storage-capability",
- opfsAvailable: options.storageMode === "opfs",
- opfsUnavailable: options.storageMode !== "opfs",
+ opfsAvailable: mode === "opfs",
+ opfsUnavailable: mode !== "opfs",
secureContext: true,
forcedUnavailable: false,
hasNavigatorStorage: false,
hasGetDirectory: typeof options.storage.getDirectory === "function",
- fallbackMode: options.storageMode === "opfs" ? null : options.storageMode || "custom",
+ fallbackMode: mode === "opfs" ? null : mode || "custom",
reason: "explicit_storage",
},
};
@@ -209,6 +213,9 @@ function resolveSessionStorage(options = {}) {
capability,
};
}
+ if (options.requireOpfs === true) {
+ throw new Error(`OPFS storage required for session snapshots: ${capability.reason}`);
+ }
browserMemorySessionStorage ??= createMemorySessionStorage();
return {
storage: browserMemorySessionStorage,
diff --git a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js
index 1217af3..0a375fb 100644
--- a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js
+++ b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js
@@ -241,15 +241,19 @@ export function detectMachineFileStorageCapability(globalScope = globalThis) {
function resolveMachineFileStorage(options = {}) {
if (options.storage) {
+ const mode = options.storageMode || storageModeFor(options.storage);
+ if (options.requireOpfs === true && mode !== "opfs") {
+ throw new Error(`OPFS storage required for machine files, got ${mode}`);
+ }
return {
storage: options.storage,
- mode: options.storageMode || storageModeFor(options.storage),
+ mode,
capability: {
apiName: "web-rtcp-5axis-machine-file-storage-capability",
- opfsAvailable: options.storageMode === "opfs",
- opfsUnavailable: options.storageMode !== "opfs",
+ opfsAvailable: mode === "opfs",
+ opfsUnavailable: mode !== "opfs",
secureContext: true,
- fallbackMode: options.storageMode === "opfs" ? null : options.storageMode || "custom",
+ fallbackMode: mode === "opfs" ? null : mode || "custom",
reason: "explicit_storage",
},
};
@@ -262,6 +266,9 @@ function resolveMachineFileStorage(options = {}) {
capability,
};
}
+ if (options.requireOpfs === true) {
+ throw new Error(`OPFS storage required for machine files: ${capability.reason}`);
+ }
browserMemoryMachineFileStorage ??= createMemoryStorage();
return {
storage: browserMemoryMachineFileStorage,
diff --git a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/tool-db-simulation.js b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/tool-db-simulation.js
index 41991c3..b23bff4 100644
--- a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/tool-db-simulation.js
+++ b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/tool-db-simulation.js
@@ -998,14 +998,18 @@ function basename(path = "") {
function resolveToolDbStorage(options = {}) {
if (options.storage) {
+ const mode = options.storageMode || storageModeFor(options.storage);
+ if (options.requireOpfs === true && mode !== "opfs") {
+ throw new Error(`OPFS storage required for tool DB, got ${mode}`);
+ }
return {
storage: options.storage,
- mode: options.storageMode || storageModeFor(options.storage),
+ mode,
capability: {
apiName: "web-rtcp-5axis-tool-db-storage-capability",
- opfsAvailable: options.storageMode === "opfs",
- opfsUnavailable: options.storageMode !== "opfs",
- fallbackMode: options.storageMode === "opfs" ? null : options.storageMode || "custom",
+ opfsAvailable: mode === "opfs",
+ opfsUnavailable: mode !== "opfs",
+ fallbackMode: mode === "opfs" ? null : mode || "custom",
reason: "explicit_storage",
},
};
@@ -1018,6 +1022,9 @@ function resolveToolDbStorage(options = {}) {
capability,
};
}
+ if (options.requireOpfs === true) {
+ throw new Error(`OPFS storage required for tool DB: ${capability.reason}`);
+ }
browserMemoryToolDbStorage ??= createMemoryToolDbStorage();
return {
storage: browserMemoryToolDbStorage,
diff --git a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/state/store.js b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/state/store.js
index 12624ca..a2aacf6 100644
--- a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/state/store.js
+++ b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/state/store.js
@@ -99,6 +99,14 @@ function createTaskHalStatusLoopState({
};
}
+function withDefaultWebOpfsRequirement(options = {}) {
+ if (options.storage || options.requireOpfs !== undefined) {
+ return options;
+ }
+ const isBrowser = typeof globalThis.window === "object" || typeof globalThis.document === "object";
+ return isBrowser ? { ...options, requireOpfs: true } : options;
+}
+
const initialState = {
machineProfile: "xyzbc-trt",
availableProfiles: fiveAxisProfiles.map(({ id, title, traj, kinematicsModuleId, kinematics }) => ({
@@ -2061,13 +2069,15 @@ export function createSimulationStore(seed = {}) {
const saveSession = async (options = {}) => {
dispatch({ type: "SESSION_SAVE_STARTED" });
try {
+ const storageOptions = withDefaultWebOpfsRequirement(options);
const sessionId = options.sessionId || state.sessionPersistence.sessionId;
const filename = options.filename || state.sessionPersistence.filename;
const payload = createFiveAxisSessionPayload(state);
const { snapshot, path, storageMode, storageCapability } = await saveFiveAxisSessionSnapshot(sessionId, payload, {
filename,
- storage: options.storage,
- storageMode: options.storageMode,
+ storage: storageOptions.storage,
+ storageMode: storageOptions.storageMode,
+ requireOpfs: storageOptions.requireOpfs,
metadata: options.metadata,
});
dispatch({
@@ -2087,12 +2097,14 @@ export function createSimulationStore(seed = {}) {
const restoreSession = async (options = {}) => {
dispatch({ type: "SESSION_RESTORE_STARTED" });
try {
+ const storageOptions = withDefaultWebOpfsRequirement(options);
const sessionId = options.sessionId || state.sessionPersistence.sessionId;
const filename = options.filename || state.sessionPersistence.filename;
const { snapshot, path, storageMode, storageCapability } = await loadFiveAxisSessionSnapshot(sessionId, {
filename,
- storage: options.storage,
- storageMode: options.storageMode,
+ storage: storageOptions.storage,
+ storageMode: storageOptions.storageMode,
+ requireOpfs: storageOptions.requireOpfs,
});
dispatch({
type: "SESSION_RESTORE_COMPLETE",
@@ -2113,8 +2125,10 @@ export function createSimulationStore(seed = {}) {
const stageMachineFiles = async (options = {}) => {
dispatch({ type: "MACHINE_FILE_STAGING_STARTED" });
try {
+ const storageOptions = withDefaultWebOpfsRequirement(options);
const { plan, save } = await stageProfileMachineFiles(state.profile, {
...options,
+ ...storageOptions,
iniText: options.iniText || state.linuxCncIniConfig?.sourceText,
});
dispatch({ type: "MACHINE_FILE_STAGING_COMPLETE", plan, save });
@@ -2153,7 +2167,7 @@ export function createSimulationStore(seed = {}) {
if (!state.toolDbSimulation) {
throw new Error("tool DB simulation is not ready");
}
- const save = await saveToolDbSimulation(state.toolDbSimulation, options);
+ const save = await saveToolDbSimulation(state.toolDbSimulation, withDefaultWebOpfsRequirement(options));
setState({
toolDbSimulation: save.toolDb,
toolDbReadiness: createToolDbReadiness(save.toolDb),
diff --git a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/styles/gmoccapy.css b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/styles/gmoccapy.css
index 81e4a2b..d548a28 100644
--- a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/styles/gmoccapy.css
+++ b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/styles/gmoccapy.css
@@ -754,6 +754,38 @@ button[data-icon-name] span {
background: #eeeae3;
}
+.axis-first-screen-strip {
+ display: grid;
+ grid-template-columns: repeat(5, minmax(96px, 1fr));
+ gap: 3px;
+ padding: 5px 6px;
+ border-bottom: 1px solid #aaa59b;
+ background: #151515;
+ color: #e9f8e9;
+ font: 11px/1.2 "Courier New", monospace;
+}
+
+.axis-first-screen-strip span {
+ min-width: 0;
+ overflow: hidden;
+ padding: 3px 5px;
+ border: 1px solid #3e5f43;
+ background: #071108;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.axis-first-screen-strip b {
+ color: var(--green);
+ font-weight: 700;
+}
+
+.axis-first-screen-strip [data-axis-ui-capability-ready="false"] {
+ border-color: #7a6840;
+ background: #1b1508;
+ color: #f0d58c;
+}
+
.tabs {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
@@ -1074,6 +1106,10 @@ button[data-icon-name] span {
.spindle h2 {
font-size: 12px;
}
+
+ .axis-first-screen-strip {
+ grid-template-columns: repeat(2, minmax(90px, 1fr));
+ }
}
@media (max-height: 720px) {
diff --git a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/ui/gmoccapy-shell.js b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/ui/gmoccapy-shell.js
index 284608e..9873c9d 100644
--- a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/ui/gmoccapy-shell.js
+++ b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/ui/gmoccapy-shell.js
@@ -50,6 +50,7 @@ export function mountGmoccapyShell(root, store) {
}
function render(regions, state, dispatch) {
+ renderShellEquivalenceDataset(regions, state);
renderTitlebar(regions.titlebar, state);
renderPreview(regions.preview, state, dispatch);
renderDro(regions.dro, state);
@@ -61,6 +62,26 @@ function render(regions, state, dispatch) {
renderBottomControls(regions["bottom-controls"], state, dispatch);
}
+function renderShellEquivalenceDataset(regions, state) {
+ const sectionMap = {
+ preview: "preview-toolpath-and-machine-model",
+ dro: "coordinates-dro",
+ gcode: "program-and-mdi",
+ "status-sidebar": "status-mode-and-switchkins",
+ "info-tabs": "execution-tool-and-runtime-state",
+ override: "feed-rapid-override",
+ "spindle-coolant": "spindle-coolant",
+ "bottom-controls": "run-jog-home-controls",
+ };
+ for (const [name, section] of Object.entries(sectionMap)) {
+ const element = regions[name];
+ if (!element) continue;
+ element.dataset.axisUiSection = section;
+ element.dataset.axisUiProfile = state.machineProfile;
+ element.dataset.axisUiCoordinates = state.profile?.traj?.coordinates || "";
+ }
+}
+
function renderTitlebar(element, state) {
const currentLine = currentGcodeExecutionLine(state);
@@ -599,7 +620,20 @@ function renderInfoTabs(element, state) {
const parityMatrix = state.linuxCncParityMatrix;
const commSummary = createGmoccapyCommunicationSummary();
const halSummary = createGmoccapyHalSummary();
+ const firstScreen = createAxisFirstScreenSummary(state);
element.innerHTML = `
+
+ ${firstScreen.items.map((item) => `
+
+ ${escapeHtml(item.label)}${escapeHtml(item.value)}
+
+ `).join("")}
+