feat(governance): 顯示任務批准邊界與進度彙總
This commit is contained in:
@@ -357,6 +357,10 @@ export function AutomationInventoryTab() {
|
||||
const actionRequiredOffsiteCards = offsiteEscrow.rollups.by_readiness.action_required ?? 0
|
||||
const blockedEscrowCards = offsiteEscrow.rollups.by_readiness.blocked ?? 0
|
||||
const executionBlockedCards = offsiteEscrow.rollups.execution_blocked_card_ids.length
|
||||
const backlogProgressPercent = backlog.progress_summary.overall_percent
|
||||
const explicitApprovalItemCount = backlog.item_approval_boundary_rollup.items_requiring_explicit_approval.length
|
||||
const taskBoundaryCount = snapshot.task_approval_boundary_rollup.total_tasks
|
||||
const explicitApprovalTaskCount = snapshot.task_approval_boundary_rollup.tasks_requiring_explicit_approval.length
|
||||
const blockedApprovals = Object.entries(snapshot.approval_boundaries)
|
||||
.filter(([, allowed]) => allowed === false)
|
||||
.map(([key]) => key)
|
||||
@@ -377,6 +381,14 @@ export function AutomationInventoryTab() {
|
||||
}
|
||||
}
|
||||
|
||||
const boundaryActionLabel = (value: string) => {
|
||||
try {
|
||||
return t(`tasks.boundaryActions.${value}` as never)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20, display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
@@ -427,13 +439,65 @@ export function AutomationInventoryTab() {
|
||||
gap: 12,
|
||||
}} className="automation-inventory-kpi-grid">
|
||||
<MetricCard label={t('metrics.progress')} value={`${snapshot.program_status.overall_completion_percent}%`} tone="ok" icon={<ShieldCheck size={16} />} />
|
||||
<MetricCard label={t('metrics.backlogProgress')} value={`${backlogProgressPercent}%`} tone="warn" icon={<PackageCheck size={16} />} />
|
||||
<MetricCard label={t('metrics.assets')} value={snapshot.assets.length} icon={<Boxes size={16} />} />
|
||||
<MetricCard label={t('metrics.backlog')} value={backlog.rollups.total_items} tone="warn" icon={<PackageCheck size={16} />} />
|
||||
<MetricCard label={t('metrics.p1Backlog')} value={p1BacklogCount} icon={<Boxes size={16} />} />
|
||||
<MetricCard label={t('metrics.blocked')} value={blockedAssets} tone={blockedAssets > 0 ? 'warn' : 'ok'} icon={<AlertTriangle size={16} />} />
|
||||
<MetricCard label={t('metrics.critical')} value={criticalAssets} tone="danger" icon={<Lock size={16} />} />
|
||||
<MetricCard label={t('metrics.taskBoundaries')} value={taskBoundaryCount} icon={<Lock size={16} />} />
|
||||
<MetricCard label={t('metrics.explicitApprovalTasks')} value={explicitApprovalTaskCount} tone={explicitApprovalTaskCount > 0 ? 'warn' : 'ok'} icon={<ShieldCheck size={16} />} />
|
||||
</div>
|
||||
|
||||
<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 }}>
|
||||
<PackageCheck size={14} style={{ color: '#d97757' }} />
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
|
||||
{t('backlogProgress.title')}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', overflowWrap: 'anywhere' }}>
|
||||
{backlog.progress_summary.formula}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12 }} className="automation-inventory-progress-kpi-grid">
|
||||
<MetricCard label={t('backlogProgress.metrics.overall')} value={`${backlog.progress_summary.overall_percent}%`} tone="ok" icon={<ShieldCheck size={16} />} />
|
||||
<MetricCard label={t('backlogProgress.metrics.done')} value={`${backlog.progress_summary.done_items}/${backlog.progress_summary.total_items}`} icon={<PackageCheck size={16} />} />
|
||||
<MetricCard label={t('backlogProgress.metrics.planned')} value={backlog.progress_summary.planned_items} tone={backlog.progress_summary.planned_items > 0 ? 'warn' : 'ok'} icon={<AlertTriangle size={16} />} />
|
||||
<MetricCard label={t('backlogProgress.metrics.requiresApproval')} value={explicitApprovalItemCount} tone={explicitApprovalItemCount > 0 ? 'warn' : 'ok'} icon={<Lock size={16} />} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 0.8fr) minmax(0, 1.2fr)', gap: 12 }} className="automation-inventory-progress-grid">
|
||||
<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('backlogProgress.priorityTitle')}
|
||||
</span>
|
||||
{backlog.progress_summary.by_priority.map(row => (
|
||||
<ProgressRow
|
||||
key={row.priority}
|
||||
label={`${row.priority} · ${row.done_items}/${row.total_items}`}
|
||||
percent={row.completion_percent}
|
||||
nextTask={t('backlogProgress.doneLabel')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 10, minWidth: 0 }} className="automation-inventory-progress-workstream-grid">
|
||||
{backlog.progress_summary.by_workstream.slice(0, 6).map(row => (
|
||||
<ProgressRow
|
||||
key={row.workstream_id}
|
||||
label={`${row.display_name} · ${row.done_items}/${row.total_items}`}
|
||||
percent={row.completion_percent}
|
||||
nextTask={row.next_task_id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
|
||||
@@ -504,6 +568,23 @@ export function AutomationInventoryTab() {
|
||||
}}>
|
||||
{item.acceptance_criteria[0]}
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
fontSize: 10,
|
||||
color: '#87867f',
|
||||
lineHeight: 1.45,
|
||||
overflowWrap: 'anywhere',
|
||||
}}>
|
||||
{item.approval_boundary.display_summary}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
|
||||
<Chip value={`${t('tasks.boundaryLabels.mode')}: ${statusLabel(item.approval_boundary.mode)}`} muted />
|
||||
<Chip value={`${t('tasks.boundaryLabels.allowed')}: ${item.approval_boundary.allowed_actions[0]}`} muted />
|
||||
<Chip value={`${t('tasks.boundaryLabels.blocked')}: ${item.approval_boundary.blocked_actions.slice(0, 3).map(boundaryActionLabel).join(' / ')}`} />
|
||||
{item.approval_boundary.requires_operator_approval_for.length > 0 ? (
|
||||
<Chip value={`${t('tasks.boundaryLabels.requiresApproval')}: ${item.approval_boundary.requires_operator_approval_for[0]}`} muted />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{group.items.length > 5 ? (
|
||||
@@ -786,6 +867,25 @@ export function AutomationInventoryTab() {
|
||||
{task.title}
|
||||
</span>
|
||||
<ProgressRow label={task.owner_agent} percent={task.completion_percent} nextTask={task.gate_status} />
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 7, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
fontSize: 10,
|
||||
color: '#87867f',
|
||||
lineHeight: 1.45,
|
||||
overflowWrap: 'anywhere',
|
||||
}}>
|
||||
{task.approval_boundary.display_summary}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
|
||||
<Chip value={`${t('tasks.boundaryLabels.mode')}: ${statusLabel(task.approval_boundary.mode)}`} muted />
|
||||
<Chip value={`${t('tasks.boundaryLabels.allowed')}: ${task.approval_boundary.allowed_actions[0]}`} muted />
|
||||
<Chip value={`${t('tasks.boundaryLabels.blocked')}: ${task.approval_boundary.blocked_actions.slice(0, 3).map(boundaryActionLabel).join(' / ')}`} />
|
||||
{task.approval_boundary.requires_operator_approval_for.length > 0 ? (
|
||||
<Chip value={`${t('tasks.boundaryLabels.requiresApproval')}: ${task.approval_boundary.requires_operator_approval_for[0]}`} muted />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -813,6 +913,9 @@ export function AutomationInventoryTab() {
|
||||
<style jsx global>{`
|
||||
@media (max-width: 900px) {
|
||||
.automation-inventory-kpi-grid,
|
||||
.automation-inventory-progress-kpi-grid,
|
||||
.automation-inventory-progress-grid,
|
||||
.automation-inventory-progress-workstream-grid,
|
||||
.automation-inventory-workstream-grid,
|
||||
.automation-inventory-domain-grid,
|
||||
.automation-inventory-backlog-grid,
|
||||
|
||||
@@ -679,8 +679,21 @@ export interface AiAgentAutomationInventorySnapshot {
|
||||
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<string, number>
|
||||
tasks_requiring_explicit_approval: string[]
|
||||
tasks_with_blocked_operations: string[]
|
||||
}
|
||||
evidence: Array<{
|
||||
evidence_id: string
|
||||
kind: 'schema' | 'test' | 'browser' | 'api' | 'build' | 'doc' | 'runtime'
|
||||
@@ -715,6 +728,27 @@ export interface AiAgentAutomationBacklogSnapshot {
|
||||
by_gate_status: Record<string, number>
|
||||
by_owner_agent: Record<string, number>
|
||||
}
|
||||
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'
|
||||
@@ -730,8 +764,21 @@ export interface AiAgentAutomationBacklogSnapshot {
|
||||
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<string, number>
|
||||
items_requiring_explicit_approval: string[]
|
||||
items_with_blocked_operations: string[]
|
||||
}
|
||||
approval_boundaries: Record<
|
||||
| 'sdk_installation_allowed'
|
||||
| 'paid_api_call_allowed'
|
||||
|
||||
Reference in New Issue
Block a user