feat(reports): 顯示資料源 PlayBook Verifier 缺口
All checks were successful
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m32s
CD Pipeline / build-and-deploy (push) Successful in 4m35s
CD Pipeline / post-deploy-checks (push) Successful in 1m36s

This commit is contained in:
Your Name
2026-06-18 20:40:01 +08:00
parent 748096c2ce
commit 6ab640e431
5 changed files with 188 additions and 3 deletions

View File

@@ -157,6 +157,24 @@ interface ReportAutomationAsset {
next_action: string
}
interface ReportSourceGapPlaybookVerifier {
work_item_id: string
source_id: string
display_name: string
route: string
playbook_draft_id: string
verifier_plan_id: string
playbook_state: string
verifier_state: string
script_state: string
schedule_state: string
owner_review_required: boolean
runtime_gate_open: boolean
playbook_template_fields: string[]
verifier_checks: string[]
next_action: string
}
interface ReportSourceHealthSnapshot {
source_health: ReportSourceHealthItem[]
all_zero_assessment: {
@@ -168,12 +186,16 @@ interface ReportSourceHealthSnapshot {
}
no_send_previews: ReportNoSendPreview[]
automation_assets: ReportAutomationAsset[]
source_gap_playbook_verifier: ReportSourceGapPlaybookVerifier[]
rollups: {
source_count: number
source_ok_count: number
source_gap_count: number
confidence_percent: number
report_work_item_count: number
source_gap_playbook_draft_count: number
source_gap_verifier_plan_count: number
source_gap_owner_review_required_count: number
live_send_allowed_count: number
runtime_gate_count: number
}
@@ -303,6 +325,7 @@ export default function ReportsPage({ params }: { params: { locale: string } })
() => new Map((sourceHealth?.no_send_previews ?? []).map(preview => [preview.cadence_id, preview])),
[sourceHealth]
)
const sourceGapCards = sourceHealth?.source_gap_playbook_verifier ?? []
const pcts = useMemo(() => {
if (!ds || ds.total === 0) return null
@@ -452,6 +475,35 @@ export default function ReportsPage({ params }: { params: { locale: string } })
</div>
</section>
<section className="rounded-[7px] border border-border bg-card p-4 shadow-sm">
<SectionHeader icon={<ShieldCheck className="h-4 w-4" />} title={t('sourceGapActions.title')} subtitle={t('sourceGapActions.subtitle')} />
<div className="mt-4 grid gap-3 lg:grid-cols-2">
{sourceGapCards.map(card => (
<div key={card.work_item_id} className="rounded-[7px] border border-border bg-background p-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-semibold text-foreground">{card.display_name}</p>
<p className="mt-1 break-all text-[11px] leading-5 text-muted-foreground">{card.work_item_id}</p>
</div>
<StatusPill ok={!card.runtime_gate_open} okLabel={t('sourceGapActions.gateClosed')} failLabel={t('sourceGapActions.gateOpen')} />
</div>
<div className="mt-3 grid gap-2 sm:grid-cols-2">
<AssetMiniLine label={t('sourceGapActions.playbook')} value={card.playbook_state} detail={card.playbook_draft_id} />
<AssetMiniLine label={t('sourceGapActions.verifier')} value={card.verifier_state} detail={card.verifier_plan_id} />
<AssetMiniLine label={t('sourceGapActions.script')} value={card.script_state} detail={card.route} />
<AssetMiniLine label={t('sourceGapActions.schedule')} value={card.schedule_state} detail={card.owner_review_required ? t('sourceGapActions.ownerRequired') : t('sourceGapActions.ownerOptional')} />
</div>
<div className="mt-3 grid gap-3 md:grid-cols-2">
<ChecklistBlock title={t('sourceGapActions.fields')} items={card.playbook_template_fields.slice(0, 5)} />
<ChecklistBlock title={t('sourceGapActions.checks')} items={card.verifier_checks.slice(0, 5)} />
</div>
<p className="mt-3 text-xs leading-5 text-muted-foreground">{card.next_action}</p>
</div>
))}
{!sourceGapCards.length && <EmptyPanel label={t('sourceGapActions.empty')} />}
</div>
</section>
<section className="grid gap-4 xl:grid-cols-[1.05fr_0.95fr]">
<div className="rounded-[7px] border border-border bg-card p-4 shadow-sm">
<SectionHeader icon={<Layers3 className="h-4 w-4" />} title={t('funnel.title')} subtitle={t('funnel.subtitle')} />
@@ -699,6 +751,32 @@ function AssetCard({ label, value, detail, danger = false }: { label: string; va
)
}
function AssetMiniLine({ label, value, detail }: { label: string; value: string; detail: string }) {
return (
<div className="min-w-0 rounded-[7px] border border-border bg-card p-2">
<p className="text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">{label}</p>
<p className="mt-1 text-xs font-semibold text-foreground">{value}</p>
<p className="mt-1 break-all text-[10px] leading-4 text-muted-foreground">{detail}</p>
</div>
)
}
function ChecklistBlock({ title, items }: { title: string; items: string[] }) {
return (
<div className="rounded-[7px] border border-border bg-card p-2">
<p className="text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">{title}</p>
<div className="mt-2 grid gap-1">
{items.map(item => (
<div key={item} className="flex min-w-0 items-start gap-1.5 text-[10px] leading-4 text-muted-foreground">
<CheckCircle2 className="mt-0.5 h-3 w-3 flex-shrink-0 text-blue-500" />
<span className="break-words">{item}</span>
</div>
))}
</div>
</div>
)
}
function AgentCard({ agent, chips }: { agent: AgentStatusReport; chips: string[] }) {
const progress = agent.work_units_total > 0 ? Math.round((agent.work_units_done / agent.work_units_total) * 100) : 0
return (