diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 09e8b653..c556f45b 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -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": "目前列表", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 09e8b653..c556f45b 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -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": "目前列表", diff --git a/apps/web/src/app/[locale]/knowledge-base/page.tsx b/apps/web/src/app/[locale]/knowledge-base/page.tsx index 206449e8..1af4320d 100644 --- a/apps/web/src/app/[locale]/knowledge-base/page.tsx +++ b/apps/web/src/app/[locale]/knowledge-base/page.tsx @@ -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({ +
{t('assetLedger.title')}
+{t('assetLedger.subtitle')}
++ {t(`assetLedger.asset.${row.key}.title` as never)} +
++ {governanceLoading && row.key !== 'playbook' && row.key !== 'script' + ? '--' + : formatCount(row.ready)} +
++ {formatAutomationAssetDetail(row.key, row.ready, row.pending)} +
++ {t('assetLedger.boundary')} +
+{t('quality.title')}