feat(web): 顯示 Backup DR 治理證據
This commit is contained in:
@@ -18,11 +18,11 @@ def test_ai_agent_automation_backlog_snapshot_endpoint_returns_committed_snapsho
|
||||
assert data["schema_version"] == "ai_agent_automation_backlog_v1"
|
||||
assert data["program_status"]["overall_completion_percent"] == 100
|
||||
assert data["program_status"]["read_only_mode"] is True
|
||||
assert data["program_status"]["current_task_id"] == "P1-103"
|
||||
assert data["program_status"]["next_task_id"] == "P1-104"
|
||||
assert data["rollups"]["total_items"] == len(data["backlog_items"]) == 18
|
||||
assert data["rollups"]["by_priority"]["P1"] == 16
|
||||
assert data["rollups"]["by_status"]["done"] == 11
|
||||
assert data["program_status"]["current_task_id"] == "P1-104"
|
||||
assert data["program_status"]["next_task_id"] == "P1-105"
|
||||
assert data["rollups"]["total_items"] == len(data["backlog_items"]) == 19
|
||||
assert data["rollups"]["by_priority"]["P1"] == 17
|
||||
assert data["rollups"]["by_status"]["done"] == 12
|
||||
assert data["approval_boundaries"]["sdk_installation_allowed"] is False
|
||||
assert data["approval_boundaries"]["paid_api_call_allowed"] is False
|
||||
assert data["approval_boundaries"]["production_routing_allowed"] is False
|
||||
@@ -30,4 +30,5 @@ def test_ai_agent_automation_backlog_snapshot_endpoint_returns_committed_snapsho
|
||||
assert any(item["item_id"] == "AUTO-P1-205" for item in data["backlog_items"])
|
||||
assert any(item["item_id"] == "AUTO-P1-206" for item in data["backlog_items"])
|
||||
assert any(item["item_id"] == "AUTO-P1-103" for item in data["backlog_items"])
|
||||
assert any(item["item_id"] == "AUTO-P1-104" for item in data["backlog_items"])
|
||||
assert any(item["item_id"] == "AUTO-P3-001" for item in data["backlog_items"])
|
||||
|
||||
@@ -18,8 +18,8 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps
|
||||
assert data["schema_version"] == "ai_agent_automation_inventory_snapshot_v1"
|
||||
assert data["program_status"]["overall_completion_percent"] == 100
|
||||
assert data["program_status"]["read_only_mode"] is True
|
||||
assert data["program_status"]["current_task_id"] == "P1-103"
|
||||
assert data["program_status"]["next_task_id"] == "P1-104"
|
||||
assert data["program_status"]["current_task_id"] == "P1-104"
|
||||
assert data["program_status"]["next_task_id"] == "P1-105"
|
||||
assert data["approval_boundaries"]["sdk_installation_allowed"] is False
|
||||
assert data["approval_boundaries"]["paid_api_call_allowed"] is False
|
||||
assert data["approval_boundaries"]["production_routing_allowed"] is False
|
||||
@@ -28,6 +28,7 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps
|
||||
assert any(task["task_id"] == "P1-205" for task in data["tasks"])
|
||||
assert any(task["task_id"] == "P1-206" for task in data["tasks"])
|
||||
assert any(task["task_id"] == "P1-103" for task in data["tasks"])
|
||||
assert any(task["task_id"] == "P1-104" for task in data["tasks"])
|
||||
assert any(evidence["evidence_id"] == "dependency_risk_policy_api" for evidence in data["evidence"])
|
||||
assert any(evidence["evidence_id"] == "dependency_drift_check_plan_api" for evidence in data["evidence"])
|
||||
assert any(
|
||||
@@ -35,3 +36,4 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps
|
||||
for evidence in data["evidence"]
|
||||
)
|
||||
assert any(evidence["evidence_id"] == "backup_notification_policy_api" for evidence in data["evidence"])
|
||||
assert any(evidence["evidence_id"] == "backup_dr_evidence_ui" for evidence in data["evidence"])
|
||||
|
||||
@@ -9,7 +9,20 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||
import { AlertTriangle, Boxes, Database, Lock, PackageCheck, RefreshCw, Server, ShieldCheck } from 'lucide-react'
|
||||
import {
|
||||
AlertTriangle,
|
||||
Archive,
|
||||
BellOff,
|
||||
BellRing,
|
||||
Boxes,
|
||||
Database,
|
||||
HardDrive,
|
||||
Lock,
|
||||
PackageCheck,
|
||||
RefreshCw,
|
||||
Server,
|
||||
ShieldCheck,
|
||||
} from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { GlassCard } from '@/components/ui/glass-card'
|
||||
import { StatusOrb } from '@/components/ui/status-orb'
|
||||
@@ -17,6 +30,9 @@ import {
|
||||
apiClient,
|
||||
type AiAgentAutomationBacklogSnapshot,
|
||||
type AiAgentAutomationInventorySnapshot,
|
||||
type BackupDrReadinessMatrixSnapshot,
|
||||
type BackupDrTargetInventorySnapshot,
|
||||
type BackupNotificationPolicySnapshot,
|
||||
} from '@/lib/api-client'
|
||||
|
||||
function formatDateTime(value: string): string {
|
||||
@@ -75,6 +91,13 @@ function Chip({ value, muted = false }: { value: string; muted?: boolean }) {
|
||||
)
|
||||
}
|
||||
|
||||
function evidenceTone(value: string): 'ok' | 'warn' | 'danger' | 'neutral' {
|
||||
if (value === 'ready' || value === 'verified' || value === 'active') return 'ok'
|
||||
if (value === 'action_required' || value === 'approval_required' || value === 'needs_metric_binding') return 'warn'
|
||||
if (value === 'blocked' || value === 'failed') return 'danger'
|
||||
return 'neutral'
|
||||
}
|
||||
|
||||
function MetricCard({
|
||||
label,
|
||||
value,
|
||||
@@ -177,6 +200,9 @@ export function AutomationInventoryTab() {
|
||||
const t = useTranslations('governance.automationInventory')
|
||||
const [snapshot, setSnapshot] = useState<AiAgentAutomationInventorySnapshot | null>(null)
|
||||
const [backlog, setBacklog] = useState<AiAgentAutomationBacklogSnapshot | null>(null)
|
||||
const [backupTargets, setBackupTargets] = useState<BackupDrTargetInventorySnapshot | null>(null)
|
||||
const [backupReadiness, setBackupReadiness] = useState<BackupDrReadinessMatrixSnapshot | null>(null)
|
||||
const [backupPolicy, setBackupPolicy] = useState<BackupNotificationPolicySnapshot | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(false)
|
||||
|
||||
@@ -185,10 +211,16 @@ export function AutomationInventoryTab() {
|
||||
Promise.all([
|
||||
apiClient.getAiAgentAutomationInventorySnapshot(),
|
||||
apiClient.getAiAgentAutomationBacklogSnapshot(),
|
||||
apiClient.getBackupDrTargetInventory(),
|
||||
apiClient.getBackupDrReadinessMatrix(),
|
||||
apiClient.getBackupNotificationPolicy(),
|
||||
])
|
||||
.then(([inventoryData, backlogData]) => {
|
||||
.then(([inventoryData, backlogData, targetData, readinessData, policyData]) => {
|
||||
setSnapshot(inventoryData)
|
||||
setBacklog(backlogData)
|
||||
setBackupTargets(targetData)
|
||||
setBackupReadiness(readinessData)
|
||||
setBackupPolicy(policyData)
|
||||
setError(false)
|
||||
})
|
||||
.catch(() => setError(true))
|
||||
@@ -224,6 +256,32 @@ export function AutomationInventoryTab() {
|
||||
.filter(group => group.items.length > 0)
|
||||
}, [backlog])
|
||||
|
||||
const visibleReadinessRows = useMemo(() => {
|
||||
if (!backupReadiness) return []
|
||||
const priority = { blocked: 0, action_required: 1, deferred: 2, ready: 3 } as Record<string, number>
|
||||
return [...backupReadiness.readiness_rows]
|
||||
.sort((a, b) => {
|
||||
const left = priority[a.overall_readiness] ?? 4
|
||||
const right = priority[b.overall_readiness] ?? 4
|
||||
if (left !== right) return left - right
|
||||
return a.target_id.localeCompare(b.target_id)
|
||||
})
|
||||
.slice(0, 8)
|
||||
}, [backupReadiness])
|
||||
|
||||
const visibleBackupTargets = useMemo(() => {
|
||||
if (!backupTargets) return []
|
||||
const priority = { critical: 0, high: 1, medium: 2, low: 3 } as Record<string, number>
|
||||
return [...backupTargets.backup_targets]
|
||||
.sort((a, b) => {
|
||||
const left = priority[a.risk_level] ?? 4
|
||||
const right = priority[b.risk_level] ?? 4
|
||||
if (left !== right) return left - right
|
||||
return a.target_id.localeCompare(b.target_id)
|
||||
})
|
||||
.slice(0, 6)
|
||||
}, [backupTargets])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: 20, display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12 }} className="automation-inventory-kpi-grid">
|
||||
@@ -237,7 +295,7 @@ export function AutomationInventoryTab() {
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !snapshot || !backlog) {
|
||||
if (error || !snapshot || !backlog || !backupTargets || !backupReadiness || !backupPolicy) {
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<GlassCard variant="subtle" padding="lg">
|
||||
@@ -275,10 +333,23 @@ export function AutomationInventoryTab() {
|
||||
const criticalAssets = snapshot.assets.filter(asset => asset.risk_level === 'critical').length
|
||||
const completedTasks = snapshot.tasks.filter(task => task.status === 'done').length
|
||||
const p1BacklogCount = backlog.rollups.by_priority.P1 ?? 0
|
||||
const readyBackupRows = backupReadiness.rollups.by_overall_readiness.ready ?? 0
|
||||
const actionRequiredBackupRows = backupReadiness.rollups.by_overall_readiness.action_required ?? 0
|
||||
const blockedBackupRows = backupReadiness.rollups.by_overall_readiness.blocked ?? 0
|
||||
const suppressedSuccessRules = backupPolicy.rollups.by_decision.suppress_immediate_success ?? 0
|
||||
const immediateEscalationRules = backupPolicy.rollups.by_decision.escalate_immediate ?? 0
|
||||
const blockedApprovals = Object.entries(snapshot.approval_boundaries)
|
||||
.filter(([, allowed]) => allowed === false)
|
||||
.map(([key]) => key)
|
||||
|
||||
const statusLabel = (value: string) => {
|
||||
try {
|
||||
return t(`backupEvidence.statuses.${value}` as never)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20, display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
@@ -450,6 +521,125 @@ export function AutomationInventoryTab() {
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
|
||||
<Archive size={14} style={{ color: '#d97757' }} />
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
|
||||
{t('backupEvidence.title')}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f' }}>
|
||||
{t('backupEvidence.source', {
|
||||
targets: formatDateTime(backupTargets.generated_at),
|
||||
readiness: formatDateTime(backupReadiness.generated_at),
|
||||
policy: formatDateTime(backupPolicy.generated_at),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, minmax(0, 1fr))', gap: 12 }} className="automation-inventory-backup-kpi-grid">
|
||||
<MetricCard label={t('backupEvidence.metrics.targets')} value={backupTargets.rollups.total_targets} icon={<HardDrive size={16} />} />
|
||||
<MetricCard label={t('backupEvidence.metrics.ready')} value={readyBackupRows} tone="ok" icon={<ShieldCheck size={16} />} />
|
||||
<MetricCard label={t('backupEvidence.metrics.actionRequired')} value={actionRequiredBackupRows} tone="warn" icon={<AlertTriangle size={16} />} />
|
||||
<MetricCard label={t('backupEvidence.metrics.blocked')} value={blockedBackupRows} tone={blockedBackupRows > 0 ? 'danger' : 'ok'} icon={<Lock size={16} />} />
|
||||
<MetricCard label={t('backupEvidence.metrics.successSuppressed')} value={suppressedSuccessRules} tone="ok" icon={<BellOff size={16} />} />
|
||||
<MetricCard label={t('backupEvidence.metrics.immediateEscalations')} value={immediateEscalationRules} tone="warn" icon={<BellRing size={16} />} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.35fr) minmax(0, 0.65fr)', gap: 12 }} className="automation-inventory-backup-evidence-grid">
|
||||
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 11, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
|
||||
{t('backupEvidence.readinessTitle')}
|
||||
</span>
|
||||
<Chip value={backupReadiness.program_status.current_task_id} muted />
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 10 }} className="automation-inventory-backup-readiness-grid">
|
||||
{visibleReadinessRows.map(row => (
|
||||
<div key={row.target_id} style={{ padding: 11, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#faf9f3', minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, minWidth: 0 }}>
|
||||
<span style={{
|
||||
fontFamily: 'Syne, sans-serif',
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
color: '#141413',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{row.display_name}
|
||||
</span>
|
||||
<Chip value={statusLabel(row.overall_readiness)} muted={row.overall_readiness === 'ready'} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
|
||||
<Chip value={`${t('backupEvidence.labels.freshness')}: ${statusLabel(row.freshness_status)}`} muted={evidenceTone(row.freshness_status) === 'ok'} />
|
||||
<Chip value={`${t('backupEvidence.labels.integrity')}: ${statusLabel(row.integrity_status)}`} muted={evidenceTone(row.integrity_status) === 'ok'} />
|
||||
<Chip value={`${t('backupEvidence.labels.restore')}: ${statusLabel(row.restore_drill_status)}`} muted={evidenceTone(row.restore_drill_status) === 'ok'} />
|
||||
<Chip value={`${t('backupEvidence.labels.offsite')}: ${statusLabel(row.offsite_status)}`} muted={evidenceTone(row.offsite_status) === 'ok'} />
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{row.blocker_summary || t('backupEvidence.noBlocker')}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
|
||||
<Chip value={row.gate_status ? statusLabel(row.gate_status) : '--'} muted />
|
||||
<Chip value={row.evidence_refs[0] ?? t('backupEvidence.noEvidence')} muted />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 }}>
|
||||
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
|
||||
{t('backupEvidence.policyTitle')}
|
||||
</span>
|
||||
{backupPolicy.policy_rules.slice(0, 5).map(rule => (
|
||||
<div key={rule.rule_id} style={{ display: 'flex', flexDirection: 'column', gap: 6, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<span style={{ fontFamily: "'DM Mono', monospace", fontSize: 11, fontWeight: 700, color: '#141413', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{rule.rule_id}
|
||||
</span>
|
||||
<Chip value={statusLabel(rule.decision)} muted={rule.decision === 'suppress_immediate_success'} />
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
|
||||
{rule.message_contract}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
|
||||
{t('backupEvidence.targetsTitle')}
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{visibleBackupTargets.map(target => (
|
||||
<div key={target.target_id} style={{ display: 'flex', flexDirection: 'column', gap: 6, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 12, fontWeight: 700, color: '#141413', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{target.display_name}
|
||||
</span>
|
||||
<Chip value={target.rpo} muted />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
<Chip value={target.risk_level} />
|
||||
<Chip value={target.owner_host} muted />
|
||||
<Chip value={target.primary_script} muted />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1.2fr) minmax(0, 0.8fr)', gap: 12 }} className="automation-inventory-bottom-grid">
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>
|
||||
@@ -511,6 +701,9 @@ export function AutomationInventoryTab() {
|
||||
.automation-inventory-workstream-grid,
|
||||
.automation-inventory-domain-grid,
|
||||
.automation-inventory-backlog-grid,
|
||||
.automation-inventory-backup-kpi-grid,
|
||||
.automation-inventory-backup-evidence-grid,
|
||||
.automation-inventory-backup-readiness-grid,
|
||||
.automation-inventory-bottom-grid,
|
||||
.automation-inventory-task-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
|
||||
@@ -261,6 +261,21 @@ export const apiClient = {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/automation-backlog-snapshot`)
|
||||
return handleResponse<AiAgentAutomationBacklogSnapshot>(res)
|
||||
},
|
||||
|
||||
async getBackupDrTargetInventory() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/backup-dr-target-inventory`)
|
||||
return handleResponse<BackupDrTargetInventorySnapshot>(res)
|
||||
},
|
||||
|
||||
async getBackupDrReadinessMatrix() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/backup-dr-readiness-matrix`)
|
||||
return handleResponse<BackupDrReadinessMatrixSnapshot>(res)
|
||||
},
|
||||
|
||||
async getBackupNotificationPolicy() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/backup-notification-policy`)
|
||||
return handleResponse<BackupNotificationPolicySnapshot>(res)
|
||||
},
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -721,3 +736,126 @@ export interface AiAgentAutomationBacklogSnapshot {
|
||||
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<string, number>
|
||||
by_target_type: Record<string, number>
|
||||
by_gate_status: Record<string, number>
|
||||
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<string, false>
|
||||
operation_boundaries: Record<string, boolean>
|
||||
}
|
||||
|
||||
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<string, number>
|
||||
by_restore_drill_status: Record<string, number>
|
||||
by_offsite_status: Record<string, number>
|
||||
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<string, false>
|
||||
operation_boundaries: Record<string, boolean>
|
||||
}
|
||||
|
||||
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<string, number>
|
||||
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<string, unknown>
|
||||
approval_boundaries: Record<string, false>
|
||||
operation_boundaries: Record<string, boolean>
|
||||
}
|
||||
|
||||
@@ -686,6 +686,52 @@ API:
|
||||
- Backup notification policy service + API tests `9 passed`。
|
||||
- `py_compile` 通過。
|
||||
|
||||
### P1-104 Backup / DR 證據 UI 摘要
|
||||
|
||||
正式 UI:
|
||||
|
||||
- `/zh-TW/governance?tab=automation-inventory`
|
||||
|
||||
接入只讀 API:
|
||||
|
||||
- `GET /api/v1/agents/backup-dr-target-inventory`
|
||||
- `GET /api/v1/agents/backup-dr-readiness-matrix`
|
||||
- `GET /api/v1/agents/backup-notification-policy`
|
||||
|
||||
顯示內容:
|
||||
|
||||
- Backup / DR 目標:`17`
|
||||
- Ready:`12`
|
||||
- 需處置:`2`
|
||||
- 阻擋:`2`
|
||||
- 成功即時抑制:`2`
|
||||
- failure / warning / core blocker 立即升級:`4`
|
||||
- 通知規則:`8`
|
||||
|
||||
核心裁決:
|
||||
|
||||
- UI 只顯示備份目標、readiness matrix、通知政策、關鍵 blocker 與 evidence ref。
|
||||
- `configs_capture` 與 `credential_escrow_markers` 仍為 blocked,不得宣稱 full DR green。
|
||||
- `signoz` 顯示 disruptive backup guard;Agent 不得任意觸發會短暫停止 collector 的備份。
|
||||
- 成功備份仍不即時送 Telegram / AwoooP;成功狀態由每日摘要與查詢承載。
|
||||
- warning、failed、action-required、core blocker 才能進即時升級。
|
||||
|
||||
實作邊界:
|
||||
|
||||
- 不執行 backup。
|
||||
- 不執行 restore。
|
||||
- 不執行 offsite sync。
|
||||
- 不寫 credential marker。
|
||||
- 不改排程、不寫 workflow。
|
||||
- 不發 Telegram 測試訊息。
|
||||
|
||||
驗證:
|
||||
|
||||
- `pnpm --filter @awoooi/web exec tsc --noEmit --tsBuildInfoFile /tmp/awoooi-p1-104-backup-evidence.tsbuildinfo` 通過。
|
||||
- 本地 desktop `/zh-TW/governance?tab=automation-inventory&_v=p1-104-backup-evidence-local`:Backup / DR 證據、準備度矩陣、通知政策、成功抑制、即時升級、Gitea 與 SignOz disruptive guard 可見;無載入錯誤;`horizontalOverflow=-6`。
|
||||
- 本地 390px mobile `/zh-TW/governance?tab=automation-inventory&_v=p1-104-backup-evidence-local`:Backup / DR 證據、準備度矩陣、通知政策、成功抑制、即時升級、Gitea 與 SignOz disruptive guard 可見;無載入錯誤;`horizontalOverflow=-6`。
|
||||
- 截圖:`/tmp/awoooi-p1-104-backup-evidence-local-desktop.png`、`/tmp/awoooi-p1-104-backup-evidence-local-mobile.png`。
|
||||
|
||||
### P0 - 治理與 Inventory 基礎
|
||||
|
||||
| ID | 狀態 | % | 負責 Agent | 任務 | 產出 | 關卡 |
|
||||
@@ -718,7 +764,7 @@ API:
|
||||
| P1-101 | 完成 | 100 | Hermes | 把備份 runbook / 腳本轉成機器可讀目標盤點 | `docs/evaluations/backup_dr_target_inventory_2026-06-04.json` | 只讀 |
|
||||
| P1-102 | 完成 | 100 | OpenClaw | 顯示備份新鮮度、完整性、復原演練狀態 | `docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json` | 不執行 restore |
|
||||
| P1-103 | 完成 | 100 | Hermes | 對齊備份通知政策 | `docs/evaluations/backup_notification_policy_2026-06-04.json` | 不發成功洗版 |
|
||||
| P1-104 | 待辦 | 0 | OpenClaw | 在 AwoooP / governance UI 加備份證據 | 備份卡片 | 瀏覽器驗證 |
|
||||
| P1-104 | 完成 | 100 | OpenClaw | 在 AwoooP / governance UI 加備份證據 | `/zh-TW/governance?tab=automation-inventory` | 只讀 + 瀏覽器驗證 |
|
||||
| P1-105 | 待辦 | 0 | OpenClaw | 定義復原演練批准包 | 復原計畫範本 | 人工批准 |
|
||||
| P1-106 | 待辦 | 0 | Hermes | 顯示異地 / escrow 準備度狀態 | DR 準備度區塊 | 不暴露 credential |
|
||||
|
||||
@@ -860,13 +906,24 @@ API:
|
||||
|
||||
任何完成宣告前,必須同步更新本文件或後續生成的 JSON 快照。
|
||||
|
||||
本次同步:
|
||||
|
||||
```text
|
||||
進度:100%。
|
||||
目前優先級:P1。
|
||||
目前任務:P1-104 在 AwoooP / governance UI 加備份證據。
|
||||
狀態變更:待辦 -> 完成。
|
||||
證據:typecheck 通過;本地 desktop 與 390px mobile governance automation-inventory tab 驗證 Backup / DR 證據、準備度矩陣、通知政策、成功抑制、即時升級、Gitea 與 SignOz disruptive guard 可見;無載入錯誤;horizontalOverflow <= 0。
|
||||
阻擋:無;backup、restore、offsite sync、credential marker、排程、workflow、Telegram 測試通知仍未批准。
|
||||
下一步:P1-105 定義復原演練批准包。
|
||||
```
|
||||
|
||||
## 13. 立即執行順序
|
||||
|
||||
1. P1-104:在 AwoooP / governance UI 加備份證據。
|
||||
2. P1-105:定義復原演練批准包。
|
||||
3. P1-106:顯示異地 / escrow 準備度狀態。
|
||||
4. P1-305 / P1-306:補每個任務的批准邊界與進度彙總細節。
|
||||
5. P2 / P3 必須等 P1 可見且關卡穩定後再做。
|
||||
1. P1-105:定義復原演練批准包。
|
||||
2. P1-106:顯示異地 / escrow 準備度狀態。
|
||||
3. P1-305 / P1-306:補每個任務的批准邊界與進度彙總細節。
|
||||
4. P2 / P3 必須等 P1 可見且關卡穩定後再做。
|
||||
|
||||
## 14. 目前風險
|
||||
|
||||
|
||||
@@ -5,30 +5,30 @@
|
||||
"program_status": {
|
||||
"overall_completion_percent": 100,
|
||||
"current_priority": "P1",
|
||||
"current_task_id": "P1-103",
|
||||
"next_task_id": "P1-104",
|
||||
"current_task_id": "P1-104",
|
||||
"next_task_id": "P1-105",
|
||||
"read_only_mode": true
|
||||
},
|
||||
"rollups": {
|
||||
"total_items": 18,
|
||||
"total_items": 19,
|
||||
"by_priority": {
|
||||
"P1": 16,
|
||||
"P1": 17,
|
||||
"P2": 1,
|
||||
"P3": 1
|
||||
},
|
||||
"by_status": {
|
||||
"planned": 7,
|
||||
"done": 11
|
||||
"done": 12
|
||||
},
|
||||
"by_gate_status": {
|
||||
"read_only_allowed": 15,
|
||||
"read_only_allowed": 16,
|
||||
"production_change_blocked": 1,
|
||||
"cost_approval_required": 1,
|
||||
"blocked_by_evidence": 1
|
||||
},
|
||||
"by_owner_agent": {
|
||||
"hermes": 10,
|
||||
"openclaw": 7,
|
||||
"openclaw": 8,
|
||||
"nemotron": 1
|
||||
}
|
||||
},
|
||||
@@ -280,6 +280,33 @@
|
||||
],
|
||||
"next_review": "P1-103"
|
||||
},
|
||||
{
|
||||
"item_id": "AUTO-P1-104",
|
||||
"priority": "P1",
|
||||
"status": "done",
|
||||
"workstream_id": "WS4",
|
||||
"source_asset_id": "backup_dr_readiness_matrix",
|
||||
"source_signal_kind": "ui_visibility_gap",
|
||||
"title": "在 AwoooP / governance UI 加備份證據",
|
||||
"owner_agent": "openclaw",
|
||||
"recommended_action": "在 automation inventory tab 顯示 Backup / DR 目標、readiness matrix、通知政策、blocked / action-required 與 success-noise suppression 證據。",
|
||||
"action_class": "execute_read_only",
|
||||
"gate_status": "read_only_allowed",
|
||||
"risk_level": "high",
|
||||
"evidence_refs": [
|
||||
"apps/web/src/app/[locale]/governance/tabs/automation-inventory-tab.tsx",
|
||||
"GET /api/v1/agents/backup-dr-target-inventory",
|
||||
"GET /api/v1/agents/backup-dr-readiness-matrix",
|
||||
"GET /api/v1/agents/backup-notification-policy"
|
||||
],
|
||||
"acceptance_criteria": [
|
||||
"顯示 Backup / DR 證據但不提供 backup、restore、offsite sync、credential marker、schedule 或 workflow 操作",
|
||||
"顯示 ready、action-required、blocked、success suppressed 與 immediate escalation rollup",
|
||||
"desktop 與 390px mobile 無橫向溢出",
|
||||
"成功備份仍不得即時送 Telegram / AwoooP 洗版"
|
||||
],
|
||||
"next_review": "P1-104"
|
||||
},
|
||||
{
|
||||
"item_id": "AUTO-P1-201",
|
||||
"priority": "P1",
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"program_status": {
|
||||
"overall_completion_percent": 100,
|
||||
"current_priority": "P1",
|
||||
"current_task_id": "P1-103",
|
||||
"next_task_id": "P1-104",
|
||||
"current_task_id": "P1-104",
|
||||
"next_task_id": "P1-105",
|
||||
"read_only_mode": true
|
||||
},
|
||||
"status_taxonomy": {
|
||||
@@ -425,7 +425,7 @@
|
||||
"display_name": "備份與 DR 自動化",
|
||||
"completion_percent": 67,
|
||||
"status": "in_progress",
|
||||
"next_task_id": "P1-104"
|
||||
"next_task_id": "P1-105"
|
||||
},
|
||||
{
|
||||
"workstream_id": "WS5",
|
||||
@@ -451,9 +451,9 @@
|
||||
{
|
||||
"workstream_id": "WS8",
|
||||
"display_name": "產品 UI",
|
||||
"completion_percent": 75,
|
||||
"completion_percent": 82,
|
||||
"status": "in_progress",
|
||||
"next_task_id": "P1-104"
|
||||
"next_task_id": "P1-305"
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
@@ -620,7 +620,18 @@
|
||||
"title": "對齊備份通知政策",
|
||||
"output": "docs/evaluations/backup_notification_policy_2026-06-04.json",
|
||||
"gate_status": "read_only_allowed",
|
||||
"next_action": "完成,P1-104 在 AwoooP / governance UI 加備份證據。"
|
||||
"next_action": "完成,P1-104 Backup / DR 證據 UI 已推進。"
|
||||
},
|
||||
{
|
||||
"task_id": "P1-104",
|
||||
"priority": "P1",
|
||||
"status": "done",
|
||||
"completion_percent": 100,
|
||||
"owner_agent": "openclaw",
|
||||
"title": "在 AwoooP / governance UI 加備份證據",
|
||||
"output": "/zh-TW/governance?tab=automation-inventory",
|
||||
"gate_status": "read_only_allowed",
|
||||
"next_action": "完成,P1-105 定義復原演練批准包。"
|
||||
},
|
||||
{
|
||||
"task_id": "P1-201",
|
||||
@@ -810,6 +821,12 @@
|
||||
"ref": "GET /api/v1/agents/backup-notification-policy",
|
||||
"result": "備份通知政策只讀 API 已新增,不送通知、不執行備份/restore/offsite sync、不寫 credential marker、不改排程或 workflow。"
|
||||
},
|
||||
{
|
||||
"evidence_id": "backup_dr_evidence_ui",
|
||||
"kind": "browser",
|
||||
"ref": "/zh-TW/governance?tab=automation-inventory",
|
||||
"result": "P1-104 Backup / DR 證據 UI 已接入 automation inventory tab;本地 desktop 與 390px mobile 驗證 Backup / DR 證據、準備度矩陣、通知政策、成功抑制、即時升級、Gitea 與 SignOz disruptive guard 可見,無載入錯誤,horizontalOverflow <= 0。"
|
||||
},
|
||||
{
|
||||
"evidence_id": "package_supply_chain_inventory_schema",
|
||||
"kind": "schema",
|
||||
|
||||
@@ -3370,3 +3370,26 @@ Phase 6 完成後
|
||||
- P1-103:備份通知政策已完成,schema 位於 `docs/schemas/backup_notification_policy_v1.schema.json`,快照位於 `docs/evaluations/backup_notification_policy_2026-06-04.json`,API 為 `GET /api/v1/agents/backup-notification-policy`;8 條規則中 2 條成功即時抑制、4 條 immediate escalation、2 條 action-required,每日成功摘要由 06:05 台北時間承載。
|
||||
|
||||
**裁決:** P0 基礎已完成,P1 產品面已接上分組 UI,Backup / DR 目標盤點、準備度矩陣、備份通知政策與 WS5 套件 / 供應鏈自動化已進入只讀 API 並達 `100%`。下一輪推進必須從 P1-104 備份證據 UI 開始,保持只讀;不得執行 restore、不得寫 credential marker、不得送 Telegram / AwoooP 測試通知、不得安裝依賴、不得升級套件、不得寫 lockfile、不得查外部 CVE、不得查外部 license、不得查外部 registry 或 Agent market 來源、不得啟用排程、不得寫 workflow、不得執行 npm audit、不得執行 pnpm install、不得執行 docker build、不得 pull image、不得重建 image、不得 push registry、不得新增 SDK、不得呼叫付費 API、不得改生產路由、不得把任何 Agent 推入 shadow/canary。
|
||||
|
||||
### 2026-06-05 凌晨 (台北) — P1-104 Backup / DR 證據 UI 完成
|
||||
|
||||
**觸發**:統帥批准繼續,要求持續更新工作清單、完成度與工作狀態,並記得推版到正式環境。
|
||||
|
||||
**已推進:**
|
||||
- P1-104:AwoooP / governance automation inventory tab 已接入 Backup / DR 證據區塊,讀取 `GET /api/v1/agents/backup-dr-target-inventory`、`GET /api/v1/agents/backup-dr-readiness-matrix`、`GET /api/v1/agents/backup-notification-policy`。
|
||||
- UI 顯示 Backup / DR 目標 `17`、ready `12`、action_required `2`、blocked `2`、成功即時抑制 `2`、immediate escalation `4`,並顯示準備度矩陣、通知政策與關鍵備份目標。
|
||||
- `docs/evaluations/ai_agent_automation_inventory_snapshot_2026-06-04_static_seed.json` 已將 `current_task_id` 推進到 `P1-104`、`next_task_id` 推進到 `P1-105`;產品 UI workstream 由 `75%` 推進到 `82%`。
|
||||
- `docs/evaluations/ai_agent_automation_backlog_2026-06-04.json` 新增 `AUTO-P1-104` done item,rollup 更新為 total `19`、P1 `17`、done `12`、read_only_allowed `16`、OpenClaw owner `8`。
|
||||
- `docs/ai/AI_AGENT_AUTOMATION_WORKLIST_2026-06-04.md` 已新增 P1-104 摘要、進度同步紀錄與下一步順序。
|
||||
|
||||
**驗證:**
|
||||
- `pnpm --filter @awoooi/web exec tsc --noEmit --tsBuildInfoFile /tmp/awoooi-p1-104-backup-evidence.tsbuildinfo` 通過。
|
||||
- 本地 desktop `/zh-TW/governance?tab=automation-inventory&_v=p1-104-backup-evidence-local`:Backup / DR 證據、準備度矩陣、通知政策、成功抑制、即時升級、Gitea 與 SignOz disruptive guard 可見;無載入錯誤;`horizontalOverflow=-6`。
|
||||
- 本地 390px mobile `/zh-TW/governance?tab=automation-inventory&_v=p1-104-backup-evidence-local`:Backup / DR 證據、準備度矩陣、通知政策、成功抑制、即時升級、Gitea 與 SignOz disruptive guard 可見;無載入錯誤;`horizontalOverflow=-6`。
|
||||
|
||||
**下一步:**
|
||||
1. P1-105:定義復原演練批准包。
|
||||
2. P1-106:顯示異地 / escrow 準備度狀態。
|
||||
3. P1-305 / P1-306:補任務批准邊界與進度彙總細節。
|
||||
|
||||
**裁決:** P1-104 已完成,但仍只屬於 read-only evidence surface。不得執行 backup、restore、offsite sync、credential marker 寫入、排程變更、workflow 寫入或 Telegram 測試通知;不得把 Backup / DR UI 可見解讀成 full DR green。下一步只能產生復原演練與 escrow review 的批准包,必須保留 OpenClaw 仲裁與人工批准邊界。
|
||||
|
||||
Reference in New Issue
Block a user