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,