From 7847e00b1b2afbe0e80a000ff7b5b984f091ed7c Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 26 Mar 2026 18:40:19 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E7=B0=BD=E6=A0=B8=E5=BE=8C?= =?UTF-8?q?=E4=BF=9D=E7=95=99=E5=85=A7=E5=AE=B9=E9=A1=AF=E7=A4=BA=205=20?= =?UTF-8?q?=E7=A7=92=20(2026-03-26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 問題: 簽核後卡片立即消失,用戶無法確認已批准內容 修復: - approval.store.ts: 簽核/拒絕後延遲 5 秒才移除 - live-approval-panel.tsx: 已解決項目顯示狀態橫幅 Co-Authored-By: Claude Opus 4.5 --- .../approval/live-approval-panel.tsx | 26 +++++++-- apps/web/src/stores/approval.store.ts | 54 ++++++++++++++++--- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/approval/live-approval-panel.tsx b/apps/web/src/components/approval/live-approval-panel.tsx index de4f7031..9f723248 100644 --- a/apps/web/src/components/approval/live-approval-panel.tsx +++ b/apps/web/src/components/approval/live-approval-panel.tsx @@ -266,20 +266,39 @@ export function LiveApprovalPanel({ {/* Approval Cards */}
- {approvals.map((approval) => ( + {approvals.map((approval) => { + // 🔴 2026-03-26 修復: 檢查原始後端狀態 (透過 pendingApprovals 取得) + const backendApproval = pendingApprovals.find(a => a.id === approval.id) + const isResolved = backendApproval?.status === 'approved' || backendApproval?.status === 'rejected' + const resolvedStatus = backendApproval?.status + + return (
+ {/* 🔴 已解決狀態橫幅 */} + {isResolved && ( +
+ {resolvedStatus === 'approved' ? '✓ 已批准' : '✗ 已拒絕'} +
+ )} handleSign(approval.id, approval.riskLevel)} onReject={() => handleReject(approval.id)} holdDuration={2000} isLoading={signingStates[approval.id] === 'signing'} + disabled={isResolved} /> {/* Permission Warning Badge (Phase 3) */} @@ -302,7 +321,8 @@ export function LiveApprovalPanel({
)}
- ))} + )})} + {/* Reject Modal */} diff --git a/apps/web/src/stores/approval.store.ts b/apps/web/src/stores/approval.store.ts index d98e7da0..c9cbfed4 100644 --- a/apps/web/src/stores/approval.store.ts +++ b/apps/web/src/stores/approval.store.ts @@ -226,26 +226,36 @@ export const useApprovalStore = create()( console.log('[Approval] Signed:', id, result.message) // Update local state + // 🔴 2026-03-26 修復: 簽核後保留內容顯示,不立即移除 set((state) => { const updatedApprovals = state.pendingApprovals.map((a) => a.id === id ? result.approval : a ) - // If approved, mark for removal animation + // If approved, mark for removal animation (但不立即移除) const newRecentlyApproved = new Set(state.recentlyApproved) if (result.approval.status === 'approved') { newRecentlyApproved.add(id) } return { - pendingApprovals: result.approval.status === 'approved' - ? updatedApprovals.filter((a) => a.id !== id) - : updatedApprovals, + // 🔴 修復: 保留在列表中,讓 UI 顯示已簽核狀態 + pendingApprovals: updatedApprovals, signingId: null, recentlyApproved: newRecentlyApproved, } }) + // 🔴 延遲 5 秒後才從列表移除 (讓用戶看到完整簽核結果) + if (result.approval.status === 'approved') { + setTimeout(() => { + set((state) => ({ + pendingApprovals: state.pendingApprovals.filter((a) => a.id !== id), + })) + console.log('[Approval] Removed approved item after delay:', id) + }, 5000) + } + // 🔧 Race Condition 修復: 延遲 1 秒後恢復 Polling,讓後端有時間更新 if (wasPolling) { setTimeout(() => { @@ -305,17 +315,31 @@ export const useApprovalStore = create()( console.log('[Approval] Rejected:', id) // Update local state + // 🔴 2026-03-26 修復: 拒絕後保留內容顯示,不立即移除 set((state) => { const newRecentlyRejected = new Set(state.recentlyRejected) newRecentlyRejected.add(id) + // 更新狀態但保留在列表中 + const updatedApprovals = state.pendingApprovals.map((a) => + a.id === id ? { ...a, status: 'rejected' as const } : a + ) + return { - pendingApprovals: state.pendingApprovals.filter((a) => a.id !== id), + pendingApprovals: updatedApprovals, rejectingId: null, recentlyRejected: newRecentlyRejected, } }) + // 🔴 延遲 5 秒後才從列表移除 + setTimeout(() => { + set((state) => ({ + pendingApprovals: state.pendingApprovals.filter((a) => a.id !== id), + })) + console.log('[Approval] Removed rejected item after delay:', id) + }, 5000) + // 🔧 Race Condition 修復: 延遲 1 秒後恢復 Polling if (wasPolling) { setTimeout(() => { @@ -541,7 +565,25 @@ export const useApprovalSSEConnected = () => // Type Converters (Backend → Frontend) // ============================================================================= +// P1 修復: 使用 mapping object 取代 toUpperCase() as Type (2026-03-26) +const DATA_IMPACT_MAP: Record = { + 'none': 'NONE', + 'read_only': 'READ_ONLY', + 'read-only': 'READ_ONLY', + 'readonly': 'READ_ONLY', + 'write': 'WRITE', + 'destructive': 'DESTRUCTIVE', + // 大寫版本 (防禦性) + 'NONE': 'NONE', + 'READ_ONLY': 'READ_ONLY', + 'WRITE': 'WRITE', + 'DESTRUCTIVE': 'DESTRUCTIVE', +} + export function toFrontendApproval(backend: ApprovalRequest): import('@/components/approval').ApprovalRequest { + const rawDataImpact = backend.blast_radius.data_impact || 'none' + const dataImpact = DATA_IMPACT_MAP[rawDataImpact] || DATA_IMPACT_MAP[rawDataImpact.toLowerCase()] || 'NONE' + return { id: backend.id, action: backend.action, @@ -552,7 +594,7 @@ export function toFrontendApproval(backend: ApprovalRequest): import('@/componen affectedPods: backend.blast_radius.affected_pods, estimatedDowntime: backend.blast_radius.estimated_downtime, relatedServices: backend.blast_radius.related_services, - dataImpact: backend.blast_radius.data_impact.toUpperCase() as 'NONE' | 'READ_ONLY' | 'WRITE' | 'DESTRUCTIVE', + dataImpact, }, dryRunChecks: backend.dry_run_checks.map((c) => ({ name: c.name,