/** * 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 getRuntimeSurfaceInventory() { const res = await fetch(`${API_BASE_URL}/agents/runtime-surface-inventory`) return handleResponse(res) }, async getGiteaWorkflowRunnerHealth() { const res = await fetch(`${API_BASE_URL}/agents/gitea-workflow-runner-health`) return handleResponse(res) }, async getObservabilityContractMatrix() { const res = await fetch(`${API_BASE_URL}/agents/observability-contract-matrix`) 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) }, async getOffsiteEscrowReadinessStatus() { const res = await fetch(`${API_BASE_URL}/agents/offsite-escrow-readiness-status`) 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 approval_boundary: { mode: string display_summary: string allowed_actions: string[] blocked_actions: string[] requires_operator_approval_for: string[] } next_action: string }> task_approval_boundary_rollup: { total_tasks: number by_mode: Record tasks_requiring_explicit_approval: string[] tasks_with_blocked_operations: 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 } progress_summary: { overall_percent: number done_items: number planned_items: number total_items: number formula: string by_priority: Array<{ priority: 'P0' | 'P1' | 'P2' | 'P3' completion_percent: number done_items: number total_items: number }> by_workstream: Array<{ workstream_id: string display_name: string completion_percent: number done_items: number total_items: number next_task_id: string }> } 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[] approval_boundary: { mode: string display_summary: string allowed_actions: string[] blocked_actions: string[] requires_operator_approval_for: string[] } next_review: string }> item_approval_boundary_rollup: { total_items: number by_mode: Record items_requiring_explicit_approval: string[] items_with_blocked_operations: string[] } approval_boundaries: Record< | 'sdk_installation_allowed' | 'paid_api_call_allowed' | 'shadow_or_canary_allowed' | 'production_routing_allowed' | 'destructive_operation_allowed', false > } export interface RuntimeSurfaceInventorySnapshot { schema_version: 'runtime_surface_inventory_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 } source_refs: string[] rollups: { total_surfaces: number by_kind: Record by_status: Record by_evidence_level: Record action_required_surface_ids: string[] secret_surface_ids: string[] live_check_missing_surface_ids: string[] total_source_components: number source_components_with_runtime_binding: number } runtime_surfaces: Array<{ surface_id: string display_name: string kind: 'deployment' | 'service' | 'ingress' | 'cronjob' | 'configmap' | 'secret' | 'rbac' | 'policy' | 'autoscaler' | 'availability' manifest_ref: string status: 'manifest_mapped' | 'action_required' | 'blocked' | 'missing' risk_level: 'low' | 'medium' | 'high' | 'critical' evidence_level: 'committed_manifest' | 'source_file' | 'missing_manifest' | 'live_check_required' runtime_binding: string health_contract: string secret_exposure: 'none' | 'name_only' | 'template_only' | 'payload_redacted' live_check_status: 'not_run' | 'not_applicable' | 'required' evidence_refs: string[] next_action: string }> source_runtime_components: Array<{ component_id: string display_name: string source_ref: string component_kind: string runtime_binding: string status: 'bound' | 'action_required' | 'source_only' next_action: string }> evidence_gaps: Array<{ gap_id: string severity: 'low' | 'medium' | 'high' | 'critical' status: 'action_required' | 'blocked' | 'accepted' summary: string evidence_refs: string[] next_action: string }> operator_contract: { display_mode: 'read_only_runtime_surface' must_not_interpret_as: string[] secret_display_policy: string } operation_boundaries: Record approval_boundaries: Record } export interface GiteaWorkflowRunnerHealthSnapshot { schema_version: 'gitea_workflow_runner_health_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 } source_refs: string[] rollups: { total_workflows: number by_workflow_status: Record by_runner_evidence_status: Record workflows_with_schedule: number workflows_with_workflow_dispatch: number workflows_with_notify_bridge: number workflows_with_actionable_or_failure_quiet_policy: number workflow_ids_requiring_runner_attestation: string[] total_runner_contracts: number runner_contracts_requiring_action: string[] notification_contracts_total: number notification_contracts_quiet_success_count: number notification_contracts_quiet_success_ids: string[] } workflow_records: Array<{ workflow_id: string file_ref: string display_name: string scope: string status: 'manifest_mapped' | 'action_required' | 'blocked' risk_level: 'low' | 'medium' | 'high' | 'critical' triggers: string[] schedule_cadence: string runner_labels: string[] runner_evidence_status: 'host_runner_mapped' | 'owner_attestation_required' | 'comment_ambiguous' job_count: number notification_policy: string notify_bridge_calls: number secrets_policy_status: string evidence_refs: string[] next_action: string }> runner_contracts: Array<{ contract_id: string display_name: string status: 'manifest_mapped' | 'action_required' | 'dry_run_only' | 'prepared_not_applied_by_snapshot' risk_level: 'low' | 'medium' | 'high' | 'critical' runner_labels: string[] used_by_workflows: string[] health_contract: string guardrail_refs: string[] evidence_refs: string[] next_action: string }> notification_contracts: Array<{ contract_id: string display_name: string status: 'preserved' | 'exception_documented' | 'action_required' policy_kind: 'failure_only' | 'actionable_only' | 'deployment_status_exception' | 'manual_status_exception' | 'read_only_no_notify' success_noise_policy: string failure_policy: string workflow_refs: string[] evidence_refs: string[] next_action: string }> latest_observations: Array<{ observation_id: string status: string summary: string evidence_refs: string[] }> operator_contract: { display_mode: 'read_only_gitea_workflow_runner_health' must_not_interpret_as: string[] secret_display_policy: string runner_mutation_policy: string notification_policy: string } operation_boundaries: Record approval_boundaries: Record } export interface ObservabilityContractMatrixSnapshot { schema_version: 'observability_contract_matrix_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 } source_refs: string[] rollups: { total_surfaces: number by_kind: Record by_status: Record by_evidence_status: Record by_noise_policy_status: Record surface_ids_requiring_action: string[] surface_ids_with_proposal_only_noise_policy: string[] noise_reduction_opportunities_total: number approval_required_opportunity_ids: string[] classification_gap_ids: string[] read_only_denials_total: number } observability_surfaces: Array<{ surface_id: string display_name: string kind: string status: 'verified' | 'action_required' | 'blocked' risk_level: 'low' | 'medium' | 'high' | 'critical' evidence_status: string noise_policy_status: string coverage_contract: string current_contract?: string evidence_refs: string[] next_action: string }> noise_reduction_opportunities: Array<{ opportunity_id: string display_name: string status: string proposal_only: true impact: string target_surface_ids?: string[] evidence_refs: string[] next_action: string }> classification_gaps: Array<{ gap_id: string display_name: string status: string severity: 'low' | 'medium' | 'high' | 'critical' summary: string evidence_refs: string[] next_action: string }> latest_observations: Array<{ observation_id: string status: string summary: string evidence_refs: string[] }> operator_contract: { display_mode: 'read_only_observability_contract_matrix' must_not_interpret_as: string[] secret_display_policy: string alertmanager_route_policy: string noise_reduction_policy: string notification_policy: string } operation_boundaries: Record approval_boundaries: Record } 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 } export interface OffsiteEscrowReadinessStatusSnapshot { schema_version: 'offsite_escrow_readiness_status_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_cards: number by_readiness: Record by_kind: Record verified_offsite_card_ids: string[] blocked_escrow_card_ids: string[] action_required_card_ids: string[] execution_blocked_card_ids: string[] } readiness_cards: Array<{ card_id: string target_id: string display_name: string kind: 'offsite_mirror' | 'credential_escrow' | 'k8s_resource_offsite' readiness: 'verified' | 'action_required' | 'blocked' offsite_status: string escrow_status: string restore_drill_status: string credential_exposure_status: string automation_gate_status: string operator_summary: string next_action: string evidence_refs: string[] blocked_operations: string[] }> operator_contract: { display_mode: 'read_only_status' success_notification_policy: string failure_notification_policy: string credential_display_policy: string must_not_interpret_as: string[] } approval_boundaries: Record operation_boundaries: Record }