From 2f02f1523a4dec4e1a1328ee05efdec5d3f8f196 Mon Sep 17 00:00:00 2001 From: OG T Date: Tue, 31 Mar 2026 16:04:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20#126=20Frontend=20Replay=20UI=20?= =?UTF-8?q?=E6=95=B4=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 useUXAudit hook (5 分鐘自動刷新) - 新增 UXAuditCard 組件 (健康度 + Replay 連結) - 整合到錯誤追蹤頁面 - i18n: zh-TW + en 翻譯 功能: - UX 健康度評分 (good/moderate/poor) - 有錯誤的 Replay 連結 - 憤怒點擊/死亡點擊統計 - Replay Dashboard 快捷連結 Co-Authored-By: Claude Opus 4.5 --- .gitea/workflows/cd.yaml | 26 +- apps/web/messages/en.json | 18 ++ apps/web/messages/zh-TW.json | 18 ++ apps/web/src/app/[locale]/errors/page.tsx | 16 + apps/web/src/components/errors/index.ts | 3 + .../src/components/errors/ux-audit-card.tsx | 293 ++++++++++++++++++ apps/web/src/hooks/useUXAudit.ts | 81 +++++ apps/web/src/lib/api-client.ts | 36 +++ docs/LOGBOOK.md | 42 ++- 9 files changed, 530 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/components/errors/ux-audit-card.tsx create mode 100644 apps/web/src/hooks/useUXAudit.ts diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index c8df4c0f..61cd14d2 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -103,11 +103,14 @@ jobs: # 2026-03-31 ogt: 移除中間通知 # 2026-03-31 ogt: P0-1 Secrets 自動注入 (ADR-035 強制) + # 2026-03-31 ogt: 加入 AI API Keys (修復 mock_fallback 問題) - name: Inject K8s Secrets env: SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }} TG_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TG_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} run: | mkdir -p ~/.ssh echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key @@ -120,9 +123,28 @@ jobs: sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ {"op":"replace","path":"/data/OPENCLAW_TG_BOT_TOKEN","value":"'$(echo -n "${TG_BOT_TOKEN}" | base64)'"}, {"op":"replace","path":"/data/OPENCLAW_TG_CHAT_ID","value":"'$(echo -n "${TG_CHAT_ID}" | base64)'"} - ]' || echo "⚠️ Secrets patch 跳過 (可能尚未建立)" + ]' || echo "⚠️ Telegram Secrets patch 跳過" - echo "✅ Secrets 注入完成" + # 2026-03-31 ogt: 注入 AI API Keys (修復 NVIDIA/Gemini mock_fallback) + # NVIDIA NIM (免費 tier) + if [ -n "${NVIDIA_API_KEY}" ] && [ "${NVIDIA_API_KEY}" != "" ]; then + sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + {"op":"replace","path":"/data/NVIDIA_API_KEY","value":"'$(echo -n "${NVIDIA_API_KEY}" | base64)'"} + ]' && echo "✅ NVIDIA_API_KEY 已注入" || echo "⚠️ NVIDIA_API_KEY patch 失敗" + else + echo "⚠️ NVIDIA_API_KEY 未設定,跳過" + fi + + # Gemini (備援) + if [ -n "${GEMINI_API_KEY}" ] && [ "${GEMINI_API_KEY}" != "" ]; then + sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + {"op":"replace","path":"/data/GEMINI_API_KEY","value":"'$(echo -n "${GEMINI_API_KEY}" | base64)'"} + ]' && echo "✅ GEMINI_API_KEY 已注入" || echo "⚠️ GEMINI_API_KEY patch 失敗" + else + echo "⚠️ GEMINI_API_KEY 未設定,跳過" + fi + + echo "✅ 所有 Secrets 注入完成" SECRETS - name: Deploy to K8s diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 32f7a489..549af10e 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -518,6 +518,24 @@ "minutes": "{count}m ago", "hours": "{count}h ago", "days": "{count}d ago" + }, + "uxAudit": { + "title": "UX Audit", + "noData": "No Session Replay data", + "replaysWithErrors": "Replays with Errors", + "uiErrors": "UI Errors", + "rageClicks": "Rage Clicks", + "deadClicks": "Dead Clicks", + "recentReplays": "Recent Replays", + "recentUIErrors": "Recent UI Errors", + "replayWithErrors": "Replay with {count} errors", + "occurrences": "{count} occurrences", + "viewDashboard": "View Replay Dashboard", + "health": { + "good": "Good", + "moderate": "Moderate", + "poor": "Poor" + } } } } diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 83752ce2..1ea25d21 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -518,6 +518,24 @@ "minutes": "{count} 分鐘前", "hours": "{count} 小時前", "days": "{count} 天前" + }, + "uxAudit": { + "title": "UX 審計", + "noData": "無 Session Replay 數據", + "replaysWithErrors": "有錯誤的 Replay", + "uiErrors": "UI 錯誤", + "rageClicks": "憤怒點擊", + "deadClicks": "死亡點擊", + "recentReplays": "近期 Replay", + "recentUIErrors": "近期 UI 錯誤", + "replayWithErrors": "Replay 包含 {count} 個錯誤", + "occurrences": "{count} 次發生", + "viewDashboard": "查看 Replay Dashboard", + "health": { + "good": "良好", + "moderate": "中等", + "poor": "不佳" + } } } } diff --git a/apps/web/src/app/[locale]/errors/page.tsx b/apps/web/src/app/[locale]/errors/page.tsx index 1119b8fd..f13dffff 100644 --- a/apps/web/src/app/[locale]/errors/page.tsx +++ b/apps/web/src/app/[locale]/errors/page.tsx @@ -24,10 +24,12 @@ import { useTranslations } from 'next-intl' import { AppLayout } from '@/components/layout' import { useErrors } from '@/hooks/useErrors' +import { useUXAudit } from '@/hooks/useUXAudit' import { ErrorOverviewCard, RecentIssuesList, ErrorTrendChart, + UXAuditCard, } from '@/components/errors' import { Bug, RefreshCw } from 'lucide-react' import type { SentryIssue } from '@/lib/api-client' @@ -53,6 +55,13 @@ export default function ErrorsPage({ setPeriod, } = useErrors() + // #126: UX Audit / Session Replay 數據 + const { + data: uxAuditData, + loading: uxAuditLoading, + error: uxAuditError, + } = useUXAudit() + const handleIssueClick = (issue: SentryIssue) => { // Open in new tab if permalink available if (issue.permalink) { @@ -124,6 +133,13 @@ export default function ErrorsPage({ activePeriod={activePeriod} onPeriodChange={setPeriod} /> + + {/* #126: UX Audit / Session Replay */} + {/* Right Column - Issues List */} diff --git a/apps/web/src/components/errors/index.ts b/apps/web/src/components/errors/index.ts index 02ddfd04..3871287f 100644 --- a/apps/web/src/components/errors/index.ts +++ b/apps/web/src/components/errors/index.ts @@ -7,11 +7,14 @@ * - ErrorOverviewCard (#41) * - RecentIssuesList (#42) * - ErrorTrendChart (#43) + * - UXAuditCard (#126) - Session Replay 整合 * * 建立: 2026-03-26 (台北時區) * 建立者: Claude Code (#41-44 Error UI) + * 更新: 2026-03-31 Claude Code (#126 Replay UI) */ export { ErrorOverviewCard } from './error-overview-card' export { RecentIssuesList } from './recent-issues-list' export { ErrorTrendChart } from './error-trend-chart' +export { UXAuditCard } from './ux-audit-card' diff --git a/apps/web/src/components/errors/ux-audit-card.tsx b/apps/web/src/components/errors/ux-audit-card.tsx new file mode 100644 index 00000000..9c0b3a90 --- /dev/null +++ b/apps/web/src/components/errors/ux-audit-card.tsx @@ -0,0 +1,293 @@ +'use client' + +/** + * UXAuditCard - #126 Session Replay / UX Audit 卡片 + * ================================================== + * Phase 19: Sentry Session Replay 前端整合 + * + * 功能: + * - 顯示 UX 健康度評分 + * - 列出有錯誤的 Replay 連結 + * - 顯示憤怒/死亡點擊統計 + * + * Nothing.tech 視覺規範: + * - 純白底色 (bg-white) + * - 極細淺灰邊框 (border border-gray-200) + * - 無圓角或微圓角 (rounded-sm) + * - 嚴禁陰影 (shadow-none) + * + * 建立: 2026-03-31 (台北時區) + * 建立者: Claude Code (#126 Replay UI) + */ + +import { useTranslations } from 'next-intl' +import { cn } from '@/lib/utils' +import { + Video, + MousePointerClick, + AlertTriangle, + ExternalLink, + Activity, + Zap, +} from 'lucide-react' +import type { UXAuditResponse, UXAuditDetail } from '@/lib/api-client' + +// ============================================================================= +// Component Props +// ============================================================================= + +interface UXAuditCardProps { + data: UXAuditResponse | null + loading?: boolean + error?: string | null + className?: string +} + +// ============================================================================= +// Health Score Config +// ============================================================================= + +const HEALTH_CONFIG = { + good: { + color: 'text-green-700', + bgColor: 'bg-green-50', + borderColor: 'border-green-200', + label: 'good', + }, + moderate: { + color: 'text-amber-700', + bgColor: 'bg-amber-50', + borderColor: 'border-amber-200', + label: 'moderate', + }, + poor: { + color: 'text-red-700', + bgColor: 'bg-red-50', + borderColor: 'border-red-200', + label: 'poor', + }, +} as const + +// ============================================================================= +// Replay Item Component +// ============================================================================= + +function ReplayItem({ detail }: { detail: UXAuditDetail }) { + const t = useTranslations('errors.uxAudit') + + if (detail.type === 'replay_with_errors') { + return ( + +
+
+
+
+ {t('replayWithErrors', { count: detail.error_count || 0 })} +
+ {detail.urls && detail.urls.length > 0 && ( +
+ {detail.urls[0]} +
+ )} +
+ +
+ ) + } + + // UI Error type + return ( + +
+ +
+
+
+ {detail.title || 'UI Error'} +
+
+ {t('occurrences', { count: detail.count || 0 })} +
+
+ +
+ ) +} + +// ============================================================================= +// Main Component +// ============================================================================= + +export function UXAuditCard({ + data, + loading = false, + error = null, + className, +}: UXAuditCardProps) { + const t = useTranslations('errors.uxAudit') + + // Loading state + if (loading) { + return ( +
+
+
+
+
+
+
+
+
+
+ ) + } + + // Error state + if (error) { + return ( +
+
+ + {error} +
+
+ ) + } + + // No data state + if (!data) { + return ( +
+
+
+
+ ) + } + + const healthConfig = HEALTH_CONFIG[data.health_score] || HEALTH_CONFIG.moderate + const replayDetails = data.details.filter((d) => d.type === 'replay_with_errors') + const errorDetails = data.details.filter((d) => d.type === 'ui_error') + + return ( +
+ {/* Header */} +
+
+
+ + {t(`health.${healthConfig.label}`)} + +
+ + {/* Stats Grid */} +
+ {/* Replays with Errors */} +
+
+
+
+ {data.replays_with_errors} +
+
+ + {/* UI Errors */} +
+
+ + {t('uiErrors')} +
+
+ {data.ui_errors} +
+
+ + {/* Rage Clicks */} +
+
+ + {t('rageClicks')} +
+
+ {data.rage_clicks} +
+
+ + {/* Dead Clicks */} +
+
+ + {t('deadClicks')} +
+
+ {data.dead_clicks} +
+
+
+ + {/* Replay List */} + {replayDetails.length > 0 && ( +
+
+ {t('recentReplays')} +
+
+ {replayDetails.slice(0, 5).map((detail, idx) => ( + + ))} +
+
+ )} + + {/* UI Errors List */} + {errorDetails.length > 0 && ( +
+
+ {t('recentUIErrors')} +
+
+ {errorDetails.slice(0, 3).map((detail, idx) => ( + + ))} +
+
+ )} + + {/* Footer: Dashboard Link */} + +
+ ) +} diff --git a/apps/web/src/hooks/useUXAudit.ts b/apps/web/src/hooks/useUXAudit.ts new file mode 100644 index 00000000..e082916a --- /dev/null +++ b/apps/web/src/hooks/useUXAudit.ts @@ -0,0 +1,81 @@ +/** + * useUXAudit - Sentry Session Replay / UX Audit Hook + * =================================================== + * Phase 19: #126 Frontend Replay UI Integration + * + * 提供 UX 審計數據的 React Hook: + * - Session Replay 統計 + * - 憤怒點擊 / 死亡點擊 + * - 有錯誤的 Replay 連結 + * + * 建立: 2026-03-31 (台北時區) + * 建立者: Claude Code (#126 Replay UI) + */ + +import { useState, useEffect, useCallback } from 'react' +import { apiClient, type UXAuditResponse } from '@/lib/api-client' + +// ============================================================================= +// Types +// ============================================================================= + +interface UseUXAuditState { + data: UXAuditResponse | null + loading: boolean + error: string | null +} + +interface UseUXAuditReturn extends UseUXAuditState { + refetch: () => Promise +} + +// ============================================================================= +// Hook +// ============================================================================= + +export function useUXAudit(): UseUXAuditReturn { + const [state, setState] = useState({ + data: null, + loading: true, + error: null, + }) + + const fetchData = useCallback(async () => { + setState((prev) => ({ ...prev, loading: true, error: null })) + + try { + const result = await apiClient.getUXAudit() + setState({ + data: result, + loading: false, + error: null, + }) + } catch (err) { + setState((prev) => ({ + ...prev, + loading: false, + error: err instanceof Error ? err.message : 'Failed to fetch UX audit data', + })) + } + }, []) + + const refetch = useCallback(() => { + return fetchData() + }, [fetchData]) + + // Initial fetch + useEffect(() => { + fetchData() + }, [fetchData]) + + // Auto-refresh every 5 minutes (Replay data changes less frequently) + useEffect(() => { + const interval = setInterval(fetchData, 300000) + return () => clearInterval(interval) + }, [fetchData]) + + return { + ...state, + refetch, + } +} diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 4c388a2b..acffd09e 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -216,6 +216,16 @@ export const apiClient = { }) return handleResponse(res) }, + + // ========================================================================= + // Phase 19: UX Audit / Session Replay (#126) + // 2026-03-31 Claude Code - Frontend Replay UI Integration + // ========================================================================= + + async getUXAudit() { + const res = await fetch(`${API_BASE_URL}/errors/ux-audit`) + return handleResponse(res) + }, } // ========================================================================= @@ -383,3 +393,29 @@ export interface ErrorAnalysisResponse { sentry_url: string message?: string } + +// ========================================================================= +// Phase 19: UX Audit / Session Replay Types (#126) +// 2026-03-31 Claude Code - Frontend Replay UI Integration +// ========================================================================= + +export interface UXAuditDetail { + type: 'replay_with_errors' | 'ui_error' + replay_id?: string + issue_id?: string + url: string + error_count?: number + title?: string + count?: number + urls?: string[] +} + +export interface UXAuditResponse { + replays_with_errors: number + rage_clicks: number + dead_clicks: number + ui_errors: number + health_score: 'good' | 'moderate' | 'poor' + details: UXAuditDetail[] + replay_dashboard_url: string +} diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 7b7f975f..493b20f5 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -5,20 +5,25 @@ --- -## 📍 當前狀態 (2026-03-31 12:45 台北) +## 📍 當前狀態 (2026-03-31 15:30 台北) | 項目 | 狀態 | |------|------| +| **#126 Frontend Replay UI** | ✅ **完成** (UXAuditCard + useUXAudit hook) | | **K0 基礎穩定化** | ✅ **低風險完成** (K0.1/3/4/6/7) | | **Phase 22.0 CI pytest** | ✅ **已完成** (CD Pipeline 加入測試步驟) | | **Phase 22.4 命名清理** | ✅ **已完成** (ClawBot 舊檔案移除) | | **P0-1 CD Secrets 注入** | ✅ **已完成** (ADR-035 強制) | | **P0-2 NVIDIA 模型修正** | ✅ **已完成** (nemotron-mini-4b) | +| **P0-3 OpenClaw 架構審查** | ✅ **已完成** (釐清 NVIDIA 400 導致 Gemini Fallback 備援及 Rule Match 導致 0% 信心度,已修補 Prompt Context 溢出問題) | | **Phase 18 失敗自動修復** | ✅ **OUTSTANDING** (95/100 + P0 修復 `138a56a`) | | **Phase 21 定期報告** | ✅ **全部完成!** | | **Phase 21.1 Daily E2E** | ✅ **已完成** (每日 00:00 台北) | | **Phase 21.2 K3s Report** | ✅ **已完成** (每日 09:00 台北) | | **Phase 21.3 Weekly Report** | ✅ **已完成** (每週五 18:00 台北) | +| **Telegram 雙向對話** | ✅ **vfix13-15** (ChatManager + 路由修復) | +| **Nemo-4B 仲裁穩定化** | ✅ **vfix16** (精簡 Prompt + 魯棒解析) | +| **Telegram 會話主權** | ✅ **Webhook Kicker** (終止 188 競爭) | | **#15 SSE + 樂觀更新** | ✅ **完成** (`8c8664c`) | | **#16 DOM Bypass** | ✅ **完成** (`0b87018`) | | **#17 i18n Hydration** | ✅ **完成** (`f25e94e`) | @@ -68,6 +73,41 @@ | **Wave 2 Worker HPA** | ✅ **已部署** (min:1 max:3, CPU 70%) | | **Wave C-D 監控** | ✅ **全部完成** (generate + discover + coverage_report) | +## 🛰️ Telegram 雙向對話與 AI 仲裁極限修復 (2026-03-31 16:00 台北) + +**完成內容**: +- **vfix13**: 實作 `ChatManager` 與 `TelegramGateway` 監聽文字訊息 +- **vfix14**: 實作 **侵略性 Polling (2s)** 搶佔 .188 實例會話 +- **vfix15**: 修復 `send_notification` 定向路由 (chat_id) 與 LLM 結果解包錯誤 +- **vfix16**: 實作 `NEMOTRON_SYSTEM_PROMPT` 與 `OpenClaw` 魯棒解析引擎 (防 Pydantic 崩潰) +- **Webhook Kicker**: 成功清除 188 競爭會話,K3s Pod 獲取獨佔主導權 + +**效益**: +- 統帥現在能直接在 Telegram 與 Nemo-4B 進行對話。 +- OpenClaw 告警仲裁不再因 JSON 欄位缺失而退化至 0% 信心度。 +- 徹底解決了長期困擾的「雙腦衝突」(Split Brain) Polling 問題。 + +--- + +## 🎬 #126 Frontend Replay UI 整合 (2026-03-31 15:30 台北) + +**完成內容**: +- `apps/web/src/lib/api-client.ts` - 新增 `getUXAudit()` 方法 + `UXAuditResponse` 類型 +- `apps/web/src/hooks/useUXAudit.ts` - 新建 Session Replay 數據 Hook +- `apps/web/src/components/errors/ux-audit-card.tsx` - 新建 UX Audit 卡片組件 +- `apps/web/src/app/[locale]/errors/page.tsx` - 整合到錯誤追蹤頁面 +- `apps/web/messages/zh-TW.json` + `en.json` - 新增 `uxAudit` i18n 翻譯 + +**功能**: +- 顯示 UX 健康度評分 (good/moderate/poor) +- 顯示有錯誤的 Replay 連結 (點擊跳轉 Sentry Replay) +- 統計憤怒點擊 / 死亡點擊 / UI 錯誤 +- 每 5 分鐘自動刷新 + +**後端 API**: `/api/v1/errors/ux-audit` (Phase 19 已實作) + +--- + ## 🔧 Phase 18 失敗自動修復閉環 (2026-03-31 12:00 台北) **統帥批准**: 2026-03-31