fix(web): prevent knowledge category key leaks
This commit is contained in:
@@ -1415,7 +1415,8 @@
|
||||
"metricApproved": "已批准",
|
||||
"scopeFiltered": "目前篩選",
|
||||
"scopeCurrent": "已載入",
|
||||
"categoryDistribution": "分類分佈"
|
||||
"categoryDistribution": "分類分佈",
|
||||
"categoryOther": "其他分類"
|
||||
},
|
||||
"noResults": "找不到相關知識條目",
|
||||
"createEntry": "新增條目",
|
||||
@@ -1446,10 +1447,24 @@
|
||||
"human": "人工建立"
|
||||
},
|
||||
"category": {
|
||||
"AI治理": "AI 治理",
|
||||
"alert_handling": "告警處理",
|
||||
"infrastructure": "基礎設施",
|
||||
"application": "應用層",
|
||||
"ai_system": "AI 系統",
|
||||
"security": "安全 / 合規"
|
||||
"security": "安全 / 合規",
|
||||
"general": "通用",
|
||||
"host_resource": "主機資源",
|
||||
"database": "資料庫",
|
||||
"auto_repair": "自動修復",
|
||||
"postmortem": "事後分析",
|
||||
"external_site": "外部網站",
|
||||
"backup_failure": "備份失敗",
|
||||
"kubernetes": "Kubernetes",
|
||||
"flywheel_health": "飛輪健康",
|
||||
"ssl_cert": "SSL 憑證",
|
||||
"devops_tool": "DevOps 工具",
|
||||
"incident_postmortem": "事故檢討"
|
||||
},
|
||||
"filterByType": "篩選類型",
|
||||
"filterByStatus": "篩選狀態",
|
||||
|
||||
@@ -1415,7 +1415,8 @@
|
||||
"metricApproved": "已批准",
|
||||
"scopeFiltered": "目前篩選",
|
||||
"scopeCurrent": "已載入",
|
||||
"categoryDistribution": "分類分佈"
|
||||
"categoryDistribution": "分類分佈",
|
||||
"categoryOther": "其他分類"
|
||||
},
|
||||
"noResults": "找不到相關知識條目",
|
||||
"createEntry": "新增條目",
|
||||
@@ -1446,10 +1447,24 @@
|
||||
"human": "人工建立"
|
||||
},
|
||||
"category": {
|
||||
"AI治理": "AI 治理",
|
||||
"alert_handling": "告警處理",
|
||||
"infrastructure": "基礎設施",
|
||||
"application": "應用層",
|
||||
"ai_system": "AI 系統",
|
||||
"security": "安全 / 合規"
|
||||
"security": "安全 / 合規",
|
||||
"general": "通用",
|
||||
"host_resource": "主機資源",
|
||||
"database": "資料庫",
|
||||
"auto_repair": "自動修復",
|
||||
"postmortem": "事後分析",
|
||||
"external_site": "外部網站",
|
||||
"backup_failure": "備份失敗",
|
||||
"kubernetes": "Kubernetes",
|
||||
"flywheel_health": "飛輪健康",
|
||||
"ssl_cert": "SSL 憑證",
|
||||
"devops_tool": "DevOps 工具",
|
||||
"incident_postmortem": "事故檢討"
|
||||
},
|
||||
"filterByType": "篩選類型",
|
||||
"filterByStatus": "篩選狀態",
|
||||
|
||||
@@ -67,6 +67,28 @@ const CATEGORY_ICONS: Record<string, React.ReactNode> = {
|
||||
}
|
||||
|
||||
const CATEGORIES = ['infrastructure', 'application', 'ai_system', 'security'] as const
|
||||
const KNOWN_CATEGORY_KEYS = new Set([
|
||||
'AI治理',
|
||||
'alert_handling',
|
||||
'application',
|
||||
'ai_system',
|
||||
'auto_repair',
|
||||
'backup_failure',
|
||||
'database',
|
||||
'devops_tool',
|
||||
'external_site',
|
||||
'flywheel_health',
|
||||
'general',
|
||||
'host_resource',
|
||||
'incident_postmortem',
|
||||
'infrastructure',
|
||||
'kubernetes',
|
||||
'postmortem',
|
||||
'security',
|
||||
'ssl_cert',
|
||||
])
|
||||
|
||||
const CATEGORY_OVERVIEW_LIMIT = 8
|
||||
|
||||
// =============================================================================
|
||||
// Type Badge Colors
|
||||
@@ -89,10 +111,28 @@ const STATUS_COLORS: Record<string, string> = {
|
||||
}
|
||||
|
||||
const CATEGORY_BAR_COLORS: Record<string, string> = {
|
||||
AI治理: 'bg-claw-blue',
|
||||
alert_handling: 'bg-status-warning',
|
||||
infrastructure: 'bg-claw-blue',
|
||||
application: 'bg-status-healthy',
|
||||
ai_system: 'bg-purple-500',
|
||||
security: 'bg-status-warning',
|
||||
general: 'bg-nothing-gray-400',
|
||||
postmortem: 'bg-status-critical',
|
||||
__other__: 'bg-nothing-gray-300',
|
||||
}
|
||||
|
||||
const humanizeCategory = (category: string) => {
|
||||
if (!category.includes('_')) return category
|
||||
return category
|
||||
.split('_')
|
||||
.filter(Boolean)
|
||||
.map(part => {
|
||||
const upper = part.toUpperCase()
|
||||
if (['AI', 'DB', 'SSL', 'SLO', 'API'].includes(upper)) return upper
|
||||
return `${part.charAt(0).toUpperCase()}${part.slice(1)}`
|
||||
})
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -227,6 +267,15 @@ export default function KnowledgeBasePage({
|
||||
(value: number) => value.toLocaleString(localeCode),
|
||||
[localeCode],
|
||||
)
|
||||
const formatCategoryLabel = useCallback(
|
||||
(category: string) => {
|
||||
if (KNOWN_CATEGORY_KEYS.has(category)) {
|
||||
return t(`category.${category}`)
|
||||
}
|
||||
return humanizeCategory(category)
|
||||
},
|
||||
[t],
|
||||
)
|
||||
|
||||
const visibleSummary = useMemo(() => {
|
||||
const loaded = displayedEntries.length
|
||||
@@ -236,13 +285,30 @@ export default function KnowledgeBasePage({
|
||||
return { loaded, aiExtracted, approved, approvedRate }
|
||||
}, [displayedEntries])
|
||||
|
||||
const categoryRows = useMemo(() => (
|
||||
CATEGORIES.map(category => {
|
||||
const count = categories.find(c => c.category === category)?.count ?? 0
|
||||
const pct = totalCount > 0 ? Math.round((count / totalCount) * 100) : 0
|
||||
return { category, count, pct }
|
||||
const categoryRows = useMemo(() => {
|
||||
const sourceRows = categories.length > 0
|
||||
? [...categories].sort((a, b) => b.count - a.count)
|
||||
: CATEGORIES.map(category => ({ category, count: 0 }))
|
||||
|
||||
const visibleRows = sourceRows.slice(0, CATEGORY_OVERVIEW_LIMIT).map(row => {
|
||||
const pct = totalCount > 0 ? Math.round((row.count / totalCount) * 100) : 0
|
||||
return { category: row.category, count: row.count, pct }
|
||||
})
|
||||
), [categories, totalCount])
|
||||
const remainingCount = sourceRows
|
||||
.slice(CATEGORY_OVERVIEW_LIMIT)
|
||||
.reduce((sum, row) => sum + row.count, 0)
|
||||
|
||||
if (remainingCount <= 0) return visibleRows
|
||||
|
||||
return [
|
||||
...visibleRows,
|
||||
{
|
||||
category: '__other__',
|
||||
count: remainingCount,
|
||||
pct: totalCount > 0 ? Math.round((remainingCount / totalCount) * 100) : 0,
|
||||
},
|
||||
]
|
||||
}, [categories, totalCount])
|
||||
|
||||
const content = (
|
||||
<div className="flex h-[calc(100vh-64px)] flex-col lg:flex-row">
|
||||
@@ -284,7 +350,7 @@ export default function KnowledgeBasePage({
|
||||
)}
|
||||
>
|
||||
{CATEGORY_ICONS[cat]}
|
||||
<span className="flex-1 text-left">{t(`category.${cat}`)}</span>
|
||||
<span className="flex-1 text-left">{formatCategoryLabel(cat)}</span>
|
||||
<span className="text-xs text-muted">{count}</span>
|
||||
</button>
|
||||
)
|
||||
@@ -380,8 +446,10 @@ export default function KnowledgeBasePage({
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{categoryRows.map(row => (
|
||||
<div key={row.category} className="grid grid-cols-[88px_1fr_42px] items-center gap-2">
|
||||
<span className="truncate text-[10px] font-body text-secondary">{t(`category.${row.category}`)}</span>
|
||||
<div key={row.category} className="grid grid-cols-[96px_1fr_42px] items-center gap-2">
|
||||
<span className="truncate text-[10px] font-body text-secondary">
|
||||
{row.category === '__other__' ? t('overview.categoryOther') : formatCategoryLabel(row.category)}
|
||||
</span>
|
||||
<div className="h-1.5 overflow-hidden rounded-full bg-nothing-gray-100">
|
||||
<div
|
||||
className={cn('h-full rounded-full', CATEGORY_BAR_COLORS[row.category] ?? 'bg-nothing-gray-300')}
|
||||
@@ -452,7 +520,7 @@ export default function KnowledgeBasePage({
|
||||
|
||||
{/* Meta row */}
|
||||
<div className="flex items-center gap-2 text-xs text-muted font-body">
|
||||
<span>{t(`category.${entry.category}`)}</span>
|
||||
<span>{formatCategoryLabel(entry.category)}</span>
|
||||
<span>·</span>
|
||||
<span>{t(`source.${entry.source}`)}</span>
|
||||
<span>·</span>
|
||||
@@ -529,7 +597,7 @@ export default function KnowledgeBasePage({
|
||||
|
||||
{/* Meta */}
|
||||
<div className="flex items-center gap-3 text-xs text-muted font-body mb-4 pb-3 border-b border-nothing-gray-100">
|
||||
<span>{t(`category.${selectedEntry.category}`)}</span>
|
||||
<span>{formatCategoryLabel(selectedEntry.category)}</span>
|
||||
<span>·</span>
|
||||
<span>{t(`source.${selectedEntry.source}`)}</span>
|
||||
<span>·</span>
|
||||
|
||||
Reference in New Issue
Block a user