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 (
+
+ )
+ }
+
+ // 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('title')}
+
+
+ {t(`health.${healthConfig.label}`)}
+
+
+
+ {/* Stats Grid */}
+
+ {/* Replays with Errors */}
+
+
+
+ {t('replaysWithErrors')}
+
+
+ {data.replays_with_errors}
+
+
+
+ {/* UI Errors */}
+
+
+
+ {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