feat(web): 顯示知識庫自動化資產總帳
This commit is contained in:
@@ -1820,6 +1820,34 @@
|
||||
"categoryDistribution": "分類分佈",
|
||||
"categoryOther": "其他分類"
|
||||
},
|
||||
"assetLedger": {
|
||||
"title": "自動化資產沉澱總帳",
|
||||
"subtitle": "把目前 KM 清單與 Hermes owner-review 讀模型,對齊到 PlayBook、腳本、排程、Verifier 五類資產。",
|
||||
"readOnly": "只讀總覽",
|
||||
"boundary": "此區塊只顯示可追蹤沉澱狀態,不代表已授權修復、已寫回 KM 或已提高 PlayBook trust。",
|
||||
"asset": {
|
||||
"km": {
|
||||
"title": "KM 條目",
|
||||
"detail": "可追蹤 {ready} 筆;待刷新 / stale {pending} 筆。"
|
||||
},
|
||||
"playbook": {
|
||||
"title": "PlayBook 關聯",
|
||||
"detail": "已關聯 {ready} 筆;缺 PlayBook {pending} 筆。"
|
||||
},
|
||||
"script": {
|
||||
"title": "腳本 / Ansible",
|
||||
"detail": "含執行或 auto-runbook 證據 {ready} 筆;待補 {pending} 筆。"
|
||||
},
|
||||
"monitoring": {
|
||||
"title": "排程 / 監控規則",
|
||||
"detail": "含告警 / 監控訊號 {ready} 筆;owner review 待處理 {pending} 筆。"
|
||||
},
|
||||
"verifier": {
|
||||
"title": "Verifier 回寫",
|
||||
"detail": "已批准或完成驗證 {ready} 筆;待 owner review {pending} 筆。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quality": {
|
||||
"title": "資料品質軌道",
|
||||
"scope": "目前列表",
|
||||
|
||||
@@ -1820,6 +1820,34 @@
|
||||
"categoryDistribution": "分類分佈",
|
||||
"categoryOther": "其他分類"
|
||||
},
|
||||
"assetLedger": {
|
||||
"title": "自動化資產沉澱總帳",
|
||||
"subtitle": "把目前 KM 清單與 Hermes owner-review 讀模型,對齊到 PlayBook、腳本、排程、Verifier 五類資產。",
|
||||
"readOnly": "只讀總覽",
|
||||
"boundary": "此區塊只顯示可追蹤沉澱狀態,不代表已授權修復、已寫回 KM 或已提高 PlayBook trust。",
|
||||
"asset": {
|
||||
"km": {
|
||||
"title": "KM 條目",
|
||||
"detail": "可追蹤 {ready} 筆;待刷新 / stale {pending} 筆。"
|
||||
},
|
||||
"playbook": {
|
||||
"title": "PlayBook 關聯",
|
||||
"detail": "已關聯 {ready} 筆;缺 PlayBook {pending} 筆。"
|
||||
},
|
||||
"script": {
|
||||
"title": "腳本 / Ansible",
|
||||
"detail": "含執行或 auto-runbook 證據 {ready} 筆;待補 {pending} 筆。"
|
||||
},
|
||||
"monitoring": {
|
||||
"title": "排程 / 監控規則",
|
||||
"detail": "含告警 / 監控訊號 {ready} 筆;owner review 待處理 {pending} 筆。"
|
||||
},
|
||||
"verifier": {
|
||||
"title": "Verifier 回寫",
|
||||
"detail": "已批准或完成驗證 {ready} 筆;待 owner review {pending} 筆。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"quality": {
|
||||
"title": "資料品質軌道",
|
||||
"scope": "目前列表",
|
||||
|
||||
@@ -123,6 +123,8 @@ interface KnowledgeGovernanceTelemetry {
|
||||
completionQueue: KnowledgeStaleOwnerReviewCompletionQueueResponse | null
|
||||
}
|
||||
|
||||
type AutomationAssetKey = 'km' | 'playbook' | 'script' | 'monitoring' | 'verifier'
|
||||
|
||||
// =============================================================================
|
||||
// Category Config
|
||||
// =============================================================================
|
||||
@@ -591,6 +593,104 @@ export default function KnowledgeBasePage({
|
||||
] as const
|
||||
}, [displayedEntries])
|
||||
|
||||
const automationAssetRows = useMemo(() => {
|
||||
const loaded = displayedEntries.length
|
||||
const pct = (count: number) => loaded > 0 ? Math.round((count / loaded) * 100) : 0
|
||||
const countWhere = (predicate: (entry: KnowledgeEntry) => boolean) =>
|
||||
displayedEntries.filter(predicate).length
|
||||
const hasTag = (entry: KnowledgeEntry, candidates: string[]) =>
|
||||
entry.tags.some(tag => candidates.includes(tag))
|
||||
|
||||
const kmReady = displayedEntries.length
|
||||
const playbookReady = countWhere(entry => Boolean(entry.related_playbook_id))
|
||||
const scriptReady = countWhere(entry =>
|
||||
entry.entry_type === 'auto_runbook'
|
||||
|| hasTag(entry, ['execution', 'execution_failed', 'auto_runbook']),
|
||||
)
|
||||
const monitoringReady = countWhere(entry =>
|
||||
['alert_handling', 'flywheel_health', 'host_resource', 'kubernetes'].includes(entry.category)
|
||||
|| hasTag(entry, ['telegram', 'warning', 'critical']),
|
||||
)
|
||||
const verifierReady = countWhere(entry =>
|
||||
entry.status === 'approved'
|
||||
|| hasTag(entry, ['human_approved', 'postmortem']),
|
||||
)
|
||||
const ownerPending =
|
||||
governanceTelemetry.ownerReviews?.total
|
||||
?? governanceTelemetry.completionQueue?.pending_count
|
||||
?? 0
|
||||
const ownerBlocked = governanceTelemetry.completionQueue?.blocked_count ?? 0
|
||||
const staleTotal =
|
||||
governanceTelemetry.staleCandidates?.total_stale
|
||||
?? governanceTelemetry.burnDown?.current_snapshot?.stale_count
|
||||
?? 0
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'km' as AutomationAssetKey,
|
||||
icon: BookOpen,
|
||||
ready: kmReady,
|
||||
pending: staleTotal,
|
||||
pct: loaded > 0 ? 100 : 0,
|
||||
tone: 'border-claw-blue/20 bg-claw-blue/8 text-claw-blue',
|
||||
},
|
||||
{
|
||||
key: 'playbook' as AutomationAssetKey,
|
||||
icon: ClipboardList,
|
||||
ready: playbookReady,
|
||||
pending: Math.max(0, loaded - playbookReady),
|
||||
pct: pct(playbookReady),
|
||||
tone: 'border-status-warning/20 bg-status-warning/10 text-status-warning',
|
||||
},
|
||||
{
|
||||
key: 'script' as AutomationAssetKey,
|
||||
icon: PlayCircle,
|
||||
ready: scriptReady,
|
||||
pending: Math.max(0, loaded - scriptReady),
|
||||
pct: pct(scriptReady),
|
||||
tone: 'border-purple-200 bg-purple-50 text-purple-600',
|
||||
},
|
||||
{
|
||||
key: 'monitoring' as AutomationAssetKey,
|
||||
icon: Eye,
|
||||
ready: monitoringReady,
|
||||
pending: ownerPending + ownerBlocked,
|
||||
pct: pct(monitoringReady),
|
||||
tone: 'border-nothing-gray-200 bg-white text-secondary',
|
||||
},
|
||||
{
|
||||
key: 'verifier' as AutomationAssetKey,
|
||||
icon: CheckCircle2,
|
||||
ready: verifierReady,
|
||||
pending: ownerPending,
|
||||
pct: pct(verifierReady),
|
||||
tone: 'border-status-healthy/20 bg-status-healthy/10 text-status-healthy',
|
||||
},
|
||||
] as const
|
||||
}, [displayedEntries, governanceTelemetry])
|
||||
|
||||
const formatAutomationAssetDetail = useCallback(
|
||||
(key: AutomationAssetKey, ready: number, pending: number) => {
|
||||
const values = {
|
||||
ready: formatCount(ready),
|
||||
pending: formatCount(pending),
|
||||
}
|
||||
switch (key) {
|
||||
case 'km':
|
||||
return t('assetLedger.asset.km.detail', values)
|
||||
case 'playbook':
|
||||
return t('assetLedger.asset.playbook.detail', values)
|
||||
case 'script':
|
||||
return t('assetLedger.asset.script.detail', values)
|
||||
case 'monitoring':
|
||||
return t('assetLedger.asset.monitoring.detail', values)
|
||||
case 'verifier':
|
||||
return t('assetLedger.asset.verifier.detail', values)
|
||||
}
|
||||
},
|
||||
[formatCount, t],
|
||||
)
|
||||
|
||||
const governanceSummary = useMemo(() => {
|
||||
const snapshot = governanceTelemetry.burnDown?.current_snapshot ?? null
|
||||
const ratio = snapshot ? Math.round(snapshot.stale_ratio * 1000) / 10 : null
|
||||
@@ -884,6 +984,55 @@ export default function KnowledgeBasePage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 rounded-md border border-nothing-gray-200 bg-white/70 px-3 py-2">
|
||||
<div className="mb-3 flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-label uppercase tracking-wider text-muted">{t('assetLedger.title')}</p>
|
||||
<p className="mt-0.5 text-[10px] font-body text-muted">{t('assetLedger.subtitle')}</p>
|
||||
</div>
|
||||
<span className="shrink-0 rounded-full border border-status-healthy/20 bg-status-healthy/10 px-2 py-0.5 text-[10px] font-label text-status-healthy">
|
||||
{t('assetLedger.readOnly')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-5">
|
||||
{automationAssetRows.map(row => {
|
||||
const Icon = row.icon
|
||||
return (
|
||||
<div key={row.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-7 w-7 shrink-0 items-center justify-center rounded-md border', row.tone)}>
|
||||
<Icon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</div>
|
||||
<span className="rounded-full border border-nothing-gray-200 bg-white px-2 py-0.5 text-[10px] font-label tabular-nums text-muted">
|
||||
{row.pct}%
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 truncate text-[10px] font-label uppercase tracking-wider text-muted">
|
||||
{t(`assetLedger.asset.${row.key}.title` as never)}
|
||||
</p>
|
||||
<p className="mt-1 text-xl font-heading font-semibold tabular-nums text-primary">
|
||||
{governanceLoading && row.key !== 'playbook' && row.key !== 'script'
|
||||
? '--'
|
||||
: formatCount(row.ready)}
|
||||
</p>
|
||||
<div className="mt-1 h-1.5 overflow-hidden rounded-full bg-nothing-gray-100">
|
||||
<div
|
||||
className="h-full rounded-full bg-claw-blue"
|
||||
style={{ width: `${row.pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-2 text-[10px] font-body leading-4 text-secondary">
|
||||
{formatAutomationAssetDetail(row.key, row.ready, row.pending)}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<p className="mt-2 text-[10px] font-body text-muted">
|
||||
{t('assetLedger.boundary')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 rounded-md border border-nothing-gray-200 bg-white/70 px-3 py-2">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<p className="text-[10px] font-label uppercase tracking-wider text-muted">{t('quality.title')}</p>
|
||||
|
||||
Reference in New Issue
Block a user