From 131e6ae190585cdb14c8631848bda388679c2cb5 Mon Sep 17 00:00:00 2001 From: HWLAB Code Queue Date: Fri, 22 May 2026 12:23:45 +0000 Subject: [PATCH] fix: add cloud web gate route contract --- docs/cloud-web-workbench.md | 7 +++++-- docs/reference/cloud-workbench.md | 10 ++++++++++ scripts/dev-artifact-publish.mjs | 3 ++- scripts/src/dev-cloud-workbench-smoke-lib.mjs | 18 ++++++++++++++++++ web/hwlab-cloud-web/app.mjs | 8 ++++++-- web/hwlab-cloud-web/index.html | 6 +++--- web/hwlab-cloud-web/scripts/build.mjs | 6 ++++++ web/hwlab-cloud-web/scripts/check.mjs | 12 +++++++++++- 8 files changed, 61 insertions(+), 9 deletions(-) diff --git a/docs/cloud-web-workbench.md b/docs/cloud-web-workbench.md index 00307010..b3fdc632 100644 --- a/docs/cloud-web-workbench.md +++ b/docs/cloud-web-workbench.md @@ -32,6 +32,7 @@ diagnostics under secondary routes: | --- | --- | --- | --- | | `/` | Cloud Workbench default. Shows agent conversation, trace, selected resource context, hardware/evidence side panel, and command input. | Yes | Same-origin `/v1`, read-only `/json-rpc`, source/gate report, blocked report. | | `/workbench` | Explicit alias for the default workbench. | Yes, alias | Same as `/`. | +| `/gate` | Internal static alias for the read-only gate diagnostics view. | No | Same as `/diagnostics/gate`; source/gate report and same-origin read-only diagnostics only. | | `/diagnostics` | Secondary diagnostics hub that links to status and gate views. | No | Same-origin `/v1` and report fixtures only. | | `/diagnostics/status` | Read-only DEV status: edge, router, API, web, DB readiness, agent service health, and artifact identity. | No | Same-origin `/v1/status` or equivalent read-only status endpoint. | | `/diagnostics/gate` | Read-only gate and M3 diagnostics inherited from `#59`: topology, route, health, blockers, and evidence summary. | No | Gate report and blocked report; read-only `/json-rpc` only when already defined. | @@ -42,6 +43,8 @@ diagnostics under secondary routes: The route relationship is: - `/` and `/workbench` are the primary product surface. +- `/gate` is a short internal alias for the same hidden gate view as + `/diagnostics/gate`; it must not become the default route. - `diagnostics/status` and `diagnostics/gate` are auxiliary views for troubleshooting and acceptance review. - Agent, hardware resources, and trusted records are first-class workbench @@ -151,9 +154,9 @@ Acceptance for the frontend design and later implementation is: | Area | Acceptance | | --- | --- | -| Default page | `http://74.48.78.17:16666/` opens the Cloud Workbench, not the Gate/M3 Diagnostics dashboard. | +| Default page | `http://74.48.78.17:16666/` opens the Cloud Workbench, not the Gate/M3 Diagnostics dashboard; its first screen does not show Gate, `BLOCKED`, `M0-M5`, or acceptance-review copy. | | API status | `http://74.48.78.17:16667` remains the DEV API/edge/status boundary; browser code uses same-origin `/v1` for workbench data and only read-only `/json-rpc` methods for diagnostics/hardware status. | -| Routes | `/`, `/workbench`, `/diagnostics`, `/diagnostics/status`, `/diagnostics/gate`, Agent, hardware resources, and trusted records are represented in navigation or route aliases. | +| Routes | `/`, `/workbench`, `/gate`, `/diagnostics`, `/diagnostics/status`, `/diagnostics/gate`, Agent, hardware resources, and trusted records are represented in navigation or route aliases. | | Layout | Left activity rail/resource tree, center agent conversation/trace, right hardware/evidence side panel, and bottom command/input area are present on the default workbench. | | Labels | `SOURCE`, `LOCAL`, `DRY-RUN`, `DEV-LIVE`, and `BLOCKED` labels are visible where evidence state is shown and follow the rules above. | | Read-only boundary | Hardware resources and diagnostics are read-only. Live controls are disabled with blocker reasons. | diff --git a/docs/reference/cloud-workbench.md b/docs/reference/cloud-workbench.md index 7f05d753..8a6b0a90 100644 --- a/docs/reference/cloud-workbench.md +++ b/docs/reference/cloud-workbench.md @@ -8,7 +8,12 @@ Cloud Workbench is the default user-facing frontend at ## Default Route - `/` must show the Cloud Workbench, not Gate, status, help, or diagnostics. +- The default first screen must not show Gate, `BLOCKED`, `M0-M5`, or + acceptance-review copy. Those terms may remain in hidden/internal views. - Gate and diagnostics are secondary routes or right-panel support views. +- `/gate` and `/diagnostics/gate` are internal diagnostic aliases served by the + same Cloud Web app router. They may carry Gate, blocker, and M0-M5 evidence + because they are not the default homepage. - `http://74.48.78.17:16667/` remains the API/edge boundary. - Browser data sources are same-origin `/v1`, read-only `/json-rpc`, source reports, gate reports, and blocked reports. @@ -63,6 +68,11 @@ node web/hwlab-cloud-web/scripts/build.mjs node scripts/dev-cloud-workbench-smoke.mjs --static ``` +The static smoke checks both sides of the route contract: `/` remains the +user-facing workbench, while `/gate` and `/diagnostics/gate` remain available +for internal diagnostics. This PR-class check is intentionally source-only and +does not deploy, restart services, or run browser e2e. + Live checks against `http://74.48.78.17:16666/` are read-only and must report blocked/skip instead of false green when the browser or network path is not available. diff --git a/scripts/dev-artifact-publish.mjs b/scripts/dev-artifact-publish.mjs index 7915d2c4..7320b77c 100644 --- a/scripts/dev-artifact-publish.mjs +++ b/scripts/dev-artifact-publish.mjs @@ -466,7 +466,8 @@ async function serveCloudWeb() { return; } - const relativePath = url.pathname === "/" ? "index.html" : url.pathname.slice(1); + const routePath = url.pathname.replace(/\/+$/, "") || "/"; + const relativePath = routePath === "/" || routePath === "/gate" || routePath === "/diagnostics/gate" ? "index.html" : url.pathname.slice(1); for (const root of roots) { const candidate = path.resolve(root, relativePath); if (!candidate.startsWith(root)) continue; diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index 5edb1d99..80900dfd 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -58,6 +58,8 @@ const diagnosticsRequiredTerms = Object.freeze([ "patch-panel DO1 -> DI1" ]); +const gateRouteAliases = Object.freeze(["/gate", "/diagnostics/gate"]); + const incompletePrimaryNavLabels = Object.freeze(["台", "证", "诊", "帮"]); const requiredWiringColumns = Object.freeze([ @@ -142,6 +144,8 @@ function runStaticSmoke() { const blockers = []; const files = readStaticFiles(); const source = Object.values(files).join("\n"); + const artifactPublisher = readText("scripts/dev-artifact-publish.mjs"); + const buildScript = readText("web/hwlab-cloud-web/scripts/build.mjs"); addCheck(checks, blockers, "static-web-assets", assetsExist(), "Cloud Web source assets are present.", { evidence: requiredWebAssets.map((file) => `web/hwlab-cloud-web/${file}`) @@ -177,6 +181,11 @@ function runStaticSmoke() { evidence: diagnosticsRequiredTerms }); + addCheck(checks, blockers, "internal-gate-route-aliases", gateRouteAliasesAreServed(files.app, artifactPublisher, buildScript), "/gate and /diagnostics/gate are direct internal diagnostic aliases, not default routes.", { + blocker: "runtime_blocker", + evidence: gateRouteAliases + }); + addCheck(checks, blockers, "feedback-120-wiring-table", wiringTableContract(files.html), "Patch-panel wiring panel is a table with source, target, status, evidence source, and trace/evidence columns.", { blocker: "contract_blocker", evidence: requiredWiringColumns @@ -398,6 +407,15 @@ function gateRetainsDiagnostics(html, app) { return diagnosticsRequiredTerms.every((term) => gateSource.includes(term)); } +function gateRouteAliasesAreServed(app, artifactPublisher, buildScript) { + return ( + /internalGatePathnames\(\)\.has/u.test(app) && + gateRouteAliases.every((route) => app.includes(`"${route}"`)) && + gateRouteAliases.every((route) => artifactPublisher.includes(`routePath === "${route}"`)) && + /\["gate", "diagnostics\/gate"\]/u.test(buildScript) + ); +} + function wiringTableContract(html) { const wiringPanel = html.match(/]*\bdata-side-panel=["']wiring["'][\s\S]*?<\/section>/u)?.[0] ?? ""; return requiredWiringColumns.every((column) => wiringPanel.includes(`${column}`)); diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index 9c4a1d7a..788a38cc 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -114,7 +114,7 @@ function initRoutes() { function routeFromLocation() { const hashRoute = window.location.hash.replace(/^#/, ""); if (viewIds.has(hashRoute)) return hashRoute; - if (window.location.pathname.replace(/\/+$/, "").endsWith("/gate")) return "gate"; + if (internalGatePathnames().has(window.location.pathname.replace(/\/+$/, "") || "/")) return "gate"; return "workspace"; } @@ -144,7 +144,7 @@ async function loadHelpSurface() { el.helpContent.dataset.helpState = "loading"; try { - const response = await fetch("./help.md", { cache: "no-store" }); + const response = await fetch("/help.md", { cache: "no-store" }); if (!response.ok) { throw new Error(`help.md HTTP ${response.status}`); } @@ -960,6 +960,10 @@ function blockerTitle(blocker) { return `阻塞:${scope}`; } +function internalGatePathnames() { + return new Set(["/gate", "/diagnostics/gate"]); +} + function userFacingSummary(text) { return String(text ?? "") .replaceAll("Public DEV route/frp", "公共 DEV 路由/frp") diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html index e75ef806..9e515ba2 100644 --- a/web/hwlab-cloud-web/index.html +++ b/web/hwlab-cloud-web/index.html @@ -4,11 +4,11 @@ HWLAB 云工作台 - - + + -
+