From fe2245bfaea0ef13e576dbcec44d0b13d1a5d8fe Mon Sep 17 00:00:00 2001 From: monkey1sai <26239865+monkey1sai@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:09:39 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(console):=20/ui=20=E6=94=B9=E6=8E=9B?= =?UTF-8?q?=20EdgeConsole=20=E5=AE=8C=E6=95=B4=E7=94=A2=E5=93=81=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E5=8F=B0=EF=BC=88B=20=E6=96=B9=E6=A1=88=E8=90=BD?= =?UTF-8?q?=E5=9C=B0=EF=BC=8CGPU=20=E7=82=BA=E6=A0=B8=E5=BF=83=E5=89=8D?= =?UTF-8?q?=E6=8F=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把 :8004/ui 從 OperatorConsole(6 頁)換成 EdgeConsole prototype 操作台:今天要做什麼首頁 + 五組導覽(工作台/核心治理/OMNIVERSE/落地端/SYSTEM)+ A1 五步引導 + 逐 A1–A10 的 3D Viewer 呈現頁(ViewerPresentationPage)+ 轉檔/Session/機隊/MinIO 治理頁。基底取用已驗證的 product-governance-console stash,疊上框架修正。 GPU 為核心前提(使用者指正):移除「零 GPU / 無 Omniverse 依賴 / GPU 未取得」等把產品定位成 no-GPU 賣點的字眼;保留 MockViewport「no-GPU=非 live 3D」與 evidence「CPU ~6s」等誠實免責不動。 邊界:MinIO/機隊/Session/轉檔頁全唯讀展示、走 coordinator proxy,無直連 MinIO、無 Kit 控制(restart 明標須送 audited intent 給 Kit Manager)。保留 #/kit、#/demo-control 原 operator-tool 路由(非 silently 砍)。未建一律標 p1/p4/demo。 驗證:vitest 140 pass;npm run build + build:ui green;Playwright 3 e2e pass(live :8004/ui,severeConsole 空);GitNexus impact LOW(isOperatorConsolePath/EdgeConsole)、detect_changes medium 無 HIGH/CRITICAL。證據 artifacts/e2e/pr3-edge-console-{home,a1,viewer}.png + product-governance-console-integration.png + unified-console-{routes,nav}.png。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-09-edge-console-product-shell.md | 75 ++++ .../e2e/product-console-integration.spec.ts | 51 +++ .../e2e/unified-console-routes.spec.ts | 52 +-- .../playwright.product-console.config.ts | 24 ++ web-viewer-sample/src/console/EdgeConsole.tsx | 112 ++++-- .../src/console/console.test.tsx | 67 +++- .../src/console/coordinatorClient.ts | 10 +- web-viewer-sample/src/console/data.ts | 52 ++- .../src/console/edge-console.css | 24 +- .../src/console/governanceClient.ts | 10 +- web-viewer-sample/src/console/pages.tsx | 331 +++++++++++++++++- web-viewer-sample/src/console/routing.ts | 14 +- web-viewer-sample/src/main.tsx | 4 +- 13 files changed, 751 insertions(+), 75 deletions(-) create mode 100644 docs/superpowers/plans/2026-06-09-edge-console-product-shell.md create mode 100644 web-viewer-sample/e2e/product-console-integration.spec.ts create mode 100644 web-viewer-sample/playwright.product-console.config.ts diff --git a/docs/superpowers/plans/2026-06-09-edge-console-product-shell.md b/docs/superpowers/plans/2026-06-09-edge-console-product-shell.md new file mode 100644 index 000000000..6706c2710 --- /dev/null +++ b/docs/superpowers/plans/2026-06-09-edge-console-product-shell.md @@ -0,0 +1,75 @@ +# EdgeConsole Product Shell 上線(B 方案 prototype 操作台落地)Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans,逐步打勾。 + +**Goal:** 把 `/ui` 的主控台從 `OperatorConsole`(Option A 六頁)換成 `EdgeConsole` 的 **完整 prototype 操作台**(B 方案):今天要做什麼首頁、A1 五步引導、逐 A1–A10 的 3D Viewer 呈現頁、轉檔/Session/機隊/MinIO 治理頁;同時修正「零 GPU / 無 Omniverse 依賴」框架字眼為 **GPU 為核心前提**,並對未建功能誠實標記。 + +**Architecture:** 純前端。基底直接取用已驗證(2026-06-08:44 tests + build:ui + Docker :8004/ui E2E pass)的 `stash@{0}`(product-governance-console WIP),用 `git stash apply` 套到新 branch;丟棄 stash 的 AGENTS.md/CLAUDE.md 舊改(main 已有四套工具契約),合併 console.test.tsx / pages.tsx(保 PR-1 LifecycleTab + PR-2 ProvLegend),疊上 GPU-premise 框架修正 + 保留 `#/kit`、`#/demo-control` 兩條 operator-tool 路由(非 silently 砍)。 + +**Tech Stack:** React 18 + TS + Vitest(renderToString)+ repo 內建 Playwright(本機無 bun)。 + +**硬約束(四套工具契約 / 邊界):** +1. 不在 main 開發 → branch `feat/edge-console-product-shell` → PR。 +2. 改 symbol 前 GitNexus impact 已跑:`isOperatorConsolePath`(LOW, caller=main.tsx)、`EdgeConsole`(LOW, 0 caller)。 +3. 邊界已審:MinioDataPage(靜態 tree,非 S3 browser)、KitGpuFleetPage(靜態機隊,明寫 restart 須送 audited intent 給 Kit Manager)、Session/Conversion(走 coordinator proxy、危險動作 disabled+p1)。全唯讀、無直連 MinIO、無 Kit 控制。 +4. 未建一律標 p1/p4/demo;不做假按鈕。 +5. 完成證據 = Playwright 截 `:8004/ui`(home + #/a1 + #/viewer)存 `artifacts/e2e/`。 +6. commit 前 GitNexus detect_changes 驗 scope。 + +--- + +### Task 1:建 branch + 套用 stash 基底 + +**Files:** 全 console 檔(apply)。 + +- [ ] **Step 1:** `git switch -c feat/edge-console-product-shell` +- [ ] **Step 2:** `git stash apply stash@{0}`(保留 stash 當備援,不 pop) +- [ ] **Step 3:** 解衝突 + - `git checkout --ours -- AGENTS.md CLAUDE.md`(保 main 的四套工具契約,丟 stash 舊 2 行) + - `console.test.tsx`:保 main 的 LifecycleTab/ProvLegend 測試 + 併入 stash 的 EdgeConsole/page 測試 + - `pages.tsx`:保 PR-2 的 ProvLegend import + CoordinatorPage `` + 併入 stash 的 +327 新頁 +- [ ] **Step 4:** 乾淨套用的檔(無 PR-1/PR-2 衝突):EdgeConsole.tsx / data.ts / routing.ts / main.tsx / coordinatorClient.ts / governanceClient.ts / edge-console.css 應自動 apply。 + +### Task 2:GPU-premise 框架修正(使用者明確指正) + +**Files:** `data.ts`, `EdgeConsole.tsx`, `pages.tsx` + +- [ ] **Step 1:** `data.ts` NAV_GROUPS:`core` group `sub` 從 `"CORE · 無 Omniverse 依賴"` → `"CORE · 語意 / 規則 / 問題"`。 +- [ ] **Step 2:** `EdgeConsole.tsx` header chip:`GPU 未取得` → `GPU · 依 session 派發`(保 ec-demo tone)。 +- [ ] **Step 3:** `pages.tsx` A1 lead:`3D 高亮是加值,A1 core 不依賴 GPU` → `3D 高亮需 GPU viewport(依 review session 派發);規則檢核在 governance-service 完成`。 +- [ ] **Step 4:** grep `零 GPU|無 GPU|無 Omniverse|no.?gpu` 全 console 掃,逐一修成 GPU-premise 語氣(不誤導為「不需 GPU」)。 + +### Task 3:保留 operator-tool 路由(非回歸) + +**Files:** `EdgeConsole.tsx` + +- [ ] **Step 1:** import `KitConsolePage`、`RealIfcConsolePage`。 +- [ ] **Step 2:** renderBody 加 `case "kit": return ;` 與 `case "demo-control": return ;`(route 已在 PRODUCT_CONSOLE_ROUTES;補 render,否則落到 HomePage)。 + +### Task 4:更新測試 / e2e + +**Files:** `console.test.tsx`, `routing.test.ts`(驗不需改), `e2e/unified-console-routes.spec.ts` + +- [ ] **Step 1:** `routing.test.ts`:所有斷言(#/kit、#/demo-control、#/nope→false)在新 PRODUCT_CONSOLE_ROUTES(超集)下仍成立 → 不需改,跑過確認。 +- [ ] **Step 2:** `unified-console-routes.spec.ts`:原斷言 OperatorConsole(op-page、op-nav-*、IntakeSelectPage「模型進件」)已不成立;改為驗 EdgeConsole 保留的 `#/kit`→`kit-proxy-panel`、`#/demo-control`→`real-ifc-demo-control`,移除 op-* 與 IntakeSelectPage 斷言。`product-console-integration.spec.ts`(已存在)為 B 操作台主驗收。 + +### Task 5:驗證(最小→完整) + +- [ ] **Step 1:** `cd web-viewer-sample; npx vitest run src/console`(全綠;含 EdgeConsole/page SSR + PR-1/PR-2)。 +- [ ] **Step 2:** `npm run build`(tsc + vite 綠)。 +- [ ] **Step 3:** `npm run build:ui`(dist-ui 重建——`:8004/ui` 入口;deploy.ps1 不會自動跑 build:ui)。 +- [ ] **Step 4:** 重啟 coordinator 容器讓 dist-ui 生效(bind-mount)。 +- [ ] **Step 5:** Playwright 截 `:8004/ui`(home「今天要做什麼」+ `#/a1` 五步 + `#/viewer` 3D Viewer)存 `artifacts/e2e/pr3-edge-console-*.png`。 +- [ ] **Step 6:** GitNexus `detect_changes(scope=all)`,記風險;HIGH/CRITICAL 先回報。 + +### Task 6:commit + PR + +- [ ] **Step 1:** `git diff --cached --check`(擋 trailing whitespace)。 +- [ ] **Step 2:** commit + push + `gh pr create`(繁中標題/描述 + Frontend Verification table + gstack 證據路徑)。 +- [ ] **Step 3:** merge(require-gstack-evidence 閘應放行——已有 e2e png)。 + +## Self-Review +- **Spec 覆蓋**:使用者 ① A1–A10 總覽=prototype 操作模型(HomePage + NAV_GROUPS + A1 五步)✓ ② 逐 A1–A10 的 3D Viewer 呈現(ViewerPresentationPage)✓ ③ GPU 為前提(Task 2 框架修正)✓ ④ 誠實 NOT BUILT(p1/p4/demo 標記沿用)✓。 +- **邊界**:無新增直連 MinIO / Kit 控制;敏感頁全唯讀展示已審 ✓。 +- **回歸**:保 `#/kit`、`#/demo-control`(Task 3);routing.test.ts 向後相容(Task 4-1)✓。 +- **Placeholder**:無;套用既有已驗證 code + 明確 edit 點。 diff --git a/web-viewer-sample/e2e/product-console-integration.spec.ts b/web-viewer-sample/e2e/product-console-integration.spec.ts new file mode 100644 index 000000000..d0db298a4 --- /dev/null +++ b/web-viewer-sample/e2e/product-console-integration.spec.ts @@ -0,0 +1,51 @@ +import { expect, test } from "@playwright/test"; + +const COORDINATOR = process.env.E2E_COORDINATOR_BASE_URL || "http://127.0.0.1:8004"; + +test.describe("Product AI-BIM Governance console integration", () => { + test("operator can navigate prototype-derived product console pages", async ({ page }) => { + const severeConsole: string[] = []; + page.on("console", (msg) => { + if (["error"].includes(msg.type())) severeConsole.push(msg.text()); + }); + page.on("pageerror", (err) => severeConsole.push(err.message)); + + await page.goto(`${COORDINATOR}/ui`); + await expect(page.getByRole("heading", { name: /今天要做什麼/ })).toBeVisible(); + await expect(page.getByText("核心治理")).toBeVisible(); + await expect(page.getByText("OMNIVERSE RUNTIME")).toBeVisible(); + await expect(page.getByText("落地端控制台")).toBeVisible(); + await expect(page.getByText("Chat USD Agent")).toBeVisible(); + + await page.goto(`${COORDINATOR}/ui#/a1`); + await expect(page.getByRole("heading", { name: /A1/ })).toBeVisible(); + await expect(page.getByText("A1 五步引導式流程")).toBeVisible(); + await expect(page.locator("main").getByText("governance-service :49102", { exact: true })).toBeVisible(); + + await page.goto(`${COORDINATOR}/ui#/viewer`); + await expect(page.getByRole("heading", { name: /3D Viewer/ })).toBeVisible(); + await expect(page.locator("main").getByText("DataChannel", { exact: true }).first()).toBeVisible(); + await expect(page.locator("main").getByText("highlightPrimsRequest", { exact: true }).first()).toBeVisible(); + + await page.goto(`${COORDINATOR}/ui#/conv`); + await expect(page.getByRole("heading", { name: /IFC→USD/ })).toBeVisible(); + await expect(page.locator("main").getByText("mapping coverage", { exact: true }).first()).toBeVisible(); + + await page.goto(`${COORDINATOR}/ui#/sessions`); + await expect(page.getByRole("heading", { name: /Session 管理/ })).toBeVisible(); + await expect(page.locator("main").getByText(/first frame/i).first()).toBeVisible(); + await expect(page.getByRole("button", { name: /Reclaim stale spectator/ })).toBeVisible(); + + await page.goto(`${COORDINATOR}/ui#/instances`); + await expect(page.getByRole("heading", { name: /Kit \/ GPU 機隊/ })).toBeVisible(); + await expect(page.locator("main").getByText("1 GPU = 1 Kit stream", { exact: true }).first()).toBeVisible(); + + await page.goto(`${COORDINATOR}/ui#/minio`); + await expect(page.getByRole("heading", { name: /MinIO 資料/ })).toBeVisible(); + await expect(page.locator("main .ec-tree").getByText("bim-control/", { exact: true })).toBeVisible(); + await expect(page.locator("main .ec-tree").getByText("model.usdc", { exact: true })).toBeVisible(); + + await page.screenshot({ path: "../artifacts/e2e/product-governance-console-integration.png", fullPage: true }); + expect(severeConsole).toEqual([]); + }); +}); diff --git a/web-viewer-sample/e2e/unified-console-routes.spec.ts b/web-viewer-sample/e2e/unified-console-routes.spec.ts index 4a5a4ac86..76fb15273 100644 --- a/web-viewer-sample/e2e/unified-console-routes.spec.ts +++ b/web-viewer-sample/e2e/unified-console-routes.spec.ts @@ -1,25 +1,22 @@ import { test, expect } from "@playwright/test"; -// CH-E:coordinator :8004/ui 服務 React UnifiedConsole(vite base=/ui/,gated on CONSOLE_DIST_DIR)。 -// 驗六個 hash 路由各自掛對應 operator 頁,且 nav 點擊可切換。/ui/console 301、/ui/open 302 凍結由 -// ui-open-regression.spec 另行覆蓋;本 spec 專注「六頁可達 + nav 切換」這條前端可操作鏈。 +// B 方案:coordinator :8004/ui 改掛 EdgeConsole 產品操作台(取代 OperatorConsole 六頁)。 +// 本 spec 專注「EdgeConsole 殼層可達 + 換台後仍保留的 operator-tool 路由(#/kit、#/demo-control)」。 +// 完整 prototype 頁(home /「今天要做什麼」、#/a1、#/viewer、#/conv、#/sessions、#/instances、#/minio) +// 由 product-console-integration.spec.ts 覆蓋。/ui/console 301、/ui/open 302 凍結由 ui-open-regression.spec 覆蓋。 const COORDINATOR = process.env.E2E_COORDINATOR_BASE_URL || "http://127.0.0.1:8004"; -test.describe("CH-E:統一治理控制台六路由(:8004/ui React console)", () => { - test("/ui 直接導航:六 hash 路由各渲染對應 operator 頁(含 #/kit、#/demo-control)", async ({ page }) => { - // /ui 無 hash → 預設 coordinator 頁;nav 六鍵皆在(前端真實可見入口)。 +test.describe("B 操作台::8004/ui EdgeConsole 路由 + 保留 operator-tool 路由", () => { + test("/ui 掛 EdgeConsole 殼層;保留 #/kit、#/demo-control、#/review、#/runtime 可達", async ({ page }) => { + // /ui 無 hash → EdgeConsole HomePage(今天要做什麼)。 await page.goto(`${COORDINATOR}/ui`); - await expect(page.getByTestId("op-page")).toBeVisible({ timeout: 20_000 }); - for (const k of ["coordinator", "intake", "demo-control", "review", "runtime", "kit"]) { - await expect(page.getByTestId(`op-nav-${k}`)).toBeVisible(); - } - await expect(page.getByRole("heading", { name: /Coordinator Console/ })).toBeVisible(); + await expect(page.getByRole("heading", { name: /今天要做什麼/ })).toBeVisible({ timeout: 20_000 }); - // #/kit → Kit 模型台(/api/kit proxy 面板)。 + // #/kit → Kit 模型台(/api/kit proxy 面板)——換 EdgeConsole 後仍保留(非 silently 砍)。 await page.goto(`${COORDINATOR}/ui#/kit`); await expect(page.getByTestId("kit-proxy-panel")).toBeVisible({ timeout: 15_000 }); - // #/demo-control → 真實 IFC 進件垂直切片。 + // #/demo-control → 真實 IFC 進件垂直切片——保留(真實 IFC E2E 入口)。 await page.goto(`${COORDINATOR}/ui#/demo-control`); await expect(page.getByTestId("real-ifc-demo-control")).toBeVisible({ timeout: 15_000 }); @@ -27,32 +24,21 @@ test.describe("CH-E:統一治理控制台六路由(:8004/ui React console) await page.goto(`${COORDINATOR}/ui#/review`); await expect(page.getByRole("heading", { name: /審查室/ })).toBeVisible({ timeout: 15_000 }); - // #/intake → 模型進件(選取現成模型)。 - await page.goto(`${COORDINATOR}/ui#/intake`); - await expect(page.getByRole("heading", { name: /模型進件/ })).toBeVisible({ timeout: 15_000 }); - - // #/runtime → Runtime Dashboard。 + // #/runtime → Runtime 監控。 await page.goto(`${COORDINATOR}/ui#/runtime`); - await expect(page.getByRole("heading", { name: /Runtime Dashboard/ })).toBeVisible({ timeout: 15_000 }); + await expect(page.getByRole("heading", { name: /Runtime/ })).toBeVisible({ timeout: 15_000 }); await page.screenshot({ path: "../artifacts/e2e/unified-console-routes.png", fullPage: true }); }); - test("nav 點擊切換頁面(hash 更新 + 內容切換,零依賴 hash 路由)", async ({ page }) => { - await page.goto(`${COORDINATOR}/ui#/coordinator`); - await expect(page.getByTestId("op-page")).toBeVisible({ timeout: 20_000 }); - - await page.getByTestId("op-nav-kit").click(); - await expect(page).toHaveURL(/#\/kit$/); - await expect(page.getByTestId("kit-proxy-panel")).toBeVisible(); - - await page.getByTestId("op-nav-demo-control").click(); - await expect(page).toHaveURL(/#\/demo-control$/); - await expect(page.getByTestId("real-ifc-demo-control")).toBeVisible(); + test("EdgeConsole nav 點擊切換頁面(hash 更新 + 內容切換,零依賴 hash 路由)", async ({ page }) => { + await page.goto(`${COORDINATOR}/ui#/a1`); + await expect(page.getByRole("heading", { name: /A1 · 治理與模型檢核/ })).toBeVisible({ timeout: 20_000 }); - await page.getByTestId("op-nav-review").click(); - await expect(page).toHaveURL(/#\/review$/); - await expect(page.getByRole("heading", { name: /審查室/ })).toBeVisible(); + // 點 nav 切到 A2 版本差異(core 治理 group 內的另一頁)。 + await page.getByRole("button", { name: /Version Diff/ }).first().click(); + await expect(page).toHaveURL(/#\/?a2$/); + await expect(page.getByRole("heading", { name: /版本差異/ })).toBeVisible({ timeout: 15_000 }); await page.screenshot({ path: "../artifacts/e2e/unified-console-nav.png", fullPage: true }); }); diff --git a/web-viewer-sample/playwright.product-console.config.ts b/web-viewer-sample/playwright.product-console.config.ts new file mode 100644 index 000000000..54b09dcac --- /dev/null +++ b/web-viewer-sample/playwright.product-console.config.ts @@ -0,0 +1,24 @@ +import { defineConfig, devices } from "@playwright/test"; + +// Product console E2E targets an already-served coordinator /ui bundle. +// It intentionally avoids the viewer webServer from playwright.config.ts. +export default defineConfig({ + testDir: "./e2e", + outputDir: "../artifacts/e2e/_product_console_output", + timeout: 60_000, + expect: { timeout: 15_000 }, + fullyParallel: false, + workers: 1, + retries: 0, + reporter: [ + ["list"], + ["html", { outputFolder: "../artifacts/e2e/product-console-report", open: "never" }], + ], + use: { + trace: "on", + screenshot: "on", + video: "retain-on-failure", + viewport: { width: 1440, height: 900 }, + }, + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], +}); diff --git a/web-viewer-sample/src/console/EdgeConsole.tsx b/web-viewer-sample/src/console/EdgeConsole.tsx index 9cadefe86..daae063b7 100644 --- a/web-viewer-sample/src/console/EdgeConsole.tsx +++ b/web-viewer-sample/src/console/EdgeConsole.tsx @@ -3,23 +3,37 @@ // 零依賴 hash 路由(不引入 react-router、不擾動既有 App ?session bootstrap)。 import { useEffect, useState } from "react"; import "./edge-console.css"; -import { PAGES, Prov } from "./data"; +import { NAV_GROUPS, PAGES, Prov } from "./data"; import { + A1GovernanceWorkbenchPage, + AdminPage, AppsPage, AppVisionPage, + ConversionSchedulingPage, CoordinatorPage, FederationPage, + GpuReviewRoomPage, + HomePage, IntakePage, IssuesRuleCenterPage, + KitGpuFleetPage, + MinioDataPage, OverviewPage, + ReportsPage, ReviewRoomPage, RuntimePage, SemanticViewerPage, + SessionManagementPage, + SpecPage, + ViewerPresentationPage, VersionDiffPage, } from "./pages"; +// operator-tool 路由保留:#/kit、#/demo-control 原由 OperatorConsole 服務;換 EdgeConsole 後仍可達(非 silently 砍)。 +import { KitConsolePage } from "./KitConsolePage"; +import { RealIfcConsolePage } from "./RealIfcConsolePage"; function usePageHash(): [string, (k: string) => void] { - const read = () => window.location.hash.replace(/^#/, "") || "overview"; + const read = () => window.location.hash.replace(/^#\/?console\/?/, "").replace(/^#\/?/, "") || "home"; const [page, setPage] = useState(read); useEffect(() => { const on = () => setPage(read()); @@ -37,6 +51,26 @@ function renderBody(page: string, go: (k: string) => void) { // app/ → A4–A10 vision 詳頁(P3-1)。 if (page.startsWith("app/")) return ; switch (page) { + case "home": return ; + case "a1": return ; + case "a2": return ; + case "a3": return ; + case "a4": return ; + case "a5": return ; + case "a6": return ; + case "a7": return ; + case "a8": return ; + case "a9": return ; + case "a10": return ; + case "viewer": return ; + case "gpu": return ; + case "conv": return ; + case "sessions": return ; + case "instances": return ; + case "minio": return ; + case "reports": return ; + case "admin": return ; + case "spec": return ; case "overview": return ; case "issues": return ; case "apps": return ; @@ -47,12 +81,34 @@ function renderBody(page: string, go: (k: string) => void) { case "runtime": return ; case "review": return ; case "semantic": return ; - default: return ; + case "kit": return ; + case "demo-control": return ; + default: return ; } } // 用語對照(操作員 biz ↔ 技術 tech)。register=biz 顯示業務語、tech 顯示技術語。 const NAV_LABEL: Record = { + home: { tech: "Today", biz: "今天要做什麼" }, + a1: { tech: "A1 Governance", biz: "治理與模型檢核" }, + a2: { tech: "A2 Version Diff", biz: "版本差異與責任" }, + a3: { tech: "A3 Federation", biz: "跨專業疊合" }, + a4: { tech: "A4 Semantic Search", biz: "語意搜尋問答" }, + a5: { tech: "A5 IoT/FM", biz: "IoT / FM 數位分身" }, + a6: { tech: "A6 4D/5D", biz: "4D / 5D 施工模擬" }, + a7: { tech: "A7 Reality Capture", biz: "Reality Capture 比對" }, + a8: { tech: "A8 Synthetic Data", biz: "Synthetic Data" }, + a9: { tech: "A9 ChatUSD", biz: "設計 / 審查 Copilot" }, + a10: { tech: "A10 Robot Sim", biz: "機器人 / 巡檢模擬" }, + viewer: { tech: "3D Viewer", biz: "3D Viewer 呈現" }, + gpu: { tech: "GPU Review Room", biz: "GPU 審查室" }, + conv: { tech: "Conversion Queue", biz: "IFC→USD 轉檔排程" }, + sessions: { tech: "Session ATC", biz: "Session 管理" }, + instances: { tech: "Kit/GPU Fleet", biz: "Kit / GPU 機隊" }, + minio: { tech: "MinIO Data", biz: "MinIO 資料" }, + reports: { tech: "Reports", biz: "報表中心" }, + admin: { tech: "Admin", biz: "系統管理" }, + spec: { tech: "Design Spec", biz: "設計規格說明" }, overview: { tech: "Overview", biz: "總覽" }, coordinator: { tech: "Coordinator Console", biz: "審查控制台" }, intake: { tech: "Model Intake", biz: "建模接收與轉換" }, @@ -63,6 +119,18 @@ const NAV_LABEL: Record = { semantic: { tech: "Semantic Viewer", biz: "語意檢核" }, }; +const COPILOT_PROMPTS: Record = { + home: ["這個專案現在卡在哪?", "幫我列今天最該處理的 3 件事", "v07 送審前還缺什麼?"], + a1: ["找出所有 FireDoor 但 FireRating 為空的構件", "把 Critical issue 的構件高亮為紅色", "列出 Mapping coverage < 95% 的子系統"], + a2: ["v07 比 v06 改了什麼?", "哪些變更會影響成本?", "上一版的 issue 修掉了嗎?"], + a4: ["三樓所有沒填防火時效的防火門", "體積最大的 10 個房間", "屬於 L2 但分類碼空白的構件"], + a5: ["現在哪個區域溫度異常?", "列出逾期未處理的維保工單", "B1 機房本月用電趨勢"], + conv: ["哪些轉檔任務卡住或失敗了?", "把 988 的模型插隊優先轉", "列出 coverage < 95% 的任務"], + sessions: ["哪個 session 有 viewer 收不到 frame?", "把閒置超過 15 分鐘的 session 回收", "S-270 現在幾個人在看?"], + instances: ["哪台 GPU 還能接新 session?", "把新審查排到最閒的節點", "edge-gpu-02 的 VRAM 還夠嗎?"], + minio: ["270 專案有幾個模型?", "哪些模型還沒轉成 USD?", "model.ifc 最大的是哪一個?"], +}; + // FlowBar(P3-3):5 步操作員心智模型 Intake→Convert→Meeting→Mark→Record。 // state 為各步真實落地狀態(asbuilt / p15);非資料宣稱,純流程示意。 const FLOW: { n: string; tech: string; biz: string; state: Prov; page: string }[] = [ @@ -98,10 +166,9 @@ export default function EdgeConsole() { // Tweaks(P3-3):register=操作員/技術用語;scenario=clean/warn(UI 偏好;真實頁一律用 live API)。 const [register, setRegister] = useState<"tech" | "biz">("tech"); const [scenario, setScenario] = useState<"clean" | "warn">("clean"); - const gov = PAGES.filter((p) => p.plane === "governance"); - const omni = PAGES.filter((p) => p.plane === "omniverse"); const navText = (key: string, fallback: string) => (NAV_LABEL[key] ? NAV_LABEL[key][register] : fallback); const flowActive = page.startsWith("app/") ? "apps" : page; + const prompts = COPILOT_PROMPTS[flowActive] ?? COPILOT_PROMPTS.home; return (
@@ -112,23 +179,23 @@ export default function EdgeConsole() {
COORD :8004 GOV :49102 - GPU 未取得 + GPU · 依 session 派發
@@ -158,13 +225,14 @@ export default function EdgeConsole() { Chat USD Agent ROADMAP · A9
-

A9 USD Code / ChatUSD Copilot 為 Phase 4 願景;後端(usd-code microservice)未建置,互動僅示意。

- {/* P3-2:suggested prompts(disabled,僅示意)+ 寫入限制聲明 + disabled 輸入框。 */} +

A9 USD Code / ChatUSD Copilot 為 Phase 4 願景;本欄先顯示 page-aware prompts 與 tool trace 版型,狀態改動需人工確認與 audit。

SUGGESTED · USD-AWARE(PREVIEW · 後端未建) -
找出所有沒有 FireRating 的防火門
-
把語意未對映的構件標出來
-
列出 coverage < 95% 的子系統
+ {prompts.map((prompt) =>
{prompt}
)} +
+
+
kit_mcp.search_prims preview
+

tool trace 僅作透明化版型;真正 MCP/NeMo 執行尚未接入。

寫入限制(規格):AI 僅能改 review / session layer,不寫回 source model。

diff --git a/web-viewer-sample/src/console/console.test.tsx b/web-viewer-sample/src/console/console.test.tsx index f42687305..4e0cdf22c 100644 --- a/web-viewer-sample/src/console/console.test.tsx +++ b/web-viewer-sample/src/console/console.test.tsx @@ -2,7 +2,25 @@ // A2/A3 帶 provenance 與真實邊界、無願景假數字。用 renderToString(不需 @testing-library / 網路)。 import { renderToString } from "react-dom/server"; import { describe, expect, it } from "vitest"; -import { AppsPage, AppVisionPage, CoordinatorPage, FederationPage, IntakePage, IssuesRuleCenterPage, OverviewPage, ReviewRoomPage, RuntimePage, SemanticViewerPage, VersionDiffPage } from "./pages"; +import { + A1GovernanceWorkbenchPage, + AppsPage, + AppVisionPage, + ConversionSchedulingPage, + CoordinatorPage, + FederationPage, + IntakePage, + IssuesRuleCenterPage, + KitGpuFleetPage, + MinioDataPage, + OverviewPage, + ReviewRoomPage, + RuntimePage, + SemanticViewerPage, + SessionManagementPage, + ViewerPresentationPage, + VersionDiffPage, +} from "./pages"; import EdgeConsole from "./EdgeConsole"; import { ProvLegend } from "./components"; import { coordinatorClient, type RuntimeStatus } from "./coordinatorClient"; @@ -312,6 +330,53 @@ describe("edge console honesty smoke", () => { expect(html).toContain("warn"); }); + it("完整產品操作台 shell 顯示 prototype 的四組資訊架構", () => { + const html = renderToString(); + expect(html).toContain("工作台"); + expect(html).toContain("核心治理"); + expect(html).toContain("OMNIVERSE RUNTIME"); + expect(html).toContain("落地端控制台"); + expect(html).toContain("IFC→USD 轉檔排程"); + expect(html).toContain("Kit / GPU 機隊"); + expect(html).toContain("MinIO 資料"); + expect(html).toContain("Chat USD Agent"); + }); + + it("prototype 核心頁面可 render:A1 stepper、3D viewer、conversion、session、Kit/GPU、MinIO", () => { + const a1 = renderToString(); + expect(a1).toContain("上傳模型"); + expect(a1).toContain("自動檢核"); + expect(a1).toContain("開 Issue"); + expect(a1).toContain("匯出 BCF"); + expect(a1).toContain("governance-service :49102"); + + const viewer = renderToString(); + expect(viewer).toContain("3D Viewer 呈現"); + expect(viewer).toContain("openStage"); + expect(viewer).toContain("highlightPrimsRequest"); + expect(viewer).toContain("DataChannel"); + + const conv = renderToString(); + expect(conv).toContain("IFC→USD 轉檔排程"); + expect(conv).toContain("mapping coverage"); + expect(conv).toContain("/api/external/ifc-ready"); + + const sessions = renderToString(); + expect(sessions).toContain("Session 管理"); + expect(sessions).toContain("first frame"); + expect(sessions).toContain("Reclaim stale spectator"); + + const fleet = renderToString(); + expect(fleet).toContain("Kit / GPU 機隊"); + expect(fleet).toContain("1 GPU = 1 Kit stream"); + expect(fleet).toContain("drain"); + + const minio = renderToString(); + expect(minio).toContain("MinIO 資料"); + expect(minio).toContain("bim-control"); + expect(minio).toContain("model.usdc"); + }); + // ── P4 Review Room(G)v1:連到既有 viewer,不在 console 內嵌 3D,不動 App/Window ── it("P4 Review Room 提供「在既有 viewer 開啟」連結且誠實標 3D 在既有 viewer", () => { const html = renderToString(); diff --git a/web-viewer-sample/src/console/coordinatorClient.ts b/web-viewer-sample/src/console/coordinatorClient.ts index fab44fa7f..8047da94b 100644 --- a/web-viewer-sample/src/console/coordinatorClient.ts +++ b/web-viewer-sample/src/console/coordinatorClient.ts @@ -17,8 +17,16 @@ // 不在此 client 提供;outbox 摘要改由 ifc_ready job 的 callback_outbox_id 觀察(誠實標 demo/未取得)。 const env = (import.meta as { env?: Record }).env; +function defaultCoordinatorBase(): string { + if (typeof window === "undefined") return "http://127.0.0.1:8004"; + const { origin, pathname, port } = window.location; + const devPorts = new Set(["5173", "5174", "5180"]); + if (pathname.startsWith("/ui") && !devPorts.has(port)) return origin; + return "http://127.0.0.1:8004"; +} + const COORD_BASE: string = - env?.VITE_COORDINATOR_API_BASE ?? env?.VITE_COORDINATOR_BASE ?? "http://127.0.0.1:8004"; + env?.VITE_COORDINATOR_API_BASE ?? env?.VITE_COORDINATOR_BASE ?? defaultCoordinatorBase(); async function jsonGet(path: string): Promise { const res = await fetch(`${COORD_BASE}${path}`, { headers: { Accept: "application/json" } }); diff --git a/web-viewer-sample/src/console/data.ts b/web-viewer-sample/src/console/data.ts index 5bdcce6bd..aea85584d 100644 --- a/web-viewer-sample/src/console/data.ts +++ b/web-viewer-sample/src/console/data.ts @@ -30,18 +30,50 @@ export interface PageDef { no: string; label: string; plane: "governance" | "omniverse"; + group: "workspace" | "core" | "omniverse" | "coordinator" | "system"; + sub?: string; + badge?: string; } -// 兩段式導覽(雲地邊界視覺化):Governance Platform(零 GPU)/ Omniverse Runtime(綁 GPU)。 +export const NAV_GROUPS: { key: PageDef["group"]; title: string; sub: string }[] = [ + { key: "workspace", title: "工作台", sub: "WORKSPACE" }, + { key: "core", title: "核心治理", sub: "CORE · 語意 / 規則 / 問題" }, + { key: "omniverse", title: "OMNIVERSE RUNTIME", sub: "KIT · USD · GPU" }, + { key: "coordinator", title: "落地端控制台", sub: "COORDINATOR · 1..N GPU" }, + { key: "system", title: "SYSTEM", sub: "Runtime · Admin · Spec" }, +]; + +// prototype product console 導覽。保留既有 route aliases(overview/coordinator/intake/review/semantic) +// 以免舊測試與 deep links 斷掉。 export const PAGES: PageDef[] = [ - { key: "overview", no: "A", label: "Overview", plane: "governance" }, - { key: "coordinator", no: "B", label: "Coordinator Console", plane: "governance" }, - { key: "intake", no: "C", label: "Model Intake", plane: "governance" }, - { key: "issues", no: "D", label: "Issues · 語意驗收 / Rule Center", plane: "governance" }, - { key: "apps", no: "E", label: "Applications · A1–A10", plane: "governance" }, - { key: "runtime", no: "F", label: "Runtime Dashboard", plane: "omniverse" }, - { key: "review", no: "G", label: "Review Room", plane: "omniverse" }, - { key: "semantic", no: "H", label: "Semantic Viewer", plane: "omniverse" }, + { key: "home", no: "⌂", label: "今天要做什麼", plane: "governance", group: "workspace" }, + { key: "a1", no: "A1", label: "治理與模型檢核", plane: "governance", group: "core", badge: "P0" }, + { key: "a2", no: "A2", label: "版本差異與責任", plane: "governance", group: "core" }, + { key: "a3", no: "A3", label: "跨專業疊合", plane: "governance", group: "core" }, + { key: "a4", no: "A4", label: "語意搜尋問答", plane: "governance", group: "core" }, + { key: "a5", no: "A5", label: "IoT / FM 數位分身", plane: "governance", group: "core" }, + { key: "issues", no: "BC", label: "Issue / BCF", plane: "governance", group: "core", badge: "A1" }, + { key: "reports", no: "RP", label: "報表中心", plane: "governance", group: "core" }, + { key: "viewer", no: "3D", label: "3D Viewer 呈現", plane: "omniverse", group: "omniverse" }, + { key: "gpu", no: "01", label: "GPU 審查室", plane: "omniverse", group: "omniverse", badge: "MVP" }, + { key: "a6", no: "A6", label: "4D / 5D 施工模擬", plane: "omniverse", group: "omniverse" }, + { key: "a7", no: "A7", label: "Reality Capture 比對", plane: "omniverse", group: "omniverse" }, + { key: "a8", no: "A8", label: "Synthetic Data", plane: "omniverse", group: "omniverse" }, + { key: "a9", no: "A9", label: "設計 / 審查 Copilot", plane: "omniverse", group: "omniverse" }, + { key: "a10", no: "A10", label: "機器人 / 巡檢模擬", plane: "omniverse", group: "omniverse" }, + { key: "conv", no: "CV", label: "IFC→USD 轉檔排程", plane: "governance", group: "coordinator", badge: "P1" }, + { key: "sessions", no: "SS", label: "Session 管理", plane: "governance", group: "coordinator" }, + { key: "instances", no: "KG", label: "Kit / GPU 機隊", plane: "omniverse", group: "coordinator" }, + { key: "minio", no: "M", label: "MinIO 資料", plane: "governance", group: "coordinator" }, + { key: "runtime", no: "RT", label: "Runtime 監控", plane: "omniverse", group: "system" }, + { key: "admin", no: "SY", label: "系統管理", plane: "governance", group: "system" }, + { key: "spec", no: "▦", label: "設計規格說明", plane: "governance", group: "system" }, + { key: "overview", no: "OV", label: "Overview", plane: "governance", group: "system" }, + { key: "coordinator", no: "CO", label: "Coordinator Console", plane: "governance", group: "coordinator" }, + { key: "intake", no: "IN", label: "Model Intake", plane: "governance", group: "coordinator" }, + { key: "review", no: "G", label: "Review Room", plane: "omniverse", group: "omniverse" }, + { key: "semantic", no: "SE", label: "Semantic Viewer", plane: "omniverse", group: "omniverse" }, + { key: "apps", no: "AP", label: "Applications · A1–A10", plane: "governance", group: "system" }, ]; export interface AppCardDef { @@ -127,7 +159,7 @@ export const ENDPOINTS: EndpointDef[] = [ ]; // ── 相依與授權風險(DEPENDENCIES panel 用,移植自設計原型 data.jsx)─────────── -// 誠實鐵律:A1 core 是零 GPU / 零 NVIDIA runtime,但**不可**寫成「零相依 / 零授權風險」。 +// 誠實鐵律:A1 core 的規則檢核在 governance-service(CPU)完成,但**不可**寫成「零相依 / 零授權風險」。 // 下列 LGPL / copyleft 元件商用前須法務確認。BCF 匯出本 repo 已改純 stdlib(不再依賴 GPLv3 // bcf-client),故 BCF 列標 permissive 並註明;其餘 copyleft 元件照實標。 export interface DependencyDef { diff --git a/web-viewer-sample/src/console/edge-console.css b/web-viewer-sample/src/console/edge-console.css index fd50d5eb6..85a7798df 100644 --- a/web-viewer-sample/src/console/edge-console.css +++ b/web-viewer-sample/src/console/edge-console.css @@ -28,13 +28,15 @@ .ec-nav { grid-area:nav; background:var(--ec-bg-2); border-right:1px solid var(--ec-line); overflow-y:auto; padding:10px 0; } -.ec-nav .ec-group { color:var(--ec-fg-4); font-size:10px; letter-spacing:.12em; padding:12px 14px 4px; } +.ec-nav .ec-group { color:var(--ec-fg-4); font-size:10px; letter-spacing:.12em; padding:12px 14px 4px; display:flex; flex-direction:column; gap:1px; } +.ec-nav .ec-group span { color:var(--ec-fg-4); letter-spacing:0; font-size:9px; opacity:.78; } .ec-nav button { display:flex; gap:9px; align-items:center; width:100%; text-align:left; background:none; border:0; color:var(--ec-fg-2); padding:7px 14px; cursor:pointer; font:inherit; } .ec-nav button:hover { background:var(--ec-bg-3); color:var(--ec-fg); } .ec-nav button.active { background:var(--ec-grn-2); color:var(--ec-fg); box-shadow:inset 3px 0 0 var(--ec-grn); } .ec-nav .ec-key { color:var(--ec-fg-4); width:14px; } +.ec-nav-badge { margin-left:auto; color:var(--ec-grn); border:1px solid var(--ec-grn-3); background:var(--ec-grn-2); border-radius:3px; padding:0 5px; font-size:9px; } .ec-main { grid-area:main; overflow-y:auto; padding:18px 22px; } .ec-main h1 { font-size:19px; margin:0 0 4px; color:var(--ec-fg); } @@ -106,6 +108,21 @@ table.ec-table tr:hover td { background:var(--ec-bg-3); } .ec-appcard.clickable:hover { border-color:var(--ec-vio); } .ec-note { color:var(--ec-fg-3); font-size:11px; margin:8px 0; } .ec-warn-note { color:var(--ec-amb); font-size:11px; } +.ec-mini-card { background:var(--ec-bg-3); border:1px solid var(--ec-line); border-radius:6px; padding:10px 12px; min-width:0; } +.ec-mini-head { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:6px; } +.ec-mini-title { color:var(--ec-fg); font-weight:600; } +.ec-action-card { background:var(--ec-bg-3); border:1px solid var(--ec-line); border-radius:6px; color:var(--ec-fg-2); + padding:12px; display:flex; flex-direction:column; gap:5px; text-align:left; cursor:pointer; font:inherit; min-height:112px; } +.ec-action-card:hover { border-color:var(--ec-grn); color:var(--ec-fg); } +.ec-action-card span:last-child { color:var(--ec-fg-3); font-size:11px; } +.ec-tree { font-family:var(--ec-mono); color:var(--ec-fg-2); font-size:12px; line-height:1.8; overflow:auto; } +.ec-tree .indent { padding-left:18px; } +.ec-tree .two { padding-left:36px; } +.ec-tree .three { padding-left:54px; } +.ec-tree-file { color:var(--ec-cyan); } +.ec-toolcall { border:1px solid var(--ec-line); border-radius:6px; padding:8px 10px; background:var(--ec-bg-3); margin-top:12px; } +.ec-dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; background:var(--ec-fg-4); } +.ec-dot.g { background:var(--ec-grn); box-shadow:0 0 8px var(--ec-grn); } /* ── BoundaryDiagram(P2-1 Overview):web-plane → boundary → internal 三欄 ── */ .ec-boundary { display:flex; align-items:stretch; gap:8px; flex-wrap:wrap; } @@ -335,3 +352,8 @@ table.ec-table tr:hover td { background:var(--ec-bg-3); } border-color:var(--ec-red); color:var(--ec-red); } + +@media (max-width: 980px) { + .ec-root { grid-template-columns:210px 1fr; grid-template-areas:"top top" "nav main" "foot foot"; } + .ec-agent { display:none; } +} diff --git a/web-viewer-sample/src/console/governanceClient.ts b/web-viewer-sample/src/console/governanceClient.ts index 6ac689dbd..beb15bb44 100644 --- a/web-viewer-sample/src/console/governanceClient.ts +++ b/web-viewer-sample/src/console/governanceClient.ts @@ -4,8 +4,16 @@ // 經 WEB_VIEWER_COORDINATOR_API_BASE 設定、config/env.ts 亦讀此名)。保留舊名 VITE_COORDINATOR_BASE // 為相容 fallback(正規名優先),預設與 config/env.ts 一致為 http://127.0.0.1:8004。 const env = (import.meta as { env?: Record }).env; +function defaultCoordinatorBase(): string { + if (typeof window === "undefined") return "http://127.0.0.1:8004"; + const { origin, pathname, port } = window.location; + const devPorts = new Set(["5173", "5174", "5180"]); + if (pathname.startsWith("/ui") && !devPorts.has(port)) return origin; + return "http://127.0.0.1:8004"; +} + const COORD_BASE: string = - env?.VITE_COORDINATOR_API_BASE ?? env?.VITE_COORDINATOR_BASE ?? "http://127.0.0.1:8004"; + env?.VITE_COORDINATOR_API_BASE ?? env?.VITE_COORDINATOR_BASE ?? defaultCoordinatorBase(); export interface RuleRunRequest { ifc_source_path: string; diff --git a/web-viewer-sample/src/console/pages.tsx b/web-viewer-sample/src/console/pages.tsx index 36a371ed6..7861c8e20 100644 --- a/web-viewer-sample/src/console/pages.tsx +++ b/web-viewer-sample/src/console/pages.tsx @@ -48,6 +48,62 @@ function BoundaryDiagram() { ); } +function MiniCard({ code, title, desc, prov = "asbuilt" }: { code: string; title: string; desc: string; prov?: Prov }) { + return ( +
+
{code}
+
{title}
+

{desc}

+
+ ); +} + +function LifecycleStrip({ steps }: { steps: string[] }) { + return ( +
+ {steps.map((s, i) => ( + + {i + 1}{s} + {i < steps.length - 1 && →} + + ))} +
+ ); +} + +export function HomePage({ onOpen }: { onOpen: (route: string) => void }) { + const actions = [ + ["A1", "跑一次治理檢核", "上傳或選取模型,自動檢查命名、分類、防火、LOD 是否合規。", "a1"], + ["A2", "比較兩個版本", "看 v06 / v07 新增、修改、刪除與 issue 影響。", "a2"], + ["A3", "打開跨專業疊合", "把建築、結構、機電模型組成 federation review room。", "a3"], + ["BC", "整理 Issue / BCF", "把 A1/A2/A3/A5 的問題輸出成 BCF / Excel / 報表。", "issues"], + ["CV", "查看轉檔排程", "確認 IFC-ready、conversion job、mapping coverage、stage writeback。", "conv"], + ["SS", "檢查 Session / Viewer", "看 primary/spectator 是否真的收到 first frame。", "sessions"], + ] as const; + return ( + <> +

今天要做什麼 · AI-BIM Governance 工作台

+

這是 operator 的第一屏:先看哪件事能交付、哪件事卡住、哪個 runtime 只是宣稱 ready。所有能力都保留 provenance,不把 roadmap 說成已完成。

+ +
+ {actions.map(([code, title, desc, route]) => ( + + ))} +
+
+ + + + + + + ); +} + export function OverviewPage() { // 可選接 coordinator /health 探活(真實端點)。未連線時誠實顯示「未連線」,不假裝 healthy。 const [health, setHealth] = useState<"unknown" | "ok" | "down">("unknown"); @@ -102,7 +158,7 @@ export function OverviewPage() { @@ -130,6 +186,277 @@ export function OverviewPage() { ); } +export function A1GovernanceWorkbenchPage() { + const rules = [ + ["NAMING-STD-01", "命名規則", "passed"], + ["SPACE-CODE-02", "樓層 / 空間", "passed"], + ["CLASS-UNI-03", "分類碼缺漏", "failed"], + ["ARC-DOOR-REQ-001", "防火門 FireRating", "failed"], + ["LOD-LOI-REQ", "LOD / LOI 不足", "failed"], + ]; + return ( + <> +

A1 · 治理與模型檢核

+

上傳模型或選取已轉檔成功的 IFC/USDC,跑自動規則檢核,直接產生 Issue 與 BCF/Excel。規則檢核在 governance-service(CPU)完成;3D 高亮需 GPU viewport(依 review session 派發)。

+ + +
+ + + +
+
+ +
+ + + + +
+
+ + {rules.map(([rule, use, state]) => ( + + ))} +
rule用途state
{rule}{use}{state === "passed" ? : }
+
+ + 執行規則檢核{" "} + 失敗構件建 issue{" "} + 匯出 BCF / Excel{" "} + 在 3D 高亮 + + + ); +} + +export function ViewerPresentationPage() { + const capabilities: [string, string, Prov][] = [ + ["openStage", "載入 selected USD / USDC stage;success 還需要 loaded stage URL 證據", "asbuilt"], + ["focusPrim / selectPrims", "點清單或 mapping table 可聚焦 / 選取 USD prim", "asbuilt"], + ["clearHighlight", "清除 viewer overlay / selection", "asbuilt"], + ["highlightPrimsRequest", "A1/A2/A4 結果轉 3D highlight;需 browser DataChannel", "p15"], + ["first_frame_at", "viewer 是否真的看到畫面,不等於 port open", "p1"], + ["stage matched", "expected_stage_url == loaded stage URL 才算 stage truth", "p1"], + ]; + return ( + <> +

3D Viewer 呈現 · USD over WebRTC

+

此頁說明打開 3D viewer 時 operator 應看到什麼:模型畫面、語意表、mapping table、selected prim、DataChannel ready、first frame、stage truth。真正 viewport 仍在既有 viewer,不在 console 內重渲染 WebRTC。

+ +
+ + + + +
+
+ + + + {capabilities.map(([cmd, desc, prov]) => ( + + ))} +
command / evidenceoperator 看到的功能status
{cmd}{desc}
+
+ +
+ + + +
+
+ + ); +} + +export function ConversionSchedulingPage() { + const [jobs, setJobs] = useState([]); + const [err, setErr] = useState(null); + const [busy, setBusy] = useState(false); + const load = useCallback(async () => { + setBusy(true); setErr(null); + try { setJobs((await coordinatorClient.listIfcReady(50)).items); } + catch (e) { setErr(`未連線 coordinator /api/external/ifc-ready:${String(e)}`); } + finally { setBusy(false); } + }, []); + useEffect(() => { void load(); }, [load]); + return ( + <> +

IFC→USD 轉檔排程

+

從 MinIO / storage 發現 source IFC,排進 conversion authority,由 `bim-streaming-server` 產出 `model.usdc`、mapping summary,再通知 Kit / Review Session。

+ {busy ? "讀取中…" : "Refresh queue"}}> + + {err &&

{err}

} + + + +
+ + {jobs.length ? ( + + {jobs.slice(0, 20).map((j) => ( + + ))}
jobprojectconversionsessionstage
{j.ifc_ready_job_id}{j.project_id}{j.conversion_status ?? "—"}{j.review_session_id ?? "—"}{j.expected_stage_url ?? "—"}
+ ) :

尚未取得 ifc-ready job;可由真實 IFC 進件頁註冊 fixture 後再回來看排程。

} +
+ + ); +} + +export function SessionManagementPage() { + const [rt, setRt] = useState(null); + const [err, setErr] = useState(null); + const load = useCallback(async () => { + setErr(null); + try { setRt(await coordinatorClient.runtimeStatus()); } + catch (e) { setErr(`未連線 coordinator /api/runtime/status:${String(e)}`); } + }, []); + useEffect(() => { void load(); }, [load]); + const sessions = rt?.sessions.items ?? []; + return ( + <> +

Session 管理 · Primary / Spectator ATC

+

每個 endpoint 像 runway,每個 primary / spectator viewer 像飛機。Open URL 不等於 occupied;occupied 必須有 browser first frame / heartbeat / stage match evidence。

+ 重新整理}> + {err &&

{err}

} +
+ + + + +
+
+ + {sessions.length ? ( + + {sessions.map((s) => ( + + ))}
sessionstatusparticipantsconversionstage
{s.session_id}{s.status}{s.participant_count}{s.conversion_status ?? "—"}{s.expected_stage_url ?? "—"}
+ ) :

目前 runtime status 無 active session;下面 endpoint pool 為治理規則示意。

} +
+ + Open primary URL{" "} + Open spectator URL{" "} + Reclaim stale spectator{" "} + Force release / restart primary + + + ); +} + +export function KitGpuFleetPage() { + return ( + <> +

Kit / GPU 機隊

+

此頁是 runtime operator 的機隊視角:哪台 GPU 在服務哪個 Kit stream,哪台可接新 session,哪些節點 drain,哪些 restart/release 必須由 Kit Manager 執行。

+ +
+ + + +
+
+ + + + + +
nodeGPUstateoperation
edge-gpu-01L40 · 48GBrunning · S-270drain / restart intent
edge-gpu-02L40 · 48GBrunning · S-899drain / restart intent
edge-gpu-03RTX 6000 · 48GBidleassign pending session
+

此表為 prototype fleet model 的 UI evidence;真實 restart/release 必須送 audited intent 給 Kit Manager,不能由 coordinator/browser 直接做。

+
+ + ); +} + +export function MinioDataPage() { + const files: [string, string, Prov][] = [ + ["model.ifc", "source IFC from customer-edge worker / MinIO", "asbuilt"], + ["model.rvt", "optional source RVT artifact", "demo"], + ["elements.json", "parsed metadata / semantic source", "asbuilt"], + ["geometries.json", "geometry extraction artifact", "asbuilt"], + ["chunks/", "large parsed chunks", "asbuilt"], + ["spatial_tree.json", "spatial lookup tree", "asbuilt"], + ["model.usdc", "expected generated output after conversion", "p1"], + ]; + return ( + <> +

MinIO 資料

+

資料頁讓 operator 看懂 project / model / version / files 關係;它不是完整 S3 browser。`bim-control` bucket 與 `model.usdc` writeback 是轉檔治理鏈的一部分。

+ +
+
bim-control/
+
project_real_ifc_demo/
+
OpenBIM/Architecture/v07/files/
+ {files.map(([f, d, p]) =>
{f} {d}
)} +
+
+ + + + + + + + ); +} + +export function ReportsPage() { + return ( + + ); +} + +export function AdminPage() { + return ( + + ); +} + +export function SpecPage() { + return ( + <> +

設計規格說明

+

此頁保留 prototype 到 repo 的落地對照:完整操作台是 frontend product shell;conversion / Kit / WebRTC / MinIO 權威仍在各自 repo 邊界。

+ + + + + + + + ); +} + +export function GpuReviewRoomPage() { + return ( + <> + + + + + + + ); +} + export function IssuesRuleCenterPage() { const [ifcPath, setIfcPath] = useState("C:\\Repos\\active\\iot\\AI-BIM-governance\\storage\\fixture-bytes.ifc"); const [idsPath, setIdsPath] = useState(""); @@ -444,7 +771,7 @@ export function VersionDiffPage() {

模型版本差異與責任追蹤 · A2

以 IFC GlobalId 多級對齊(GlobalId → Tag → type+name+location)比對兩個 model version, - 標記 added / removed / moved / property changed。純 CPU,不需 GPU。 + 標記 added / removed / moved / property changed;差異計算在 CPU 完成。

diff --git a/web-viewer-sample/src/console/routing.ts b/web-viewer-sample/src/console/routing.ts index 97148e119..edc21c322 100644 --- a/web-viewer-sample/src/console/routing.ts +++ b/web-viewer-sample/src/console/routing.ts @@ -4,10 +4,20 @@ // 1. ROOT /console[/...](dev)或 hash #/console[/...] —— 顯式 console 路徑,永遠 console(與 ?session= 無關)。 // 2. coordinator :8004 服務的 /ui pathname —— CH-E 正規 console 入口;viewer 改走 /ui/open 302 至 // viewer origin(server-side redirect,不經此 SPA path),故 /ui 預設掛 console。 -// 3. 短 hash #(coordinator|intake|runtime|review|kit|demo-control),允許 #/ 前綴(spec 用 #/kit、#/demo-control)。 +// 3. prototype/product console 短 hash,允許 #/ 前綴(例如 #/a1、#/viewer、#/minio)。 // pathname 只認根層 /console(避免 /foo/console 巢狀誤判)。/ui 與短 hash 讓位給 viewer 的 ?session= 進件 // (viewer attach 優先),避免覆寫既有 viewer 入口。 -const SHORT_CONSOLE_HASH = /^#\/?(coordinator|intake|runtime|review|kit|demo-control)$/; +const PRODUCT_CONSOLE_ROUTES = [ + "home", "overview", + "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10", + "viewer", "gpu", + "conv", "sessions", "instances", "minio", + "issues", "reports", "runtime", "admin", "spec", + "coordinator", "intake", "review", "semantic", + "version-diff", "federation", "apps", + "kit", "demo-control", +]; +const SHORT_CONSOLE_HASH = new RegExp(`^#/?(${PRODUCT_CONSOLE_ROUTES.join("|")})$`); export function isOperatorConsolePath(pathname: string, hash: string, search = ""): boolean { // 顯式 console 路徑:永遠 console。 if (/^\/console(?:\/|$)/.test(pathname)) return true; diff --git a/web-viewer-sample/src/main.tsx b/web-viewer-sample/src/main.tsx index 89b729571..b14792b26 100644 --- a/web-viewer-sample/src/main.tsx +++ b/web-viewer-sample/src/main.tsx @@ -12,7 +12,7 @@ import ReactDOM from "react-dom/client"; import App from "./App.tsx"; -import OperatorConsole from "./console/OperatorConsole"; +import EdgeConsole from "./console/EdgeConsole"; import { isOperatorConsolePath } from "./console/routing"; import "./index.css"; import "./styles/demo-theme.css"; @@ -37,5 +37,5 @@ if (initialSession && /^(lwv_|review_session_)[A-Za-z0-9_]+$/.test(initialSessio // /console[/...] 或 #/console[/...] 掛統一治理控制台 operator 三頁;其餘維持既有 viewer App 不變。 const useOperatorConsole = isOperatorConsolePath(window.location.pathname, window.location.hash, window.location.search); ReactDOM.createRoot(document.getElementById("root")!).render( - useOperatorConsole ? : + useOperatorConsole ? : ); From 2bb6e895eb9c2ac4313bf71396528db5abf0446d Mon Sep 17 00:00:00 2001 From: monkey1sai <26239865+monkey1sai@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:32:10 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(console):=20=E5=9B=9E=E6=87=89=20PR=20r?= =?UTF-8?q?eview=20=E2=80=94=20=E6=B6=88=E9=99=A4=20A1/session=20=E5=81=87?= =?UTF-8?q?=E6=8C=89=E9=88=95=E3=80=81playwright=20testMatch=E3=80=81coord?= =?UTF-8?q?inatorBase=20DRY?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A1 三顆 asbuilt 按鈕原本 enabled 但無 onClick(假按鈕,違反誠實鐵律)→ 改為 onClick 導向 #/issues(真實 Rule Center 執行 rule-run/建 issue/匯出 BCF)。 Session 管理「Controlled actions」原 3 顆 enabled 無 onClick → 全改 disabled + Phase 1 read-only caption(對齊 ATC Tower 既有唯讀模式);E2E 仍斷言按鈕可見故不破。 playwright.product-console.config 加 testMatch 僅收 product-console-integration / unified-console-routes(不誤跑需 webServer 的 viewer-harness)。defaultCoordinatorBase 抽到 coordinatorBase.ts 共用(消 coordinatorClient/governanceClient 重複,回應 Copilot DRY)。plan.md H3→H2(MD001)。 驗證:vitest 140 pass、build green、build:ui + coordinator 重啟(bundle hash DuDENF0U)、Playwright 3 e2e pass(testMatch 生效,僅 2 spec)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-09-edge-console-product-shell.md | 2 +- .../playwright.product-console.config.ts | 2 ++ web-viewer-sample/src/console/coordinatorBase.ts | 10 ++++++++++ .../src/console/coordinatorClient.ts | 9 ++------- .../src/console/governanceClient.ts | 9 ++------- web-viewer-sample/src/console/pages.tsx | 16 ++++++++-------- 6 files changed, 25 insertions(+), 23 deletions(-) create mode 100644 web-viewer-sample/src/console/coordinatorBase.ts diff --git a/docs/superpowers/plans/2026-06-09-edge-console-product-shell.md b/docs/superpowers/plans/2026-06-09-edge-console-product-shell.md index 6706c2710..7c229eee1 100644 --- a/docs/superpowers/plans/2026-06-09-edge-console-product-shell.md +++ b/docs/superpowers/plans/2026-06-09-edge-console-product-shell.md @@ -18,7 +18,7 @@ --- -### Task 1:建 branch + 套用 stash 基底 +## Task 1:建 branch + 套用 stash 基底 **Files:** 全 console 檔(apply)。 diff --git a/web-viewer-sample/playwright.product-console.config.ts b/web-viewer-sample/playwright.product-console.config.ts index 54b09dcac..cb29a25f7 100644 --- a/web-viewer-sample/playwright.product-console.config.ts +++ b/web-viewer-sample/playwright.product-console.config.ts @@ -4,6 +4,8 @@ import { defineConfig, devices } from "@playwright/test"; // It intentionally avoids the viewer webServer from playwright.config.ts. export default defineConfig({ testDir: "./e2e", + // 僅跑產品操作台 evidence specs;不收 viewer-harness 等需 webServer/baseURL 的 spec(避免相對導航失敗或誤跑無關套件)。 + testMatch: ["product-console-integration.spec.ts", "unified-console-routes.spec.ts"], outputDir: "../artifacts/e2e/_product_console_output", timeout: 60_000, expect: { timeout: 15_000 }, diff --git a/web-viewer-sample/src/console/coordinatorBase.ts b/web-viewer-sample/src/console/coordinatorBase.ts new file mode 100644 index 000000000..48f257ee6 --- /dev/null +++ b/web-viewer-sample/src/console/coordinatorBase.ts @@ -0,0 +1,10 @@ +// 共用:決定 coordinator API base。 +// served from coordinator /ui(非 dev port)→ same-origin;否則回退 127.0.0.1:8004。 +// 由 coordinatorClient.ts / governanceClient.ts 共用,避免兩處 defaultCoordinatorBase 重複(DRY)。 +export function defaultCoordinatorBase(): string { + if (typeof window === "undefined") return "http://127.0.0.1:8004"; + const { origin, pathname, port } = window.location; + const devPorts = new Set(["5173", "5174", "5180"]); + if (pathname.startsWith("/ui") && !devPorts.has(port)) return origin; + return "http://127.0.0.1:8004"; +} diff --git a/web-viewer-sample/src/console/coordinatorClient.ts b/web-viewer-sample/src/console/coordinatorClient.ts index 8047da94b..e3a37e096 100644 --- a/web-viewer-sample/src/console/coordinatorClient.ts +++ b/web-viewer-sample/src/console/coordinatorClient.ts @@ -16,14 +16,9 @@ // callback-outbox 直查(/api/internal/callback-outbox/:id)需 internal token,瀏覽器不可達 → // 不在此 client 提供;outbox 摘要改由 ifc_ready job 的 callback_outbox_id 觀察(誠實標 demo/未取得)。 +import { defaultCoordinatorBase } from "./coordinatorBase"; + const env = (import.meta as { env?: Record }).env; -function defaultCoordinatorBase(): string { - if (typeof window === "undefined") return "http://127.0.0.1:8004"; - const { origin, pathname, port } = window.location; - const devPorts = new Set(["5173", "5174", "5180"]); - if (pathname.startsWith("/ui") && !devPorts.has(port)) return origin; - return "http://127.0.0.1:8004"; -} const COORD_BASE: string = env?.VITE_COORDINATOR_API_BASE ?? env?.VITE_COORDINATOR_BASE ?? defaultCoordinatorBase(); diff --git a/web-viewer-sample/src/console/governanceClient.ts b/web-viewer-sample/src/console/governanceClient.ts index beb15bb44..a98217c76 100644 --- a/web-viewer-sample/src/console/governanceClient.ts +++ b/web-viewer-sample/src/console/governanceClient.ts @@ -3,14 +3,9 @@ // coordinator base 用全站/部署一致的正規 env 名 VITE_COORDINATOR_API_BASE(compose 注入、deploy.ps1 // 經 WEB_VIEWER_COORDINATOR_API_BASE 設定、config/env.ts 亦讀此名)。保留舊名 VITE_COORDINATOR_BASE // 為相容 fallback(正規名優先),預設與 config/env.ts 一致為 http://127.0.0.1:8004。 +import { defaultCoordinatorBase } from "./coordinatorBase"; + const env = (import.meta as { env?: Record }).env; -function defaultCoordinatorBase(): string { - if (typeof window === "undefined") return "http://127.0.0.1:8004"; - const { origin, pathname, port } = window.location; - const devPorts = new Set(["5173", "5174", "5180"]); - if (pathname.startsWith("/ui") && !devPorts.has(port)) return origin; - return "http://127.0.0.1:8004"; -} const COORD_BASE: string = env?.VITE_COORDINATOR_API_BASE ?? env?.VITE_COORDINATOR_BASE ?? defaultCoordinatorBase(); diff --git a/web-viewer-sample/src/console/pages.tsx b/web-viewer-sample/src/console/pages.tsx index 7861c8e20..7d7902e68 100644 --- a/web-viewer-sample/src/console/pages.tsx +++ b/web-viewer-sample/src/console/pages.tsx @@ -220,10 +220,10 @@ export function A1GovernanceWorkbenchPage() { ))} - - 執行規則檢核{" "} - 失敗構件建 issue{" "} - 匯出 BCF / Excel{" "} + + { window.location.hash = "issues"; }}>執行規則檢核{" "} + { window.location.hash = "issues"; }}>失敗構件建 issue{" "} + { window.location.hash = "issues"; }}>匯出 BCF / Excel{" "} 在 3D 高亮 @@ -335,10 +335,10 @@ export function SessionManagementPage() { ))} ) :

目前 runtime status 無 active session;下面 endpoint pool 為治理規則示意。

}
- - Open primary URL{" "} - Open spectator URL{" "} - Reclaim stale spectator{" "} + + Open primary URL{" "} + Open spectator URL{" "} + Reclaim stale spectator{" "} Force release / restart primary