@@ -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 願景;本欄先顯示 page-aware prompts 與 tool trace 版型,狀態改動需人工確認與 audit。
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/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 fab44fa7f..e3a37e096 100644
--- a/web-viewer-sample/src/console/coordinatorClient.ts
+++ b/web-viewer-sample/src/console/coordinatorClient.ts
@@ -16,9 +16,12 @@
// 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;
+
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..a98217c76 100644
--- a/web-viewer-sample/src/console/governanceClient.ts
+++ b/web-viewer-sample/src/console/governanceClient.ts
@@ -3,9 +3,12 @@
// 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;
+
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..7d7902e68 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 (
+
+ );
+}
+
+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 派發)。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | rule | 用途 | state |
+ {rules.map(([rule, use, state]) => (
+ | {rule} | {use} | {state === "passed" ? : } |
+ ))}
+
+
+
+ { window.location.hash = "issues"; }}>執行規則檢核{" "}
+ { window.location.hash = "issues"; }}>失敗構件建 issue{" "}
+ { window.location.hash = "issues"; }}>匯出 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。
+
+
+
+
+
+
+
+
+
+
+ | command / evidence | operator 看到的功能 | status |
+ {capabilities.map(([cmd, desc, prov]) => (
+ | {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 ? (
+ | job | project | conversion | session | stage |
+ {jobs.slice(0, 20).map((j) => (
+ | {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 ? (
+ | session | status | participants | conversion | stage |
+ {sessions.map((s) => (
+ | {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 執行。
+
+
+
+
+
+
+
+
+ | node | GPU | state | operation |
+ | edge-gpu-01 | L40 · 48GB | running · S-270 | drain / restart intent |
+ | edge-gpu-02 | L40 · 48GB | running · S-899 | drain / restart intent |
+ | edge-gpu-03 | RTX 6000 · 48GB | idle | assign 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]) =>
)}
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+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 ?
:
);