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) <noreply@anthropic.com>
79 lines
2.2 KiB
TypeScript
79 lines
2.2 KiB
TypeScript
/**
|
||
* Activity Store — visionA Cloud(Dashboard 時間軸用)
|
||
*
|
||
* 對齊:
|
||
* - `.autoflow/03-design/components.md` §4 Dashboard `ActivityTimeline`
|
||
* - `.autoflow/03-design/flows/flow-offline-handling.md` §9 Activity Timeline 擴充
|
||
*
|
||
* 職責:
|
||
* - 保存最近的活動事件(配對、裝置上下線、燒錄、模型上傳等)
|
||
* - 容量上限 100 筆(超過從頭 drop)
|
||
*
|
||
* F6 範圍(雛形):
|
||
* - 事件來源尚未接(F7/F8 會從 WS `/ws/devices/events` 推入 + login / upload 事件 seed)
|
||
* - 這個 store 先建立,讓 Dashboard 能 render empty state;真實事件由 F7+ 補
|
||
*/
|
||
|
||
"use client";
|
||
|
||
import { create } from "zustand";
|
||
|
||
/** 活動類型(對齊 flow-offline-handling.md §9 雲端版 + local-tool 既有類型) */
|
||
export type ActivityType =
|
||
| "device_paired"
|
||
| "device_unpaired"
|
||
| "device_online"
|
||
| "device_offline"
|
||
| "tunnel_reconnected"
|
||
| "flash_start"
|
||
| "flash_complete"
|
||
| "flash_error"
|
||
| "model_upload"
|
||
| "model_delete"
|
||
| "cluster_degraded";
|
||
|
||
export interface ActivityEntry {
|
||
id: string;
|
||
type: ActivityType;
|
||
message: string;
|
||
/** Unix ms(用 `Date.now()`;方便前端相對時間格式化) */
|
||
timestamp: number;
|
||
/** 可選關聯 */
|
||
deviceId?: string;
|
||
modelId?: string;
|
||
}
|
||
|
||
const ACTIVITY_LIMIT = 100;
|
||
|
||
interface ActivityState {
|
||
activities: ActivityEntry[];
|
||
addActivity: (entry: Omit<ActivityEntry, "id" | "timestamp"> & {
|
||
id?: string;
|
||
timestamp?: number;
|
||
}) => void;
|
||
clear: () => void;
|
||
}
|
||
|
||
export const useActivityStore = create<ActivityState>()((set) => ({
|
||
activities: [],
|
||
|
||
addActivity: (entry) =>
|
||
set((state) => {
|
||
const id = entry.id ?? `act_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||
const timestamp = entry.timestamp ?? Date.now();
|
||
const next: ActivityEntry = {
|
||
id,
|
||
type: entry.type,
|
||
message: entry.message,
|
||
timestamp,
|
||
deviceId: entry.deviceId,
|
||
modelId: entry.modelId,
|
||
};
|
||
// 新事件放最前面,超過上限從尾部 drop
|
||
const combined = [next, ...state.activities].slice(0, ACTIVITY_LIMIT);
|
||
return { activities: combined };
|
||
}),
|
||
|
||
clear: () => set({ activities: [] }),
|
||
}));
|