From 28fa8e6af47e6139f80f1fd6c90bd2d191f9a5bb Mon Sep 17 00:00:00 2001 From: OG T Date: Mon, 23 Mar 2026 12:37:56 +0800 Subject: [PATCH] feat(web): Phase 6.5c implement [Y/n] execution wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DualStateIncidentCard: - Add proposalId prop for approval actions - Add onApprovalChange callback for status updates - Implement handleApprove() calling POST /api/v1/approvals/{id}/sign - Implement handleReject() calling POST /api/v1/approvals/{id}/reject - Add ButtonState management (idle/loading/approved/rejected/error) - Loading spinner during API call - Success state: green "已授權" / red "已拒絕" - Error state: orange "錯誤" with auto-recovery API Client: - Fix endpoint mismatch: rename approveApproval to signApproval - Use correct endpoint /sign instead of /approve - Add signer parameter for multi-sig support Co-Authored-By: Claude Opus 4.5 --- .../incident/dual-state-incident-card.tsx | 130 +++++++++++++++++- apps/web/src/lib/api-client.ts | 14 +- 2 files changed, 133 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/incident/dual-state-incident-card.tsx b/apps/web/src/components/incident/dual-state-incident-card.tsx index 55b25c83..df4aff75 100644 --- a/apps/web/src/components/incident/dual-state-incident-card.tsx +++ b/apps/web/src/components/incident/dual-state-incident-card.tsx @@ -14,10 +14,18 @@ * - normal: 淺灰邊框,靜態 * - alert: 紅色邊框,脈衝雷達動畫 * + * Phase 6.5c: [Y/n] 執行神經實裝 + * - Y: 呼叫 signApproval API + * - n: 呼叫 rejectApproval API + * - Loading state + Success/Error feedback + * * 統帥鐵律: 禁止假數據! */ -import React from 'react' +import React, { useState, useCallback } from 'react' +import { apiClient } from '@/lib/api-client' + +type ButtonState = 'idle' | 'loading' | 'approved' | 'rejected' | 'error' export interface DualStateIncidentCardProps { id: string @@ -26,6 +34,10 @@ export interface DualStateIncidentCardProps { tier?: 1 | 2 | 3 message: string timestamp: string + /** Proposal ID for approval actions (required for tier > 1) */ + proposalId?: string + /** Callback when approval status changes */ + onApprovalChange?: (proposalId: string, newStatus: 'approved' | 'rejected') => void } export const DualStateIncidentCard: React.FC = ({ @@ -35,8 +47,116 @@ export const DualStateIncidentCard: React.FC = ({ tier, message, timestamp, + proposalId, + onApprovalChange, }) => { const isAlert = status === 'alert' + const [buttonState, setButtonState] = useState('idle') + const [errorMessage, setErrorMessage] = useState(null) + + /** + * 處理簽核 (Y 按鈕) + * 呼叫 POST /api/v1/approvals/{id}/sign + */ + const handleApprove = useCallback(async () => { + if (!proposalId || buttonState === 'loading') return + + setButtonState('loading') + setErrorMessage(null) + + try { + const result = await apiClient.signApproval(proposalId, 'commander') + + if (result.status === 'approved' || result.status === 'APPROVED') { + setButtonState('approved') + onApprovalChange?.(proposalId, 'approved') + } else { + // Multi-sig: 還需要更多簽核 + setButtonState('idle') + } + } catch (error) { + console.error('[DualStateIncidentCard] Approve failed:', error) + setButtonState('error') + setErrorMessage(error instanceof Error ? error.message : 'Unknown error') + // 3 秒後恢復 + setTimeout(() => setButtonState('idle'), 3000) + } + }, [proposalId, buttonState, onApprovalChange]) + + /** + * 處理拒絕 (n 按鈕) + * 呼叫 POST /api/v1/approvals/{id}/reject + */ + const handleReject = useCallback(async () => { + if (!proposalId || buttonState === 'loading') return + + setButtonState('loading') + setErrorMessage(null) + + try { + await apiClient.rejectApproval(proposalId, 'commander') + setButtonState('rejected') + onApprovalChange?.(proposalId, 'rejected') + } catch (error) { + console.error('[DualStateIncidentCard] Reject failed:', error) + setButtonState('error') + setErrorMessage(error instanceof Error ? error.message : 'Unknown error') + setTimeout(() => setButtonState('idle'), 3000) + } + }, [proposalId, buttonState, onApprovalChange]) + + /** + * 渲染決策按鈕區塊 + */ + const renderActionButton = () => { + switch (buttonState) { + case 'loading': + return ( + + + 執行中... + + ) + case 'approved': + return ( + + [ 已授權 ] + + ) + case 'rejected': + return ( + + [ 已拒絕 ] + + ) + case 'error': + return ( + + [ 錯誤 ] + + ) + default: + return ( +
+ + / + +
+ ) + } + } return (
= ({
{timestamp}
- {/* 大腦決策層 (Proposal UI) */} + {/* 大腦決策層 (Proposal UI) - Phase 6.5c 執行神經實裝 */} {isAlert && tier && (
{tier === 1 ? '>_ AI 執行中 (Tier 1)' : `>_ 等待統帥親核 (Tier ${tier})`} - {tier > 1 && ( - - )} + {tier > 1 && renderActionButton()}
)} diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 2490ca84..f4baa781 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -114,13 +114,19 @@ export const apiClient = { }>(res) }, - async approveApproval(approvalId: string, reason?: string) { - const res = await fetch(`${API_BASE_URL}/approvals/${approvalId}/approve`, { + async signApproval(approvalId: string, signer: string = 'commander', reason?: string) { + const res = await fetch(`${API_BASE_URL}/approvals/${approvalId}/sign`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ reason }), + body: JSON.stringify({ signer, reason }), }) - return handleResponse<{ id: string; status: string }>(res) + return handleResponse<{ + approval_id: string + status: string + current_signatures: number + required_signatures: number + message: string + }>(res) }, async rejectApproval(approvalId: string, reason?: string) {