visionA/visionA-frontend/src/stores/activity-store.ts
jim800121chen 99dea42239 feat(visionA-frontend): Phase 0 → 0.7 雲端前端(Next.js + OIDC redirect 流程)
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>
2026-05-01 11:21:36 +08:00

79 lines
2.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Activity Store — visionA CloudDashboard 時間軸用)
*
* 對齊:
* - `.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: [] }),
}));