feat(web): #126 Frontend Replay UI 整合
All checks were successful
E2E Health Check / e2e-health (push) Successful in 18s
All checks were successful
E2E Health Check / e2e-health (push) Successful in 18s
- 新增 useUXAudit hook (5 分鐘自動刷新) - 新增 UXAuditCard 組件 (健康度 + Replay 連結) - 整合到錯誤追蹤頁面 - i18n: zh-TW + en 翻譯 功能: - UX 健康度評分 (good/moderate/poor) - 有錯誤的 Replay 連結 - 憤怒點擊/死亡點擊統計 - Replay Dashboard 快捷連結 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "不佳"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */}
|
||||
<UXAuditCard
|
||||
data={uxAuditData}
|
||||
loading={uxAuditLoading}
|
||||
error={uxAuditError}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Issues List */}
|
||||
|
||||
@@ -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'
|
||||
|
||||
293
apps/web/src/components/errors/ux-audit-card.tsx
Normal file
293
apps/web/src/components/errors/ux-audit-card.tsx
Normal file
@@ -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 (
|
||||
<a
|
||||
href={detail.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 p-2 hover:bg-gray-50 rounded-sm transition-colors group"
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<Video className="h-4 w-4 text-purple-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-gray-900 truncate">
|
||||
{t('replayWithErrors', { count: detail.error_count || 0 })}
|
||||
</div>
|
||||
{detail.urls && detail.urls.length > 0 && (
|
||||
<div className="text-xs text-gray-500 truncate">
|
||||
{detail.urls[0]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ExternalLink className="h-3.5 w-3.5 text-gray-400 group-hover:text-purple-600 transition-colors" />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
// UI Error type
|
||||
return (
|
||||
<a
|
||||
href={detail.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 p-2 hover:bg-gray-50 rounded-sm transition-colors group"
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<AlertTriangle className="h-4 w-4 text-orange-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-gray-900 truncate">
|
||||
{detail.title || 'UI Error'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{t('occurrences', { count: detail.count || 0 })}
|
||||
</div>
|
||||
</div>
|
||||
<ExternalLink className="h-3.5 w-3.5 text-gray-400 group-hover:text-orange-600 transition-colors" />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main Component
|
||||
// =============================================================================
|
||||
|
||||
export function UXAuditCard({
|
||||
data,
|
||||
loading = false,
|
||||
error = null,
|
||||
className,
|
||||
}: UXAuditCardProps) {
|
||||
const t = useTranslations('errors.uxAudit')
|
||||
|
||||
// Loading state
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={cn('bg-white border border-gray-200 rounded-sm', className)}>
|
||||
<div className="p-3 border-b border-gray-100">
|
||||
<div className="h-4 bg-gray-200 rounded w-1/3 animate-pulse" />
|
||||
</div>
|
||||
<div className="p-4 space-y-3 animate-pulse">
|
||||
<div className="h-8 bg-gray-100 rounded w-1/4" />
|
||||
<div className="h-4 bg-gray-100 rounded w-full" />
|
||||
<div className="h-4 bg-gray-100 rounded w-3/4" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (error) {
|
||||
return (
|
||||
<div className={cn('bg-white border border-red-200 rounded-sm p-4', className)}>
|
||||
<div className="flex items-center gap-2 text-red-600">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<span className="text-sm">{error}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// No data state
|
||||
if (!data) {
|
||||
return (
|
||||
<div className={cn('bg-white border border-gray-200 rounded-sm p-6', className)}>
|
||||
<div className="text-center text-gray-500">
|
||||
<Video className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">{t('noData')}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={cn('bg-white border border-gray-200 rounded-sm', className)}>
|
||||
{/* Header */}
|
||||
<div className="px-3 py-2 border-b border-gray-100 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Video className="h-4 w-4 text-purple-600" />
|
||||
<h3 className="text-sm font-medium text-gray-900">{t('title')}</h3>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
'px-2 py-0.5 text-xs font-medium rounded',
|
||||
healthConfig.bgColor,
|
||||
healthConfig.color,
|
||||
)}
|
||||
>
|
||||
{t(`health.${healthConfig.label}`)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 gap-px bg-gray-100 border-b border-gray-100">
|
||||
{/* Replays with Errors */}
|
||||
<div className="bg-white p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Video className="h-4 w-4 text-purple-500" />
|
||||
<span className="text-xs text-gray-500">{t('replaysWithErrors')}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xl font-semibold text-gray-900">
|
||||
{data.replays_with_errors}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* UI Errors */}
|
||||
<div className="bg-white p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-orange-500" />
|
||||
<span className="text-xs text-gray-500">{t('uiErrors')}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xl font-semibold text-gray-900">
|
||||
{data.ui_errors}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rage Clicks */}
|
||||
<div className="bg-white p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="h-4 w-4 text-red-500" />
|
||||
<span className="text-xs text-gray-500">{t('rageClicks')}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xl font-semibold text-gray-900">
|
||||
{data.rage_clicks}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dead Clicks */}
|
||||
<div className="bg-white p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<MousePointerClick className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-xs text-gray-500">{t('deadClicks')}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xl font-semibold text-gray-900">
|
||||
{data.dead_clicks}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Replay List */}
|
||||
{replayDetails.length > 0 && (
|
||||
<div className="border-b border-gray-100">
|
||||
<div className="px-3 py-1.5 bg-gray-50 text-xs font-medium text-gray-600">
|
||||
{t('recentReplays')}
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{replayDetails.slice(0, 5).map((detail, idx) => (
|
||||
<ReplayItem key={detail.replay_id || idx} detail={detail} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* UI Errors List */}
|
||||
{errorDetails.length > 0 && (
|
||||
<div className="border-b border-gray-100">
|
||||
<div className="px-3 py-1.5 bg-gray-50 text-xs font-medium text-gray-600">
|
||||
{t('recentUIErrors')}
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{errorDetails.slice(0, 3).map((detail, idx) => (
|
||||
<ReplayItem key={detail.issue_id || idx} detail={detail} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer: Dashboard Link */}
|
||||
<div className="px-3 py-2">
|
||||
<a
|
||||
href={data.replay_dashboard_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-1.5 text-xs text-purple-600 hover:text-purple-700 transition-colors"
|
||||
>
|
||||
<Activity className="h-3.5 w-3.5" />
|
||||
{t('viewDashboard')}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
81
apps/web/src/hooks/useUXAudit.ts
Normal file
81
apps/web/src/hooks/useUXAudit.ts
Normal file
@@ -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<void>
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook
|
||||
// =============================================================================
|
||||
|
||||
export function useUXAudit(): UseUXAuditReturn {
|
||||
const [state, setState] = useState<UseUXAuditState>({
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -216,6 +216,16 @@ export const apiClient = {
|
||||
})
|
||||
return handleResponse<ErrorAnalysisResponse>(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<UXAuditResponse>(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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user