diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 8b57f659..d6a53f3b 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1418,6 +1418,16 @@ "categoryDistribution": "分類分佈", "categoryOther": "其他分類" }, + "quality": { + "title": "資料品質軌道", + "scope": "目前列表", + "reviewBacklog": "待審核", + "freshWithin7d": "7 天內更新", + "incidentLinked": "事故關聯", + "signalRich": "訊號完整", + "playbookLinked": "Playbook 關聯", + "countOfLoaded": "{count} / {total}" + }, "noResults": "找不到相關知識條目", "createEntry": "新增條目", "viewCount": "瀏覽", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 8b57f659..d6a53f3b 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1418,6 +1418,16 @@ "categoryDistribution": "分類分佈", "categoryOther": "其他分類" }, + "quality": { + "title": "資料品質軌道", + "scope": "目前列表", + "reviewBacklog": "待審核", + "freshWithin7d": "7 天內更新", + "incidentLinked": "事故關聯", + "signalRich": "訊號完整", + "playbookLinked": "Playbook 關聯", + "countOfLoaded": "{count} / {total}" + }, "noResults": "找不到相關知識條目", "createEntry": "新增條目", "viewCount": "瀏覽", diff --git a/apps/web/src/app/[locale]/knowledge-base/page.tsx b/apps/web/src/app/[locale]/knowledge-base/page.tsx index 975140a3..57c28c89 100644 --- a/apps/web/src/app/[locale]/knowledge-base/page.tsx +++ b/apps/web/src/app/[locale]/knowledge-base/page.tsx @@ -390,6 +390,29 @@ export default function KnowledgeBasePage({ ] }, [categories, totalCount]) + const qualityRows = useMemo(() => { + const loaded = displayedEntries.length + const now = Date.now() + const freshWindowMs = 7 * 24 * 60 * 60 * 1000 + const pct = (count: number) => loaded > 0 ? Math.round((count / loaded) * 100) : 0 + const reviewBacklog = displayedEntries.filter(entry => entry.status === 'draft' || entry.status === 'review').length + const freshWithin7d = displayedEntries.filter(entry => { + const updatedAt = Date.parse(entry.updated_at) + return Number.isFinite(updatedAt) && now - updatedAt <= freshWindowMs + }).length + const incidentLinked = displayedEntries.filter(entry => Boolean(entry.related_incident_id)).length + const playbookLinked = displayedEntries.filter(entry => Boolean(entry.related_playbook_id)).length + const signalRich = displayedEntries.filter(entry => getSignalTags(entry).length > 0).length + + return [ + { key: 'reviewBacklog', count: reviewBacklog, pct: pct(reviewBacklog), tone: 'bg-status-warning' }, + { key: 'freshWithin7d', count: freshWithin7d, pct: pct(freshWithin7d), tone: 'bg-status-healthy' }, + { key: 'incidentLinked', count: incidentLinked, pct: pct(incidentLinked), tone: 'bg-claw-blue' }, + { key: 'signalRich', count: signalRich, pct: pct(signalRich), tone: 'bg-purple-500' }, + { key: 'playbookLinked', count: playbookLinked, pct: pct(playbookLinked), tone: 'bg-nothing-gray-400' }, + ] as const + }, [displayedEntries]) + const content = (
{t('quality.title')}
+ {t('quality.scope')} +