From 99dea42239eb649b3427a7885d1ed086360cd7e9 Mon Sep 17 00:00:00 2001 From: jim800121chen Date: Fri, 1 May 2026 11:21:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(visionA-frontend):=20Phase=200=20=E2=86=92?= =?UTF-8?q?=200.7=20=E9=9B=B2=E7=AB=AF=E5=89=8D=E7=AB=AF=EF=BC=88Next.js?= =?UTF-8?q?=20+=20OIDC=20redirect=20=E6=B5=81=E7=A8=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit visionA 雲端版前端 — 沿用 local-tool 既有 UI(原則 4:先抄 local-tool)+ 新增雲端特有的登入 / 配對 / 設定流程,含以下整合階段: - Phase 0:13 頁 + 30+ 元件 + 雛形 banner - dashboard / devices / models / workspace / clusters / settings 等頁 - AppShell + Sidebar + Header + tokens + i18n(中英雙語 96 keys) - API client + 5 stores + 3 hooks - Phase 0.6:OIDC redirect 改造 - login 頁改為 OIDC redirect(`window.location.href = /api/auth/login`) - register 改說明頁、account 改唯讀(user 資料來源是 MC) - api client 改 cookie session(credentials: include)+ 完全清掉 localStorage - Phase 0.7:stage 部署 + nil guard - getApiBaseUrl() 修:browser 環境視為 same-origin(與 login 頁一致) - login 頁加「已登入 → router.replace('/')」effect - User type email/name 改 optional(MC id_token 不一定回 email/name claim) - header.tsx UserMenu displayName 4 層 fallback:name → email → id → i18n - 雛形 banner 文案更新(已接 Innovedus 帳號中心)+ 版號 Phase 0.7 驗證:pnpm lint / test (125/125) / build 全綠 Co-Authored-By: Claude Opus 4.7 (1M context) --- visionA-frontend/.env.local.example | 38 + visionA-frontend/.gitignore | 44 + visionA-frontend/README.md | 234 + visionA-frontend/components.json | 23 + visionA-frontend/eslint.config.mjs | 18 + visionA-frontend/next.config.ts | 15 + visionA-frontend/package.json | 43 + visionA-frontend/pnpm-lock.yaml | 6978 +++++++++++++++++ visionA-frontend/postcss.config.mjs | 7 + visionA-frontend/public/.gitkeep | 1 + .../src/app/account/account.test.tsx | 262 + visionA-frontend/src/app/account/page.tsx | 204 + visionA-frontend/src/app/clusters/page.tsx | 59 + .../app/devices/[id]/device-detail-client.tsx | 222 + .../src/app/devices/[id]/page.tsx | 19 + visionA-frontend/src/app/devices/page.tsx | 69 + .../src/app/devices/pair/page.tsx | 389 + visionA-frontend/src/app/globals.css | 239 + visionA-frontend/src/app/layout.tsx | 67 + visionA-frontend/src/app/login/login.test.tsx | 196 + visionA-frontend/src/app/login/page.tsx | 152 + .../app/models/[id]/model-detail-client.tsx | 188 + visionA-frontend/src/app/models/[id]/page.tsx | 10 + visionA-frontend/src/app/models/page.tsx | 56 + visionA-frontend/src/app/page.tsx | 165 + visionA-frontend/src/app/register/page.tsx | 125 + .../src/app/register/register.test.tsx | 143 + visionA-frontend/src/app/settings/page.tsx | 151 + .../src/app/workspace/[deviceId]/page.tsx | 10 + .../workspace/[deviceId]/workspace-client.tsx | 203 + visionA-frontend/src/app/workspace/page.tsx | 90 + visionA-frontend/src/components/.gitkeep | 1 + .../cloud/remote-device-badge.test.tsx | 69 + .../components/cloud/remote-device-badge.tsx | 158 + .../dashboard/activity-timeline.tsx | 132 + .../dashboard/connected-devices-list.tsx | 86 + .../components/dashboard/stat-card.test.tsx | 27 + .../src/components/dashboard/stat-card.tsx | 69 + .../src/components/devices/device-card.tsx | 96 + .../src/components/devices/device-list.tsx | 102 + .../src/components/layout/app-shell.tsx | 60 + .../src/components/layout/header.tsx | 399 + .../layout/prototype-banner.test.tsx | 88 + .../components/layout/prototype-banner.tsx | 51 + .../src/components/layout/sidebar.test.tsx | 108 + .../src/components/layout/sidebar.tsx | 157 + .../src/components/models/model-card.tsx | 100 + .../src/components/models/model-filters.tsx | 81 + .../src/components/models/model-grid.tsx | 72 + .../components/models/model-upload-dialog.tsx | 324 + .../components/pairing/pairing-countdown.tsx | 156 + .../components/pairing/pairing-token-card.tsx | 224 + .../src/components/store-hydration.tsx | 37 + .../src/components/theme-provider.tsx | 29 + visionA-frontend/src/components/ui/.gitkeep | 1 + .../src/components/ui/alert-dialog.tsx | 207 + visionA-frontend/src/components/ui/avatar.tsx | 66 + visionA-frontend/src/components/ui/badge.tsx | 55 + .../src/components/ui/button.test.tsx | 55 + visionA-frontend/src/components/ui/button.tsx | 75 + .../src/components/ui/card.test.tsx | 75 + visionA-frontend/src/components/ui/card.tsx | 100 + .../src/components/ui/checkbox.tsx | 40 + visionA-frontend/src/components/ui/dialog.tsx | 174 + .../src/components/ui/dropdown-menu.tsx | 282 + .../src/components/ui/empty-state.tsx | 57 + visionA-frontend/src/components/ui/input.tsx | 30 + visionA-frontend/src/components/ui/label.tsx | 33 + .../src/components/ui/progress.tsx | 39 + .../src/components/ui/scroll-area.tsx | 66 + visionA-frontend/src/components/ui/select.tsx | 200 + .../src/components/ui/separator.tsx | 35 + .../src/components/ui/skeleton.tsx | 23 + visionA-frontend/src/components/ui/slider.tsx | 71 + visionA-frontend/src/components/ui/sonner.tsx | 52 + .../src/components/ui/spinner.tsx | 46 + visionA-frontend/src/components/ui/tabs.tsx | 100 + .../src/components/ui/tooltip.tsx | 76 + visionA-frontend/src/hooks/.gitkeep | 1 + visionA-frontend/src/hooks/use-fetch.test.tsx | 112 + visionA-frontend/src/hooks/use-fetch.ts | 137 + .../src/hooks/use-tunnel-status.ts | 34 + visionA-frontend/src/hooks/use-websocket.ts | 157 + visionA-frontend/src/lib/.gitkeep | 1 + visionA-frontend/src/lib/api.test.ts | 321 + visionA-frontend/src/lib/api.ts | 491 ++ .../src/lib/i18n/context.test.tsx | 91 + visionA-frontend/src/lib/i18n/context.tsx | 92 + .../src/lib/i18n/dictionaries/en.ts | 349 + .../src/lib/i18n/dictionaries/zh-Hant.ts | 337 + visionA-frontend/src/lib/i18n/i18n.test.ts | 69 + visionA-frontend/src/lib/i18n/index.ts | 22 + visionA-frontend/src/lib/i18n/sync.tsx | 44 + visionA-frontend/src/lib/i18n/types.ts | 36 + visionA-frontend/src/lib/utils.ts | 12 + visionA-frontend/src/stores/.gitkeep | 1 + visionA-frontend/src/stores/activity-store.ts | 78 + .../src/stores/auth-store.test.ts | 280 + visionA-frontend/src/stores/auth-store.ts | 156 + .../stores/device-preferences-store.test.ts | 57 + .../src/stores/device-preferences-store.ts | 97 + .../src/stores/device-store.test.ts | 165 + visionA-frontend/src/stores/device-store.ts | 204 + .../src/stores/model-store.test.ts | 38 + visionA-frontend/src/stores/model-store.ts | 223 + visionA-frontend/src/stores/pairing-store.ts | 196 + .../src/stores/session-store.test.ts | 100 + visionA-frontend/src/stores/session-store.ts | 147 + visionA-frontend/src/tests/setup.ts | 8 + visionA-frontend/src/types/.gitkeep | 1 + visionA-frontend/src/types/api.ts | 48 + visionA-frontend/src/types/pairing.ts | 53 + visionA-frontend/src/types/user.ts | 40 + visionA-frontend/tsconfig.json | 34 + visionA-frontend/vitest.config.ts | 25 + 115 files changed, 19261 insertions(+) create mode 100644 visionA-frontend/.env.local.example create mode 100644 visionA-frontend/.gitignore create mode 100644 visionA-frontend/README.md create mode 100644 visionA-frontend/components.json create mode 100644 visionA-frontend/eslint.config.mjs create mode 100644 visionA-frontend/next.config.ts create mode 100644 visionA-frontend/package.json create mode 100644 visionA-frontend/pnpm-lock.yaml create mode 100644 visionA-frontend/postcss.config.mjs create mode 100644 visionA-frontend/public/.gitkeep create mode 100644 visionA-frontend/src/app/account/account.test.tsx create mode 100644 visionA-frontend/src/app/account/page.tsx create mode 100644 visionA-frontend/src/app/clusters/page.tsx create mode 100644 visionA-frontend/src/app/devices/[id]/device-detail-client.tsx create mode 100644 visionA-frontend/src/app/devices/[id]/page.tsx create mode 100644 visionA-frontend/src/app/devices/page.tsx create mode 100644 visionA-frontend/src/app/devices/pair/page.tsx create mode 100644 visionA-frontend/src/app/globals.css create mode 100644 visionA-frontend/src/app/layout.tsx create mode 100644 visionA-frontend/src/app/login/login.test.tsx create mode 100644 visionA-frontend/src/app/login/page.tsx create mode 100644 visionA-frontend/src/app/models/[id]/model-detail-client.tsx create mode 100644 visionA-frontend/src/app/models/[id]/page.tsx create mode 100644 visionA-frontend/src/app/models/page.tsx create mode 100644 visionA-frontend/src/app/page.tsx create mode 100644 visionA-frontend/src/app/register/page.tsx create mode 100644 visionA-frontend/src/app/register/register.test.tsx create mode 100644 visionA-frontend/src/app/settings/page.tsx create mode 100644 visionA-frontend/src/app/workspace/[deviceId]/page.tsx create mode 100644 visionA-frontend/src/app/workspace/[deviceId]/workspace-client.tsx create mode 100644 visionA-frontend/src/app/workspace/page.tsx create mode 100644 visionA-frontend/src/components/.gitkeep create mode 100644 visionA-frontend/src/components/cloud/remote-device-badge.test.tsx create mode 100644 visionA-frontend/src/components/cloud/remote-device-badge.tsx create mode 100644 visionA-frontend/src/components/dashboard/activity-timeline.tsx create mode 100644 visionA-frontend/src/components/dashboard/connected-devices-list.tsx create mode 100644 visionA-frontend/src/components/dashboard/stat-card.test.tsx create mode 100644 visionA-frontend/src/components/dashboard/stat-card.tsx create mode 100644 visionA-frontend/src/components/devices/device-card.tsx create mode 100644 visionA-frontend/src/components/devices/device-list.tsx create mode 100644 visionA-frontend/src/components/layout/app-shell.tsx create mode 100644 visionA-frontend/src/components/layout/header.tsx create mode 100644 visionA-frontend/src/components/layout/prototype-banner.test.tsx create mode 100644 visionA-frontend/src/components/layout/prototype-banner.tsx create mode 100644 visionA-frontend/src/components/layout/sidebar.test.tsx create mode 100644 visionA-frontend/src/components/layout/sidebar.tsx create mode 100644 visionA-frontend/src/components/models/model-card.tsx create mode 100644 visionA-frontend/src/components/models/model-filters.tsx create mode 100644 visionA-frontend/src/components/models/model-grid.tsx create mode 100644 visionA-frontend/src/components/models/model-upload-dialog.tsx create mode 100644 visionA-frontend/src/components/pairing/pairing-countdown.tsx create mode 100644 visionA-frontend/src/components/pairing/pairing-token-card.tsx create mode 100644 visionA-frontend/src/components/store-hydration.tsx create mode 100644 visionA-frontend/src/components/theme-provider.tsx create mode 100644 visionA-frontend/src/components/ui/.gitkeep create mode 100644 visionA-frontend/src/components/ui/alert-dialog.tsx create mode 100644 visionA-frontend/src/components/ui/avatar.tsx create mode 100644 visionA-frontend/src/components/ui/badge.tsx create mode 100644 visionA-frontend/src/components/ui/button.test.tsx create mode 100644 visionA-frontend/src/components/ui/button.tsx create mode 100644 visionA-frontend/src/components/ui/card.test.tsx create mode 100644 visionA-frontend/src/components/ui/card.tsx create mode 100644 visionA-frontend/src/components/ui/checkbox.tsx create mode 100644 visionA-frontend/src/components/ui/dialog.tsx create mode 100644 visionA-frontend/src/components/ui/dropdown-menu.tsx create mode 100644 visionA-frontend/src/components/ui/empty-state.tsx create mode 100644 visionA-frontend/src/components/ui/input.tsx create mode 100644 visionA-frontend/src/components/ui/label.tsx create mode 100644 visionA-frontend/src/components/ui/progress.tsx create mode 100644 visionA-frontend/src/components/ui/scroll-area.tsx create mode 100644 visionA-frontend/src/components/ui/select.tsx create mode 100644 visionA-frontend/src/components/ui/separator.tsx create mode 100644 visionA-frontend/src/components/ui/skeleton.tsx create mode 100644 visionA-frontend/src/components/ui/slider.tsx create mode 100644 visionA-frontend/src/components/ui/sonner.tsx create mode 100644 visionA-frontend/src/components/ui/spinner.tsx create mode 100644 visionA-frontend/src/components/ui/tabs.tsx create mode 100644 visionA-frontend/src/components/ui/tooltip.tsx create mode 100644 visionA-frontend/src/hooks/.gitkeep create mode 100644 visionA-frontend/src/hooks/use-fetch.test.tsx create mode 100644 visionA-frontend/src/hooks/use-fetch.ts create mode 100644 visionA-frontend/src/hooks/use-tunnel-status.ts create mode 100644 visionA-frontend/src/hooks/use-websocket.ts create mode 100644 visionA-frontend/src/lib/.gitkeep create mode 100644 visionA-frontend/src/lib/api.test.ts create mode 100644 visionA-frontend/src/lib/api.ts create mode 100644 visionA-frontend/src/lib/i18n/context.test.tsx create mode 100644 visionA-frontend/src/lib/i18n/context.tsx create mode 100644 visionA-frontend/src/lib/i18n/dictionaries/en.ts create mode 100644 visionA-frontend/src/lib/i18n/dictionaries/zh-Hant.ts create mode 100644 visionA-frontend/src/lib/i18n/i18n.test.ts create mode 100644 visionA-frontend/src/lib/i18n/index.ts create mode 100644 visionA-frontend/src/lib/i18n/sync.tsx create mode 100644 visionA-frontend/src/lib/i18n/types.ts create mode 100644 visionA-frontend/src/lib/utils.ts create mode 100644 visionA-frontend/src/stores/.gitkeep create mode 100644 visionA-frontend/src/stores/activity-store.ts create mode 100644 visionA-frontend/src/stores/auth-store.test.ts create mode 100644 visionA-frontend/src/stores/auth-store.ts create mode 100644 visionA-frontend/src/stores/device-preferences-store.test.ts create mode 100644 visionA-frontend/src/stores/device-preferences-store.ts create mode 100644 visionA-frontend/src/stores/device-store.test.ts create mode 100644 visionA-frontend/src/stores/device-store.ts create mode 100644 visionA-frontend/src/stores/model-store.test.ts create mode 100644 visionA-frontend/src/stores/model-store.ts create mode 100644 visionA-frontend/src/stores/pairing-store.ts create mode 100644 visionA-frontend/src/stores/session-store.test.ts create mode 100644 visionA-frontend/src/stores/session-store.ts create mode 100644 visionA-frontend/src/tests/setup.ts create mode 100644 visionA-frontend/src/types/.gitkeep create mode 100644 visionA-frontend/src/types/api.ts create mode 100644 visionA-frontend/src/types/pairing.ts create mode 100644 visionA-frontend/src/types/user.ts create mode 100644 visionA-frontend/tsconfig.json create mode 100644 visionA-frontend/vitest.config.ts diff --git a/visionA-frontend/.env.local.example b/visionA-frontend/.env.local.example new file mode 100644 index 0000000..c234e6c --- /dev/null +++ b/visionA-frontend/.env.local.example @@ -0,0 +1,38 @@ +# visionA Cloud Frontend — 環境變數範本 +# +# 複製一份為 `.env.local`,並依實際環境填入。 +# `.env.local` 由 .gitignore 忽略,**切勿 commit**。 +# +# 所有 runtime 變數都以 `NEXT_PUBLIC_` 開頭(Next.js 要求前端可讀)。 +# ⚠️ 因此不要放真正機密的值——這類值應走後端 API。 + +# 後端 API server base URL(不含 trailing slash) +# 預設對應 visionA-backend 本地開發 port(TDD §1.4 api-server 預設 3001; +# F5 雛形階段允許以 3721 對應 local-tool 既有後端,以方便整合測試) +# +# ⚠️ BFF Pattern(OF2 / Phase 0.6 OIDC 之後): +# - 所有 API 請求帶 `credentials: 'include'`,瀏覽器會自動攜帶 backend 設的 +# `visiona_session` HttpOnly cookie。 +# - 跨 origin(本範例:frontend localhost:3000 ↔ backend localhost:3721)時, +# backend CORS 必須回傳: +# * `Access-Control-Allow-Credentials: true` +# * `Access-Control-Allow-Origin: http://localhost:3000`(明確 origin,**禁止 `*`**) +# - 上述條件任一不符 → 瀏覽器會在 console 報 CORS 錯誤、fetch 拋 NetworkError。 +# - 同 origin 部署(prod 通常將 frontend / backend 放同網域)時無此限制。 +NEXT_PUBLIC_API_BASE=http://localhost:3721 + +# WebSocket base URL(通常是 API base 改 ws/wss;同源部署時可留空讓前端自動推導) +NEXT_PUBLIC_WS_BASE=ws://localhost:3721 + +# Innovedus Member Center 註冊頁 URL(Phase 0.6 OIDC 接入;對齊 oidc-tdd.md §10.1 / §10.5) +# - login 頁的「前往註冊」連結會指向這裡(在新分頁開啟) +# - register 頁的「前往 Innovedus 帳號中心」按鈕也會跳到這裡(同分頁完整導航) +# - 未設定(或留空)→ 兩處 UI 均會 disable(aria-disabled="true") +# - dev 預設指向 Member Center 本地 dev port 5050 +NEXT_PUBLIC_MEMBER_CENTER_REGISTER_URL=http://localhost:5050/account/register + +# Innovedus Member Center profile 頁 URL(Phase 0.6 OIDC 接入;對齊 oidc-tdd.md §10.3) +# - /account 頁「前往 Innovedus 帳號中心」按鈕會指向這裡(在新分頁開啟) +# - 未設定(或留空)→ 按鈕會被替換為「未設定」hint +# - dev 預設指向 Member Center 本地 dev port 5050 +NEXT_PUBLIC_MEMBER_CENTER_PROFILE_URL=http://localhost:5050/account/profile diff --git a/visionA-frontend/.gitignore b/visionA-frontend/.gitignore new file mode 100644 index 0000000..82991c6 --- /dev/null +++ b/visionA-frontend/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ +/dist/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files(可依需要 opt-in 提交,例如 .env.example / .env.local.example) +.env* +!.env.example +!.env.local.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/visionA-frontend/README.md b/visionA-frontend/README.md new file mode 100644 index 0000000..c0810d8 --- /dev/null +++ b/visionA-frontend/README.md @@ -0,0 +1,234 @@ +# visionA Cloud Frontend + +> visionA Cloud 前端 Web 應用(Next.js 16 + React 19 + Tailwind 4)。 +> +> 目的:將 `local-tool/` 的使用體驗延伸到雲端,讓使用者透過瀏覽器操作由 local agent 連入的邊緣裝置(Kneron KL520/KL720 等)。 + +> 🚧 **Phase 0 雛形** — 任何功能皆以 UI 走通為目標,不做真實身分驗證、OAuth、WebSocket 即時推送、MJPEG 串流。完整功能清單見下方「雛形範圍與限制」。 +> +> 規劃中的正式版請見 [`.autoflow/02-prd/PRD.md`](../.autoflow/02-prd/PRD.md),架構決策見 [`.autoflow/04-architecture/design-doc.md`](../.autoflow/04-architecture/design-doc.md)。 + +--- + +## 技術堆疊 + +| 層級 | 技術 | 版本 | +|------|------|------| +| 框架 | Next.js(App Router) | 16.1.6 | +| UI 函式庫 | React / React DOM | 19.2.3 | +| 語言 | TypeScript | ^5 | +| 樣式 | Tailwind CSS(含 `@tailwindcss/postcss`)| ^4 | +| 元件 primitive | Radix UI | ^1.4.3 | +| 元件規範 | shadcn `new-york` style(`components.json`) | — | +| 圖示 | lucide-react | ^0.575.0 | +| 狀態管理 | Zustand | ^5.0.11 | +| 主題 | next-themes | ^0.4.6 | +| Toast | sonner | ^2.0.7 | +| Class 工具 | clsx + tailwind-merge + class-variance-authority | — | +| 測試 | Vitest + @testing-library/react + jsdom | ^4 / ^16 / ^28 | +| Lint | ESLint + eslint-config-next | ^9 / 16.1.6 | + +版本與 [`local-tool/frontend/`](../local-tool/frontend/) 對齊,確保「雲端版 / 離線版同一套前端」的架構決策可執行。 + +--- + +## 前置需求 + +- Node.js **≥ 20** +- pnpm **≥ 10** +- **依賴:** 需同時運行 [`visionA-backend`](../visionA-backend/README.md)(Go)。前端所有 API 呼叫會指向 `NEXT_PUBLIC_API_BASE`,後端未啟動時功能會顯示錯誤但 UI 仍可瀏覽(多數頁面對 501 / 連線錯誤已做 graceful fallback)。 + +--- + +## 快速啟動 + +```bash +# 1. 安裝依賴 +cd visionA-frontend +pnpm install + +# 2. 設定環境變數 +cp .env.local.example .env.local +# 檢查 NEXT_PUBLIC_API_BASE 是否指到正確後端位址 + +# 3. 啟動後端(另一個 terminal) +cd ../visionA-backend +make run # 或參考 visionA-backend/README.md + +# 4. 啟動前端 +cd ../visionA-frontend +pnpm dev # http://localhost:3000 +``` + +進入後**任何 email + 任何密碼**都可登入(Phase 0 的 StaticAuthProvider)。 + +### 可用腳本 + +| 指令 | 說明 | +|------|------| +| `pnpm dev` | 開發模式(Turbopack / HMR) | +| `pnpm build` | 產線打包(`output: "standalone"`,便於 Docker 部署) | +| `pnpm start` | 啟動 production build | +| `pnpm lint` | 執行 ESLint | +| `pnpm test` | 執行 Vitest(一次性) | +| `pnpm test:watch` | 執行 Vitest(watch mode) | + +--- + +## 專案結構 + +``` +visionA-frontend/ +├── src/ +│ ├── app/ # Next.js App Router 路由 +│ │ ├── layout.tsx # Root layout(Theme / Locale / AppShell) +│ │ ├── page.tsx # / Dashboard +│ │ ├── login/ # /login +│ │ ├── register/ # /register(Phase 0 Coming Soon) +│ │ ├── account/ # /account 帳號設定 stub +│ │ ├── devices/ # /devices, /devices/[id], /devices/pair +│ │ ├── models/ # /models, /models/[id] +│ │ ├── workspace/ # /workspace, /workspace/[deviceId] +│ │ ├── clusters/ # /clusters(Phase 0 預告頁) +│ │ └── settings/ # /settings +│ ├── components/ +│ │ ├── ui/ # Shadcn primitive(Button / Card / Input ...) +│ │ ├── layout/ # Sidebar / Header / PrototypeBanner / AppShell +│ │ ├── dashboard/ # StatCard / ActivityTimeline / ConnectedDevicesList +│ │ ├── devices/ # DeviceCard 相關 +│ │ ├── models/ # ModelCard / ModelUploadDialog +│ │ ├── cloud/ # RemoteDeviceBadge +│ │ └── pairing/ # PairingTokenCard / PairingCountdown +│ ├── hooks/ # useFetch / useWebsocket / useTunnelStatus +│ ├── lib/ +│ │ ├── api.ts # 統一 API client(envelope / 401 / 501 / timeout) +│ │ ├── utils.ts # cn() 等 +│ │ └── i18n/ # zh-Hant / en 字典 + Context +│ ├── stores/ # Zustand stores(auth / session / device / model / activity / pairing) +│ ├── tests/ # Vitest setup +│ └── types/ # 共用型別(api / user / pairing ...) +├── public/ # 靜態資源 +├── components.json # Shadcn CLI 設定 +├── next.config.ts # output: standalone +├── tsconfig.json # @/* → ./src/* +└── package.json +``` + +--- + +## 主要頁面清單 + +| 路徑 | Phase 0 狀態 | 說明 | +|------|-------------|------| +| `/login` | ✅ 可用 | 任意帳密皆通過(StaticAuth) | +| `/register` | ⚠️ Coming Soon | 引導回 /login;Phase 1 才實作 | +| `/` Dashboard | ✅ 可用 | StatCard × 4、近期活動、快速操作;無裝置時顯示空狀態引導 /devices/pair | +| `/devices` | ✅ 可用 | 裝置列表;RemoteDeviceBadge 顯示遠端連線狀態 | +| `/devices/[id]` | ✅ 可用 | 裝置詳細、離線 banner | +| `/devices/pair` | ✅ 可用(核心) | 三步配對流程:產生 Pairing Token → 15 分鐘倒數 → 輪詢 3 分鐘 | +| `/models` | ✅ 可用 | 模型列表 + 上傳 Dialog(XHR 進度) | +| `/models/[id]` | ✅ 可用 | 模型詳細、部署到裝置 | +| `/workspace` | ✅ 可用 | 選擇線上裝置 | +| `/workspace/[deviceId]` | ⚠️ Camera placeholder | 推論 Start/Stop 可按;MJPEG stream Phase 1 接上 | +| `/clusters` | ⚠️ Phase 1 預告 | POC 有完整實作,雛形後端是 stub | +| `/account` | ✅ 可用 stub | 顯示使用者 + 登出;其他操作是 toast 提醒 | +| `/settings` | ✅ 可用 | 語言 / 主題 / API 端點切換 | + +--- + +## 環境變數 + +| 變數 | 說明 | 預設值 | 必要 | +|------|------|--------|------| +| `NEXT_PUBLIC_API_BASE` | 雲端 API Server 位址(無尾斜線) | `http://localhost:3721` | 是 | +| `NEXT_PUBLIC_WS_BASE` | WebSocket 位址;留空則從 API 推導 | 從 API base 推導 | 否 | + +所有 runtime 變數都以 `NEXT_PUBLIC_` 開頭(Next.js 要求前端可讀),**不要放真正機密的值**——這類值應走後端。 + +--- + +## Phase 0 雛形範圍與限制 + +### ✅ 可做到的 +- 完整 UI 骨架(Sidebar / Header / 全域雛形 Banner / Sonner toast) +- i18n(zh-Hant / en)可即時切換,key 集合兩語系強制同步 +- Light / Dark / System 主題切換 +- 裝置 / 模型 / 活動 三個 store 的 CRUD UI(對齊 api-spec) +- **Pairing 流程**:產 token → 視覺切兩行顯示(複製永遠是完整 36 字元)→ 15 分鐘倒數(3 色階段)→ 輪詢連線狀態 → 成功 toast + 跳轉 +- 離線降級:`RemoteDeviceBadge` + 頁面離線 Banner + Workspace 遮罩 +- 模型上傳:XHR progress + presigned URL 直送 storage + +### ❌ Phase 0 刻意不做(Phase 1 接手) +- **任何帳密可登入** — 後端 `StaticAuthProvider` 回 `demo-user`;重啟後端後前端 localStorage 的 token 雖然還在,但伺服器上沒有任何 session 資料 +- **OAuth / 2FA / 密碼重設** — 全部 Phase 1 +- **註冊功能** — 顯示 Coming Soon 頁面 +- **重啟後端 → 裝置 / 模型資料消失** — 後端目前是 InMemoryRepository +- **WebSocket 即時推送** — 事件 / 推論結果 / 配對狀態的 WS endpoint 目前回 501,前端改用 3 秒輪詢(pairing 頁) +- **Camera / MJPEG 串流** — `/workspace/[deviceId]` 的 Camera tab 顯示 placeholder,Image / Video / Batch tabs disabled +- **Cluster CRUD** — `/clusters` 顯示 Phase 1 預告頁 +- **Account CRUD** — 變更個人資料 / 刪除帳號都是 toast stub +- **真實 SHA-256 checksum** — 模型上傳用 `placeholder:{size}:{nameLen}`(詳見 `model-upload-dialog.tsx` §placeholderChecksum) + +前端安全債詳見 [`.autoflow/04-architecture/security.md`](../.autoflow/04-architecture/security.md) §14(localStorage token / WS querystring / CSP / CSRF 等 6 項,每項有 Phase 1 計畫)。 + +--- + +## 效能預算(Phase 1 開始嚴格執行) + +| 資源 | 預算 | +|------|------| +| JavaScript(首次載入,壓縮後) | < 200 KB | +| CSS(壓縮後) | < 50 KB | +| 首次載入總大小 | < 500 KB | +| Core Web Vitals | LCP < 2.5s、INP < 200ms、CLS < 0.1 | + +雛形(Phase 0)階段僅確保 `pnpm build` 成功;CI 中加入 bundle 檢查由 DevOps 任務處理。 + +--- + +## 無障礙規範 + +目標 **WCAG 2.2 AA**(詳見 [`.autoflow/03-design/design-spec.md`](../.autoflow/03-design/design-spec.md) §8): + +- 色彩對比:一般文字 ≥ 4.5:1、大文字 ≥ 3:1 +- 鍵盤可操作所有核心流程(Tab / Enter / Escape) +- Focus ring 可見(shadcn 預設 `ring-2 ring-ring`) +- 語意化 HTML(`