feat(awooop): expose execution evidence on incidents
All checks were successful
Code Review / ai-code-review (push) Successful in 17s
CD Pipeline / tests (push) Successful in 3m27s
CD Pipeline / build-and-deploy (push) Successful in 4m6s
CD Pipeline / post-deploy-checks (push) Successful in 1m33s

This commit is contained in:
Your Name
2026-05-20 15:19:48 +08:00
parent 312042ae6d
commit d4573cd00a
6 changed files with 197 additions and 0 deletions

View File

@@ -62,6 +62,32 @@ export interface AwoooPStatusChain {
last_error?: string | null;
}>;
};
execution?: {
operation_total?: number | null;
latest_operation_type?: string | null;
latest_status?: string | null;
latest_actor?: string | null;
latest_action?: string | null;
latest_executor?: string | null;
playbook_ids?: string[];
playbook_paths?: string[];
ansible?: {
considered?: boolean | null;
record_total?: number | null;
candidate_count?: number | null;
not_used_reason?: string | null;
latest_operation_type?: string | null;
latest_status?: string | null;
latest_playbook_path?: string | null;
latest_check_mode?: string | null | boolean;
candidate_playbooks?: Array<{
catalog_id?: string | null;
playbook_path?: string | null;
risk_level?: string | null;
match_score?: number | null;
}>;
};
};
}
function toneClass(chain?: AwoooPStatusChain | null) {

View File

@@ -137,6 +137,11 @@ function chainValue(value: string | null | undefined, fallback = '--'): string {
return normalized || fallback
}
function compactChainPath(value: string | null | undefined, fallback = '--'): string {
const normalized = chainValue(value, fallback)
return normalized.replace(/^infra\/ansible\/playbooks\//, '')
}
/** 格式化持續時間 */
function formatDuration(createdAt: string | undefined): string {
if (!createdAt) return '--'
@@ -351,6 +356,30 @@ export function IncidentCard({ incident, decision, statusChain, onApprovalChange
tools: mcpTopTools || '--',
})
: null
const execution = statusChain?.execution
const ansible = execution?.ansible
const ansibleSummary = ansible?.considered
? t('flowExecutionAnsibleConsidered', {
records: ansible.record_total ?? 0,
candidates: ansible.candidate_count ?? 0,
})
: ansible?.not_used_reason
? t('flowExecutionAnsibleNotUsed', { reason: String(ansible.not_used_reason).slice(0, 96) })
: t('flowExecutionAnsibleEmpty')
const executionPlaybook = compactChainPath(
execution?.playbook_paths?.[0]
?? execution?.playbook_ids?.[0]
?? ansible?.candidate_playbooks?.[0]?.playbook_path
)
const statusChainExecutionDetail = hasTruthChain
? t('flowExecutionDetail', {
executor: chainValue(execution?.latest_executor),
operation: chainValue(execution?.latest_operation_type),
status: chainValue(execution?.latest_status),
ansible: ansibleSummary,
playbook: executionPlaybook,
})
: null
const serviceName = incident.affected_services?.[0] ?? '--'
const duration = formatDuration(incident.created_at)
@@ -626,6 +655,10 @@ export function IncidentCard({ incident, decision, statusChain, onApprovalChange
<span data-testid="incident-mcp-evidence" style={{ color: '#555550' }}>
{statusChainMcpDetail}
</span>
<span style={{ color: '#b0ad9f' }}>/</span>
<span data-testid="incident-execution-evidence" style={{ color: '#555550' }}>
{statusChainExecutionDetail}
</span>
</>
)}
</div>