feat(web): 前移 Knowledge Base 自動化掌控台
All checks were successful
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m37s
CD Pipeline / build-and-deploy (push) Successful in 5m31s
CD Pipeline / post-deploy-checks (push) Successful in 1m43s

This commit is contained in:
Your Name
2026-06-18 17:27:36 +08:00
parent 6e396f3bdb
commit d581f455f7
3 changed files with 137 additions and 0 deletions

View File

@@ -1810,6 +1810,24 @@
"errorDescription": "主知識條目 API 未成功回應:{reason}。下方治理軌道仍會顯示 Hermes owner-review 與陳舊 KM 狀態,避免誤判成知識庫真的歸零。",
"retry": "重新讀取"
},
"decision": {
"badge": "KM 治理警戒",
"readback": "Hermes / AwoooP 只讀回讀",
"title": "KM 自動化掌控台",
"subtitle": "先看 stale ratio、owner review、ready / blocked 與寫回數,再進入條目列表;這裡把 KM、PlayBook、腳本、排程與 Verifier 的沉澱狀態前移到首屏。",
"openWorkItems": "處理 Owner Review",
"card": {
"staleRatio": "Stale Ratio",
"staleRatioSub": "門檻 {threshold}",
"staleTotal": "Stale KM",
"staleTotalSub": "距離門檻需處理 {count}",
"ownerQueue": "Owner Review",
"ownerQueueSub": "ready {ready} / blocked {blocked}",
"writeback": "已寫回",
"writebackSafe": "只讀回讀;讀取不寫入",
"writebackUnsafe": "偵測到讀取寫入風險"
}
},
"overview": {
"metricTotal": "總條目",
"metricLoaded": "目前列表",

View File

@@ -1810,6 +1810,24 @@
"errorDescription": "主知識條目 API 未成功回應:{reason}。下方治理軌道仍會顯示 Hermes owner-review 與陳舊 KM 狀態,避免誤判成知識庫真的歸零。",
"retry": "重新讀取"
},
"decision": {
"badge": "KM 治理警戒",
"readback": "Hermes / AwoooP 只讀回讀",
"title": "KM 自動化掌控台",
"subtitle": "先看 stale ratio、owner review、ready / blocked 與寫回數,再進入條目列表;這裡把 KM、PlayBook、腳本、排程與 Verifier 的沉澱狀態前移到首屏。",
"openWorkItems": "處理 Owner Review",
"card": {
"staleRatio": "Stale Ratio",
"staleRatioSub": "門檻 {threshold}",
"staleTotal": "Stale KM",
"staleTotalSub": "距離門檻需處理 {count}",
"ownerQueue": "Owner Review",
"ownerQueueSub": "ready {ready} / blocked {blocked}",
"writeback": "已寫回",
"writebackSafe": "只讀回讀;讀取不寫入",
"writebackUnsafe": "偵測到讀取寫入風險"
}
},
"overview": {
"metricTotal": "總條目",
"metricLoaded": "目前列表",

View File

@@ -800,6 +800,57 @@ export default function KnowledgeBasePage({
}))
}, [formatCount, governanceSummary, governanceTelemetry.burnDown, t])
const governanceDecisionCards = useMemo(() => {
const ratioAboveThreshold =
governanceSummary.ratio !== null
&& governanceSummary.threshold !== null
&& governanceSummary.ratio > governanceSummary.threshold
const staleRatioValue = governanceSummary.ratio === null ? '--' : `${governanceSummary.ratio}%`
const thresholdText = governanceSummary.threshold === null ? '--' : `${governanceSummary.threshold}%`
const staleTotalValue = governanceSummary.staleTotal === null ? '--' : formatCount(governanceSummary.staleTotal)
const entriesToThreshold = governanceSummary.entriesToThreshold === null ? '--' : formatCount(governanceSummary.entriesToThreshold)
return [
{
key: 'staleRatio',
icon: Clock3,
value: staleRatioValue,
sub: t('decision.card.staleRatioSub', { threshold: thresholdText }),
tone: ratioAboveThreshold
? 'border-status-critical/25 bg-status-critical/10 text-status-critical'
: 'border-status-healthy/25 bg-status-healthy/10 text-status-healthy',
},
{
key: 'staleTotal',
icon: BookOpen,
value: staleTotalValue,
sub: t('decision.card.staleTotalSub', { count: entriesToThreshold }),
tone: 'border-status-warning/25 bg-status-warning/10 text-status-warning',
},
{
key: 'ownerQueue',
icon: Users,
value: formatCount(governanceSummary.ownerPending),
sub: t('decision.card.ownerQueueSub', {
ready: formatCount(governanceSummary.ownerReady),
blocked: formatCount(governanceSummary.ownerBlocked),
}),
tone: 'border-claw-blue/25 bg-claw-blue/8 text-claw-blue',
},
{
key: 'writeback',
icon: Database,
value: formatCount(governanceSummary.completed),
sub: governanceSummary.writesOnRead
? t('decision.card.writebackUnsafe')
: t('decision.card.writebackSafe'),
tone: governanceSummary.writesOnRead
? 'border-status-critical/25 bg-status-critical/10 text-status-critical'
: 'border-status-healthy/25 bg-status-healthy/10 text-status-healthy',
},
] as const
}, [formatCount, governanceSummary, t])
const content = (
<div className="flex min-h-[calc(100vh-64px)] flex-col lg:h-[calc(100vh-64px)] lg:flex-row">
@@ -942,6 +993,56 @@ export default function KnowledgeBasePage({
)}
<section className="border-b border-nothing-gray-200/50 px-4 py-3 bg-white/35">
<div className="mb-3 rounded-md border border-nothing-gray-200 bg-white/80 px-3 py-3">
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="inline-flex items-center gap-1.5 rounded-full border border-status-warning/20 bg-status-warning/10 px-2 py-0.5 text-[10px] font-label uppercase tracking-wider text-status-warning">
<TriangleAlert className="h-3 w-3" aria-hidden="true" />
{t('decision.badge')}
</span>
<span className="text-[10px] font-label uppercase tracking-wider text-muted">
{governanceLoading ? t('workItems.loading') : t('decision.readback')}
</span>
</div>
<h1 className="mt-2 text-xl font-heading font-semibold text-primary">
{t('decision.title')}
</h1>
<p className="mt-1 max-w-4xl text-xs font-body leading-5 text-secondary">
{t('decision.subtitle')}
</p>
</div>
<Link
href={governanceSummary.workItemsHref as never}
className="inline-flex shrink-0 items-center justify-center gap-1.5 rounded-md border border-claw-blue/20 bg-claw-blue/8 px-3 py-1.5 text-xs font-label text-claw-blue transition-colors hover:bg-claw-blue/12"
>
{t('decision.openWorkItems')}
<ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
</Link>
</div>
<div className="mt-3 grid grid-cols-2 gap-2 xl:grid-cols-4">
{governanceDecisionCards.map(card => {
const Icon = card.icon
return (
<div key={card.key} className="rounded-md border border-nothing-gray-200 bg-white/70 p-2">
<div className="flex items-start justify-between gap-2">
<div className={cn('flex h-8 w-8 shrink-0 items-center justify-center rounded-md border', card.tone)}>
<Icon className="h-4 w-4" aria-hidden="true" />
</div>
<span className="truncate text-[10px] font-label uppercase tracking-wider text-muted">
{t(`decision.card.${card.key}` as never)}
</span>
</div>
<p className="mt-2 text-2xl font-heading font-semibold tabular-nums text-primary">
{governanceLoading ? '--' : card.value}
</p>
<p className="mt-0.5 truncate text-[10px] font-body text-muted">{card.sub}</p>
</div>
)
})}
</div>
</div>
<div className="grid grid-cols-1 gap-4 xl:grid-cols-[minmax(0,1fr)_280px]">
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
{[