diff --git a/package.json b/package.json
index 60ed0187..62d82e35 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,7 @@
"cli:dry-run": "node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp",
"cli:projects": "node tools/hwlab-cli/bin/hwlab-cli.mjs project list",
"l6:smoke": "node scripts/l6-cli-web-smoke.mjs",
- "web:check": "node web/hwlab-cloud-web/scripts/check.mjs && node --test web/hwlab-cloud-web/message-markdown.test.mjs web/hwlab-cloud-web/scripts/trace-scroll.test.mjs",
+ "web:check": "node web/hwlab-cloud-web/scripts/check.mjs && node --test web/hwlab-cloud-web/live-status.test.mjs web/hwlab-cloud-web/message-markdown.test.mjs web/hwlab-cloud-web/scripts/trace-scroll.test.mjs",
"web:build": "node web/hwlab-cloud-web/scripts/build.mjs",
"artifact-catalog:preview-blocked": "node scripts/refresh-artifact-catalog.mjs --target-ref HEAD --blocked --no-write",
"g14:gitops:render": "node scripts/g14-gitops-render.mjs",
diff --git a/scripts/artifact-publish.mjs b/scripts/artifact-publish.mjs
index 681fb08c..6833c593 100644
--- a/scripts/artifact-publish.mjs
+++ b/scripts/artifact-publish.mjs
@@ -905,6 +905,8 @@ function contentType(filePath) {
if (filePath.endsWith(".html")) return "text/html; charset=utf-8";
if (filePath.endsWith(".css")) return "text/css; charset=utf-8";
if (filePath.endsWith(".mjs") || filePath.endsWith(".js")) return "text/javascript; charset=utf-8";
+ if (filePath.endsWith(".svg")) return "image/svg+xml";
+ if (filePath.endsWith(".ico")) return "image/svg+xml";
return "application/octet-stream";
}
diff --git a/scripts/dev-cloud-workbench-smoke.test.mjs b/scripts/dev-cloud-workbench-smoke.test.mjs
index af6abe6c..11dc0526 100644
--- a/scripts/dev-cloud-workbench-smoke.test.mjs
+++ b/scripts/dev-cloud-workbench-smoke.test.mjs
@@ -732,7 +732,7 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou
test("repo-owned web checks expose source build and DEV live layout smoke gates", () => {
assert.match(rootPackage.scripts["web:check"], /^node web\/hwlab-cloud-web\/scripts\/check\.mjs/u);
- assert.match(rootPackage.scripts["web:check"], /node --test web\/hwlab-cloud-web\/message-markdown\.test\.mjs web\/hwlab-cloud-web\/scripts\/trace-scroll\.test\.mjs/u);
+ assert.match(rootPackage.scripts["web:check"], /node --test web\/hwlab-cloud-web\/live-status\.test\.mjs web\/hwlab-cloud-web\/message-markdown\.test\.mjs web\/hwlab-cloud-web\/scripts\/trace-scroll\.test\.mjs/u);
assert.equal(
rootPackage.scripts["web:layout"],
"node scripts/dev-cloud-workbench-layout-smoke.mjs --static --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout.json"
diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs
index f35c7847..dc6f1a9c 100644
--- a/web/hwlab-cloud-web/app.mjs
+++ b/web/hwlab-cloud-web/app.mjs
@@ -1804,8 +1804,7 @@ function shouldReconcileRestoredCodeAgentResult(message) {
if (state.resultReconciliationInFlight.has(message.traceId)) return false;
const status = String(message.status ?? "").toLowerCase();
if (["completed", "canceled"].includes(status)) return false;
- if (["failed", "timeout", "error"].includes(status)) return isRecoverableCodeAgentTerminalMessage(message) || traceHasTerminalResultEvent(message.runnerTrace);
- return true;
+ return traceHasTerminalResultEvent(message.runnerTrace);
}
function isRecoverableCodeAgentTerminalMessage(message) {
diff --git a/web/hwlab-cloud-web/favicon.ico b/web/hwlab-cloud-web/favicon.ico
new file mode 100644
index 00000000..3d1bbcf3
--- /dev/null
+++ b/web/hwlab-cloud-web/favicon.ico
@@ -0,0 +1,6 @@
+
diff --git a/web/hwlab-cloud-web/favicon.svg b/web/hwlab-cloud-web/favicon.svg
new file mode 100644
index 00000000..3d1bbcf3
--- /dev/null
+++ b/web/hwlab-cloud-web/favicon.svg
@@ -0,0 +1,6 @@
+
diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html
index 2829e5f7..8b646f7e 100644
--- a/web/hwlab-cloud-web/index.html
+++ b/web/hwlab-cloud-web/index.html
@@ -4,6 +4,7 @@
HWLAB 云工作台
+
diff --git a/web/hwlab-cloud-web/live-status.mjs b/web/hwlab-cloud-web/live-status.mjs
index 3b63abcb..9b07b857 100644
--- a/web/hwlab-cloud-web/live-status.mjs
+++ b/web/hwlab-cloud-web/live-status.mjs
@@ -222,6 +222,12 @@ function devicePodEvidenceSummary(data = {}) {
].filter(Boolean).join(" / ");
}
+function shortEvidenceToken(value) {
+ const text = String(value ?? "").trim();
+ if (!text) return "unknown";
+ return text.length > 16 ? text.slice(0, 16) : text;
+}
+
function classifyApiProbe(probe, context) {
if (!probe) {
return unverifiedProbe({
diff --git a/web/hwlab-cloud-web/live-status.test.mjs b/web/hwlab-cloud-web/live-status.test.mjs
new file mode 100644
index 00000000..d31d4e9b
--- /dev/null
+++ b/web/hwlab-cloud-web/live-status.test.mjs
@@ -0,0 +1,33 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { classifyWorkbenchLiveStatus } from "./live-status.mjs";
+
+test("Device Pod profile hash evidence is summarized without undefined helper crashes", () => {
+ const status = classifyWorkbenchLiveStatus({
+ healthLive: { ok: true, data: { serviceId: "hwlab-cloud-api", status: "ok" } },
+ restIndex: { ok: true, data: { serviceId: "hwlab-cloud-api", status: "ok" } },
+ health: { ok: true, data: { serviceId: "hwlab-cloud-api", status: "ok" } },
+ adapter: { ok: true, data: { serviceId: "hwlab-cloud-api", status: "ok" } },
+ devicePodStatus: {
+ ok: true,
+ data: {
+ status: "ready",
+ serviceId: "hwlab-device-pod",
+ summary: {
+ devicePodId: "device-pod-71-freq",
+ targetId: "71-FREQ",
+ profileHash: "abcdef0123456789fedcba9876543210",
+ latestEvent: { refs: { traceId: "trc_live_status", operationId: "op_live_status" } }
+ }
+ }
+ }
+ });
+
+ const devicePodProbe = status.probes.find((probe) => probe.serviceId === "hwlab-device-pod");
+ assert.ok(devicePodProbe, JSON.stringify(status.probes));
+ assert.equal(devicePodProbe.kind, "pass");
+ assert.equal(devicePodProbe.reasonCode, "device_pod_ready");
+ assert.match(devicePodProbe.evidenceSummary, /profile=abcdef0123456789/u);
+ assert.doesNotMatch(devicePodProbe.evidenceSummary, /fedcba9876543210/u);
+});
diff --git a/web/hwlab-cloud-web/package.json b/web/hwlab-cloud-web/package.json
index 48b2c26d..1b77f191 100644
--- a/web/hwlab-cloud-web/package.json
+++ b/web/hwlab-cloud-web/package.json
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
- "check": "node --test auth.test.mjs code-agent-facts.test.mjs code-agent-status.test.mjs scripts/sidebar-resize.test.mjs scripts/trace-scroll.test.mjs && node scripts/check.mjs",
+ "check": "node --test auth.test.mjs code-agent-facts.test.mjs code-agent-status.test.mjs live-status.test.mjs scripts/sidebar-resize.test.mjs scripts/trace-scroll.test.mjs && node scripts/check.mjs",
"build": "node scripts/build.mjs",
"layout": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --static --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout.json",
"layout:build": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --build --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout-build.json",
diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs
index 3c78e922..f4a22987 100644
--- a/web/hwlab-cloud-web/scripts/check.mjs
+++ b/web/hwlab-cloud-web/scripts/check.mjs
@@ -17,6 +17,8 @@ const requiredFiles = Object.freeze([
"message-markdown.mjs",
"live-status.mjs",
"runtime.mjs",
+ "favicon.svg",
+ "favicon.ico",
"help.md",
"third_party/marked/marked.esm.js",
"third_party/marked/LICENSE"
@@ -34,6 +36,8 @@ const styles = readWeb("styles.css");
const app = readWeb("app.mjs");
const liveStatus = readWeb("live-status.mjs");
const helpMarkdown = readWeb("help.md");
+const faviconSvg = readWeb("favicon.svg");
+const faviconIco = readWeb("favicon.ico");
const distContractScript = readWeb("scripts/dist-contract.mjs");
const cloudApiServer = readRepo("internal/cloud/server.mjs");
const devicePodData = readRepo("internal/device-pod/fake-data.mjs");
@@ -107,8 +111,15 @@ assert.match(app, /function\s+renderDrafts\s*\(/u, "restored Code Agent state mu
assert.doesNotMatch(functionBody(app, "loadLiveSurface"), /\/v1\/m3|audit\.event\.query|evidence\.record\.query/u, "live surface must use Device Pod routes, not legacy hardware/evidence panels");
assert.match(liveStatus, /function classifyDevicePodProbe/u, "live status must classify Device Pod probe");
+assert.match(liveStatus, /function shortEvidenceToken\s*\(/u, "live status must define profile hash evidence shortener locally");
+assert.doesNotMatch(functionBody(app, "shouldReconcileRestoredCodeAgentResult"), /return\s+true\s*;/u, "restored Code Agent state must not blindly fetch stale result traces");
+assert.doesNotMatch(functionBody(app, "shouldReconcileRestoredCodeAgentResult"), /isRecoverableCodeAgentTerminalMessage/u, "restored Code Agent state must not fetch stale result traces just because a timeout is recoverable");
assert.doesNotMatch(liveStatus, /classifyM3|serviceForM3Layer|\/v1\/m3/u, "live status must not keep legacy M3 probes");
+assert.match(html, /rel="icon"[^>]*href="\/favicon\.svg"/u, "Cloud Web must declare a repo-owned favicon");
+assert.match(faviconSvg, /