From b85a0e232e62dc74d0430cbd0e13ee1adb4a3d83 Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 9 Apr 2026 15:51:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20S4+S5=20=E8=99=95=E7=BD=AE?= =?UTF-8?q?=E7=B5=B1=E8=A8=88=E7=92=B0=E5=BD=A2=E5=9C=96=20+=20=E6=9C=80?= =?UTF-8?q?=E8=BF=91=E6=B4=BB=E5=8B=95=E6=99=82=E9=96=93=E7=B7=9A=20?= =?UTF-8?q?=E2=80=94=20Sprint=205R?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - S4: DispositionMini 元件 (SVG 環形圖 + 四類列表) - S5: RecentActivity 元件 (時間線 + 色點 + JetBrains Mono) - 左欄改為 flex:6 可滾動多卡片列 - 右欄改為 flex:4 (60:40 比例) - 左欄結構: 活躍事件 → 處置統計 → 最近活動 Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/app/[locale]/page.tsx | 110 ++++++++++-------- .../components/shared/disposition-mini.tsx | 106 +++++++++++++++++ .../src/components/shared/recent-activity.tsx | 86 ++++++++++++++ 3 files changed, 252 insertions(+), 50 deletions(-) create mode 100644 apps/web/src/components/shared/disposition-mini.tsx create mode 100644 apps/web/src/components/shared/recent-activity.tsx diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx index f4763a70..5eac2b83 100644 --- a/apps/web/src/app/[locale]/page.tsx +++ b/apps/web/src/app/[locale]/page.tsx @@ -28,6 +28,8 @@ import { PageTabs, type TabConfig } from '@/components/layout/page-tabs' import { LobsterLoading } from '@/components/shared/lobster-loading' import { ServiceTopology } from '@/components/topology' import { BarChart3, Flame, Telescope, FlaskConical, Activity, GitBranch } from 'lucide-react' +import { DispositionMini } from '@/components/shared/disposition-mini' +import { RecentActivity } from '@/components/shared/recent-activity' const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' @@ -766,70 +768,78 @@ export default function Home({ params }: { params: { locale: string } }) { {/* ── 主體 2 欄 ─────────────────────────────────────────────────────── */}
- {/* ── Feed:活躍事件 (flex:1) ───────────────────────────────────── */} + {/* ── 左欄 (60%): 活躍事件 + 處置統計 + 最近活動 ─────────────── */}
+ + {/* 活躍事件 */}
-
- - {tDashboard('activeIncidents')} - - {(incidents?.length ?? 0) > 0 && ( - - {incidents?.length} +
+
+ + {tDashboard('activeIncidents')} - )} + {(incidents?.length ?? 0) > 0 && ( + + {incidents?.length} + + )} + 查看全部告警 → +
+ +
+ {isIncidentsLoading ? ( + + ) : incidentsError ? ( +
{incidentsError}
+ ) : (incidents?.length ?? 0) === 0 ? ( +
+
+ {tDashboard('stable')} · 0 {tDashboard('activeIncidents')} +
+ ) : ( +
+ {incidents?.map((incident) => ( + + ))} +
+ )} +
-
- {isIncidentsLoading ? ( - - ) : incidentsError ? ( -
{incidentsError}
- ) : (incidents?.length ?? 0) === 0 ? ( -
-
- {tDashboard('stable')} · 0 {tDashboard('activeIncidents')} -
- ) : ( -
- {incidents?.map((incident) => ( - - ))} -
- )} -
+ {/* 處置統計迷你版 (S4) */} + + + {/* 最近活動 (S5) */} + +
- {/* ── Right Panel:AI + Infra (width:530px) ────────────────────── */} + {/* ── 右欄 (40%): OpenClaw + 基礎架構 + 監控工具 ─────────────── */}
{/* WoooClaw + Reasoning Stream */} diff --git a/apps/web/src/components/shared/disposition-mini.tsx b/apps/web/src/components/shared/disposition-mini.tsx new file mode 100644 index 00000000..0cbaf074 --- /dev/null +++ b/apps/web/src/components/shared/disposition-mini.tsx @@ -0,0 +1,106 @@ +'use client' + +/** + * DispositionMini — 處置統計迷你版 (環形圖 + 四類列表) + * Sprint 5R S4: 設計稿 L386-414 + * @created 2026-04-09 Claude Opus 4.6 Asia/Taipei + */ + +import { useState, useEffect } from 'react' +import { useTranslations } from 'next-intl' + +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' + +interface DispositionSummary { + total: number + auto_rate: number + by_type: { auto_repair: number; human_approved: number; manual_resolved: number; cold_start_trust: number } +} + +export function DispositionMini() { + const t = useTranslations('dashboard') + const [data, setData] = useState(null) + + useEffect(() => { + fetch(`${API_BASE}/api/v1/stats/disposition`) + .then(r => r.ok ? r.json() : null) + .then(d => { if (d?.summary) setData(d.summary) }) + .catch(() => {}) + }, []) + + if (!data || data.total === 0) return null + + const { auto_repair = 0, human_approved = 0, manual_resolved = 0, cold_start_trust = 0 } = data.by_type ?? {} + const total = data.total || 1 + const pct = Math.round(data.auto_rate * 100) + + // SVG 環形圖計算 (circumference = 2 * PI * 22 ≈ 138.2) + const C = 2 * Math.PI * 22 + const segments = [ + { value: auto_repair, color: '#22C55E' }, + { value: human_approved, color: '#F59E0B' }, + { value: manual_resolved, color: '#A855F7' }, + { value: cold_start_trust, color: '#4A90D9' }, + ] + let offset = 0 + const arcs = segments.map(seg => { + const len = (seg.value / total) * C + const arc = { len, gap: C - len, offset: -offset, color: seg.color } + offset += len + return arc + }) + + const items = [ + { label: t('autoRepairLabel'), value: auto_repair, color: '#22C55E' }, + { label: t('humanApprovedLabel'), value: human_approved, color: '#F59E0B' }, + { label: t('manualResolvedLabel'), value: manual_resolved, color: '#A855F7' }, + { label: t('coldStartLabel'), value: cold_start_trust, color: '#4A90D9' }, + ] + + return ( +
+
+
+ {t('dispositionBreakdown')} + 查看完整報表 → +
+
+ {/* 環形圖 */} +
+ + + {arcs.map((arc, i) => ( + + ))} + +
+ {pct}% +
+
+ {/* 列表 */} +
+ {items.map(item => ( +
+ + {item.label} + {item.value} +
+ ))} +
+
+
+ ) +} diff --git a/apps/web/src/components/shared/recent-activity.tsx b/apps/web/src/components/shared/recent-activity.tsx new file mode 100644 index 00000000..e2d55537 --- /dev/null +++ b/apps/web/src/components/shared/recent-activity.tsx @@ -0,0 +1,86 @@ +'use client' + +/** + * RecentActivity — 最近活動時間線 + * Sprint 5R S5: 設計稿 L416-429 + * @created 2026-04-09 Claude Opus 4.6 Asia/Taipei + */ + +import { useState, useEffect } from 'react' +import { useTranslations } from 'next-intl' + +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' + +interface LogEntry { + id: string + event_type: string + action_detail: string | null + actor: string | null + created_at: string +} + +const EVENT_COLOR: Record = { + RESOLVED: '#22C55E', + EXECUTION_COMPLETED: '#22C55E', + ALERT_RECEIVED: '#cc2200', + AUTO_REPAIR_TRIGGERED: '#4A90D9', + TELEGRAM_SENT: '#4A90D9', + EXECUTION_STARTED: '#F59E0B', +} + +export function RecentActivity() { + const t = useTranslations('dashboard') + const [logs, setLogs] = useState([]) + + useEffect(() => { + fetch(`${API_BASE}/api/v1/alert-operation-logs?limit=5`) + .then(r => r.ok ? r.json() : { items: [] }) + .then(d => setLogs(d.items ?? [])) + .catch(() => {}) + }, []) + + if (logs.length === 0) return null + + return ( +
+
+
+ {t('activityStream')} + 查看活動串流 → +
+
+ {logs.map((log, i) => { + const time = (() => { + try { + return new Date(log.created_at).toLocaleTimeString('zh-TW', { timeZone: 'Asia/Taipei', hour: '2-digit', minute: '2-digit' }) + } catch { return '--' } + })() + const dotColor = EVENT_COLOR[log.event_type] ?? '#87867f' + const detail = log.action_detail || log.event_type.replace(/_/g, ' ').toLowerCase() + + return ( +
+ {time} + + + {log.actor && {log.actor}} + {log.actor && ' · '} + {detail} + +
+ ) + })} +
+
+ ) +}