/** * AWOOOI API Client * ADR-005: 所有請求經過 BFF * * 專案鐵律: 禁止任何 Fallback IP,環境變數缺失即噴錯 */ import { CURRENT_USER } from '@/lib/constants/user' // 絕對純化: 環境變數缺失時直接拋出致命錯誤,嚴禁任何 Fallback const getApiBaseUrl = (): string => { const url = process.env.NEXT_PUBLIC_API_URL if (!url) { const fatalMsg = '[AWOOOI FATAL] Missing NEXT_PUBLIC_API_URL configuration.' console.error(fatalMsg) if (typeof window !== 'undefined') { console.error('%c' + fatalMsg, 'color: #ef4444; font-weight: bold; font-size: 16px;') } throw new Error(fatalMsg) } return url.endsWith('/api/v1') ? url : `${url}/api/v1` } const API_BASE_URL = getApiBaseUrl() export class ApiError extends Error { constructor( public status: number, public code: string, message: string ) { super(message) this.name = 'ApiError' } } async function handleResponse(response: Response): Promise { if (!response.ok) { const error = await response.json().catch(() => ({})) throw new ApiError( response.status, error.code || 'UNKNOWN_ERROR', error.message || response.statusText ) } return response.json() } export const apiClient = { // Health async getHealth() { const res = await fetch(`${API_BASE_URL}/health`) return handleResponse<{ status: 'healthy' | 'degraded' | 'unhealthy' version: string timestamp: string components: Record ollama_route_order?: string[] }>(res) }, // Agent async getAgentStatus() { const res = await fetch(`${API_BASE_URL}/agent/status`) return handleResponse<{ status: 'idle' | 'thinking' | 'executing' | 'waiting_approval' active_conversations: number current_task: string | null last_activity: string | null }>(res) }, async chat(message: string, conversationId?: string) { const res = await fetch(`${API_BASE_URL}/agent/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message, conversation_id: conversationId }), }) return handleResponse<{ message: string conversation_id: string requires_approval: boolean approval_id?: string }>(res) }, // Plugins async listPlugins(category?: string) { const params = category ? `?category=${category}` : '' const res = await fetch(`${API_BASE_URL}/plugins${params}`) return handleResponse>(res) }, // Approvals async listApprovals(status?: string) { const params = status ? `?status=${status}` : '' const res = await fetch(`${API_BASE_URL}/approvals${params}`) return handleResponse<{ items: Array<{ id: string type: string status: string action: { plugin_id: string operation: string risk_level: string } requested_at: string }> }>(res) }, async signApproval(approvalId: string, signer: string = CURRENT_USER.id, comment?: string, csrfToken?: string | null) { // Phase 22 P0: 加入 CSRF token + credentials (2026-03-31 Claude Code) const headers: Record = { 'Content-Type': 'application/json' } if (csrfToken) headers['X-CSRF-Token'] = csrfToken const res = await fetch(`${API_BASE_URL}/approvals/${approvalId}/sign`, { method: 'POST', headers, credentials: 'include', body: JSON.stringify({ signer_id: signer, signer_name: signer, comment: comment, }), }) // 🔧 Fix: 回傳型別與後端實際結構對齊 return handleResponse<{ success: boolean message: string approval: ApprovalResponse execution_triggered: boolean // 向下相容舊欄位 (deprecated) approval_id?: string status?: string current_signatures?: number required_signatures?: number }>(res) }, async rejectApproval(approvalId: string, reason?: string, csrfToken?: string | null) { // Phase 22 P0: 加入 CSRF token + credentials (2026-03-31 Claude Code) const headers: Record = { 'Content-Type': 'application/json' } if (csrfToken) headers['X-CSRF-Token'] = csrfToken const res = await fetch(`${API_BASE_URL}/approvals/${approvalId}/reject`, { method: 'POST', headers, credentials: 'include', body: JSON.stringify({ rejector_id: CURRENT_USER.id, rejector_name: CURRENT_USER.name, reason: reason || 'Rejected via WarRoom', }), }) return handleResponse<{ id: string; status: string }>(res) }, // ========================================================================= // Phase 7: Incidents API (真實血脈) // ========================================================================= async listIncidents() { const res = await fetch(`${API_BASE_URL}/incidents`) return handleResponse(res) }, async getIncident(incidentId: string) { const res = await fetch(`${API_BASE_URL}/incidents/${incidentId}`) return handleResponse(res) }, async getIncidentTimeline(incidentId: string) { const res = await fetch(`${API_BASE_URL}/incidents/${incidentId}/timeline`) return handleResponse(res) }, async generateProposal(incidentId: string) { const res = await fetch(`${API_BASE_URL}/incidents/${incidentId}/proposal`, { method: 'POST', }) return handleResponse(res) }, // ========================================================================= // Phase 7: Pending Approvals API (真實血脈) // ========================================================================= async getPendingApprovals() { const res = await fetch(`${API_BASE_URL}/approvals/pending`) return handleResponse(res) }, // ========================================================================= // Phase 10: Sentry Errors API (#40 BFF) // ========================================================================= async getErrorStats() { const res = await fetch(`${API_BASE_URL}/errors/stats`) return handleResponse(res) }, async listErrors(params?: { status?: string; level?: string; limit?: number }) { const searchParams = new URLSearchParams() if (params?.status) searchParams.set('status', params.status) if (params?.level) searchParams.set('level', params.level) if (params?.limit) searchParams.set('limit', params.limit.toString()) const query = searchParams.toString() ? `?${searchParams.toString()}` : '' const res = await fetch(`${API_BASE_URL}/errors/issues${query}`) return handleResponse(res) }, async getErrorDetail(issueId: string) { const res = await fetch(`${API_BASE_URL}/errors/issues/${issueId}`) return handleResponse(res) }, async getErrorTrends(period: '24h' | '7d' | '30d' = '24h') { const res = await fetch(`${API_BASE_URL}/errors/trends?period=${period}`) return handleResponse(res) }, async analyzeError(issueId: string) { const res = await fetch(`${API_BASE_URL}/errors/issues/${issueId}/analyze`, { method: 'POST', }) 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) }, async getAgentMarketGovernanceSnapshot() { const res = await fetch(`${API_BASE_URL}/agents/market-governance-snapshot`) return handleResponse(res) }, async getAiAgentAutomationInventorySnapshot() { const res = await fetch(`${API_BASE_URL}/agents/automation-inventory-snapshot`) return handleResponse(res) }, async getAiAgentAutomationBacklogSnapshot() { const res = await fetch(`${API_BASE_URL}/agents/automation-backlog-snapshot`) return handleResponse(res) }, async getBackupDrTargetInventory() { const res = await fetch(`${API_BASE_URL}/agents/backup-dr-target-inventory`) return handleResponse(res) }, async getBackupDrReadinessMatrix() { const res = await fetch(`${API_BASE_URL}/agents/backup-dr-readiness-matrix`) return handleResponse(res) }, async getBackupNotificationPolicy() { const res = await fetch(`${API_BASE_URL}/agents/backup-notification-policy`) return handleResponse(res) }, } // ========================================================================= // Type Definitions (Phase 7) // ========================================================================= /** * Phase 6.5: 決策令牌資訊 * 確保 UI 永遠有決策可操作 */ export interface DecisionInfo { token: string state: 'init' | 'analyzing' | 'ready' | 'executing' | 'completed' | 'error' proposal_data: { action: string description: string reasoning: string risk_level: 'low' | 'medium' | 'critical' kubectl_command: string source: string confidence: number } | null proposal_id: string | null } export interface IncidentResponse { incident_id: string status: 'investigating' | 'mitigating' | 'resolved' | 'closed' severity: 'P0' | 'P1' | 'P2' | 'P3' signal_count: number affected_services: string[] proposal_count: number created_at: string updated_at: string /** Phase 6.5: 決策令牌 (確保 UI 永不鎖死) */ decision: DecisionInfo | null } export interface IncidentListResponse { count: number incidents: IncidentResponse[] } export interface IncidentTimelineEvent { stage: string status: string title: string description: string | null actor: string | null timestamp: string | null source_table: string | null data: Record } export interface IncidentTimelineStage extends IncidentTimelineEvent { label: string events: IncidentTimelineEvent[] } export interface IncidentTimelineResponse { incident_id: string title: string status: string severity: string started_at: string | null updated_at: string | null resolved_at: string | null affected_services: string[] approval_ids: string[] timeline: IncidentTimelineStage[] events: IncidentTimelineEvent[] ascii_timeline: string } export interface BlastRadius { affected_pods: number estimated_downtime: string related_services: string[] data_impact: 'none' | 'read_only' | 'write' | 'destructive' } export interface DryRunCheck { name: string passed: boolean message: string } export interface ApprovalResponse { id: string action: string description: string status: 'pending' | 'approved' | 'rejected' | 'expired' risk_level: 'low' | 'medium' | 'high' | 'critical' blast_radius: BlastRadius dry_run_checks: DryRunCheck[] required_signatures: number current_signatures: number signatures: Array<{ signer: string; signed_at: string }> requested_by: string created_at: string expires_at: string | null } export interface PendingApprovalsResponse { count: number approvals: ApprovalResponse[] } export interface ProposalGenerateResponse { success: boolean message: string incident_id: string proposal: ApprovalResponse | null incident_status: string | null } // ========================================================================= // Phase 10: Sentry Error Types (#40 BFF) // ========================================================================= export interface SentryIssue { id: string short_id: string title: string culprit: string | null level: 'error' | 'warning' | 'info' | 'fatal' status: 'unresolved' | 'resolved' | 'ignored' count: number user_count: number first_seen: string last_seen: string permalink: string | null } export interface ErrorStatsResponse { total_issues: number unresolved_issues: number error_count_24h: number critical_count: number projects: string[] } export interface ErrorListResponse { issues: SentryIssue[] total: number has_more: boolean } export interface ErrorDetailResponse { issue: Record latest_event: Record | null sentry_url: string } export interface ErrorTrendPoint { timestamp: string count: number } export interface ErrorTrendResponse { period: '24h' | '7d' | '30d' data: ErrorTrendPoint[] total_count: number change_percent: number } export interface FixRecommendation { summary: string steps: string[] code_suggestion: string | null } export interface PreventionMeasure { type: string description: string } export interface ErrorAnalysis { root_cause: string category: string severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' impact_assessment: string fix_recommendation: FixRecommendation prevention: PreventionMeasure[] related_files: string[] confidence: number reasoning: string } export interface ErrorAnalysisResponse { status: 'completed' | 'failed' issue_id: string provider: string analysis?: ErrorAnalysis analyzed_at?: string 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 } // ========================================================================= // Agent Market Governance Snapshot // ========================================================================= export interface AgentMarketGovernanceSnapshot { schema_version: 'agent_market_governance_snapshot_v1' generated_at: string current_decision: string policy: Record evaluation_cadence: { workflow: string schedule: string timezone: 'Asia/Taipei' next_scheduled_run_at: string trigger_modes: string[] primary_source_policy: string operator_review_gate: string } market_watch_health: { status: 'healthy' | 'blocked' freshness_sla_hours: 168 stale_grace_hours: 6 stale_after: string source_failures_block_priority_upgrade: boolean blocked_from_integration: number operator_blockers: string[] } summary: { candidate_count: number source_count: number source_failures: number changed_candidates: number integration_queue_count: number blocked_from_integration: number watch_only_candidates_reviewed: number eligible_for_market_scorecard_prescreen: number recommended_watch_additions_remaining: number priority_upgrades_approved: number market_scorecard_updates_approved: number replay_candidates_approved: number sdk_installations_approved: number paid_api_calls_approved: number production_changes_approved: number shadow_or_canary_approved: number replacement_decisions_approved: number } candidate_groups: { production_baseline: string[] replay_or_integration_blocked: string[] watch_only_candidates: string[] watch_only_scorecard_prescreen_ready: string[] } candidate_statuses: Array<{ candidate_id: string display_name: string role: string evaluation_priority: string gate_status: | 'production_baseline' | 'integration_blocked' | 'integration_reviewed' | 'watch_only_prescreen_ready' | 'watch_only_blocked' | 'watch_only_monitoring' | 'registered_no_review' current_gate: string required_next_gate: string integration_decision: string score: number | null evidence: { latest_replay_summary: string | null latest_smoke_gate: string | null latest_smoke_matrix: string | null latest_smoke_model: string | null } approvals: { replay: false sdk_install: false paid_api: false shadow_or_canary: false production_routing: false } operator_blockers: string[] }> operator_decision_queue: Array<{ candidate_id: string display_name: string priority: number queue_status: | 'baseline_protected' | 'blocked_needs_evidence' | 'operator_review_required' | 'operator_priority_review' | 'watch_only_blocked' | 'watch_only_monitoring' | 'registered_no_review' recommended_action: string approval_boundary: { replacement_adr_required: boolean priority_upgrade_required: boolean market_scorecard_update_required: boolean replay_approval_required: boolean sdk_install_approval_required: boolean paid_api_approval_required: boolean shadow_or_canary_approval_required: boolean production_routing_approval_required: boolean } risk_notes: string[] evidence_refs: string[] }> next_allowed_actions: string[] forbidden_actions_without_new_approval: string[] } // ========================================================================= // AI Agent Automation Inventory Snapshot // ========================================================================= export interface AiAgentAutomationInventorySnapshot { schema_version: 'ai_agent_automation_inventory_snapshot_v1' generated_at: string program_status: { overall_completion_percent: number current_priority: 'P0' | 'P1' | 'P2' | 'P3' current_task_id: string next_task_id: string read_only_mode: true } status_taxonomy: { task_statuses: string[] gate_statuses: string[] priorities: Array<'P0' | 'P1' | 'P2' | 'P3'> } agent_roles: Array<{ agent_id: string display_name: string primary_role: string allowed_actions: string[] blocked_actions: string[] }> asset_domains: Array<{ domain_id: string display_name: string description: string }> assets: Array<{ asset_id: string domain_id: string display_name: string asset_type: string status: string gate_status: string owner_agent: string risk_level: 'low' | 'medium' | 'high' | 'critical' evidence_refs: string[] next_action: string }> workstreams: Array<{ workstream_id: string display_name: string completion_percent: number status: string next_task_id: string }> tasks: Array<{ task_id: string priority: 'P0' | 'P1' | 'P2' | 'P3' status: string completion_percent: number owner_agent: string title: string output: string gate_status: string next_action: string }> evidence: Array<{ evidence_id: string kind: 'schema' | 'test' | 'browser' | 'api' | 'build' | 'doc' | 'runtime' ref: string result: string }> approval_boundaries: Record< | 'sdk_installation_allowed' | 'paid_api_call_allowed' | 'shadow_or_canary_allowed' | 'production_routing_allowed' | 'destructive_operation_allowed', false > } export interface AiAgentAutomationBacklogSnapshot { schema_version: 'ai_agent_automation_backlog_v1' generated_at: string source_inventory_snapshot_ref: string program_status: { overall_completion_percent: number current_priority: 'P0' | 'P1' | 'P2' | 'P3' current_task_id: string next_task_id: string read_only_mode: true } rollups: { total_items: number by_priority: Record by_status: Record by_gate_status: Record by_owner_agent: Record } backlog_items: Array<{ item_id: string priority: 'P0' | 'P1' | 'P2' | 'P3' status: string workstream_id: string source_asset_id: string source_signal_kind: string title: string owner_agent: string recommended_action: string action_class: string gate_status: string risk_level: 'low' | 'medium' | 'high' | 'critical' evidence_refs: string[] acceptance_criteria: string[] next_review: string }> approval_boundaries: Record< | 'sdk_installation_allowed' | 'paid_api_call_allowed' | 'shadow_or_canary_allowed' | 'production_routing_allowed' | 'destructive_operation_allowed', false > } export interface BackupDrTargetInventorySnapshot { schema_version: 'backup_dr_target_inventory_v1' generated_at: string source_refs: string[] program_status: { overall_completion_percent: number current_priority: 'P0' | 'P1' | 'P2' | 'P3' current_task_id: string next_task_id: string read_only_mode: true } rollups: { total_targets: number by_status: Record by_target_type: Record by_gate_status: Record blocked_target_ids: string[] } backup_targets: Array<{ target_id: string display_name: string target_type: string status: string risk_level: 'low' | 'medium' | 'high' | 'critical' owner_host: string primary_script: string schedule: string rpo: string storage_class: string storage_ref: string offsite_policy: string automation_gate_status: string restore_gate_status: string secret_policy: string evidence_refs: string[] next_action: string }> approval_boundaries: Record operation_boundaries: Record } export interface BackupDrReadinessMatrixSnapshot { schema_version: 'backup_dr_readiness_matrix_v1' generated_at: string source_target_inventory_ref: string source_refs: string[] program_status: { overall_completion_percent: number current_priority: 'P0' | 'P1' | 'P2' | 'P3' current_task_id: string next_task_id: string read_only_mode: true } rollups: { total_rows: number by_overall_readiness: Record by_restore_drill_status: Record by_offsite_status: Record blocked_row_ids: string[] action_required_row_ids: string[] } readiness_rows: Array<{ target_id: string display_name: string overall_readiness: string freshness_status: string integrity_status: string restore_drill_status: string offsite_status: string notification_policy: string gate_status: string evidence_level: string evidence_refs: string[] blocker_summary: string next_action: string }> approval_boundaries: Record operation_boundaries: Record } export interface BackupNotificationPolicySnapshot { schema_version: 'backup_notification_policy_v1' generated_at: string source_readiness_matrix_ref: string source_refs: string[] program_status: { overall_completion_percent: number current_priority: 'P0' | 'P1' | 'P2' | 'P3' current_task_id: string next_task_id: string read_only_mode: true } rollups: { total_rules: number by_decision: Record immediate_escalation_rule_ids: string[] suppressed_success_rule_ids: string[] } notification_channels: Array<{ channel_id: string purpose: string immediate_allowed: boolean success_immediate_allowed: boolean requires_operator_action: boolean }> policy_rules: Array<{ rule_id: string event_kind: string backup_state: string severity: string decision: string channels: string[] owner_agent: string requires_incident: boolean requires_approval_record: boolean message_contract: string evidence_refs: string[] }> daily_summary_contract: Record approval_boundaries: Record operation_boundaries: Record }