feat(web): show source dossier coverage on alerts
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Successful in 3m51s
CD Pipeline / build-and-deploy (push) Successful in 3m32s
CD Pipeline / post-deploy-checks (push) Successful in 1m40s

This commit is contained in:
Your Name
2026-05-20 16:05:01 +08:00
parent 31a49c72de
commit 49ad1cfb1a
3 changed files with 190 additions and 2 deletions

View File

@@ -737,7 +737,18 @@
"statusChainWindow": "AI flow evidence: {loaded}/{shown} on this page connected to truth-chain",
"previousPage": "Previous",
"nextPage": "Next",
"pageIndicator": "Page {page} of {totalPages}"
"pageIndicator": "Page {page} of {totalPages}",
"sourceCoverageTitle": "Source Dossier Coverage",
"sourceCoverageSubtitle": "DB persistence and Sentry / SigNoz references across the latest {limit} inbound source events",
"sourceCoverageError": "Source dossier coverage failed to load: {error}",
"sourceCoverageWithRefs": "With refs",
"sourceCoverageMissing": "Missing refs",
"sourceCoverageAlert": "Alert refs",
"sourceCoverageSentry": "Sentry refs",
"sourceCoverageSigNoz": "SigNoz refs",
"sourceCoverageRatio": "source refs coverage {ratio} / total {total}",
"sourceCoverageProvider": "{provider}: total {total}, missing {missing}, Sentry {sentry}, SigNoz {signoz}",
"sourceCoverageProviderWindow": "{provider} window: total {total}, with refs {withRefs}, missing {missing}"
},
"navSection": {
"aiCore": "AI Core",

View File

@@ -738,7 +738,18 @@
"statusChainWindow": "AI 流程證據:本頁 {loaded}/{shown} 筆已接上 truth-chain",
"previousPage": "上一頁",
"nextPage": "下一頁",
"pageIndicator": "第 {page} / {totalPages} 頁"
"pageIndicator": "第 {page} / {totalPages} 頁",
"sourceCoverageTitle": "來源卷宗覆蓋率",
"sourceCoverageSubtitle": "最近 {limit} 筆 inbound source event 的 DB 保存與 Sentry / SigNoz 關聯",
"sourceCoverageError": "來源卷宗覆蓋率讀取失敗:{error}",
"sourceCoverageWithRefs": "含 refs",
"sourceCoverageMissing": "缺 refs",
"sourceCoverageAlert": "Alert refs",
"sourceCoverageSentry": "Sentry refs",
"sourceCoverageSigNoz": "SigNoz refs",
"sourceCoverageRatio": "source refs 覆蓋率 {ratio} / total {total}",
"sourceCoverageProvider": "{provider}: total {total}, missing {missing}, Sentry {sentry}, SigNoz {signoz}",
"sourceCoverageProviderWindow": "{provider} window: total {total}, with refs {withRefs}, missing {missing}"
},
"navSection": {
"aiCore": "AI 核心",

View File

@@ -16,6 +16,7 @@ import { useIncidents } from '@/hooks/useIncidents'
import { useIncidentStatusChains } from '@/hooks/useIncidentStatusChains'
import { IncidentCard } from '@/components/incident'
import { cn } from '@/lib/utils'
import { API_V1_URL } from '@/lib/config'
import { Bell, BellOff, RefreshCw, AlertTriangle, AlertCircle, Info, ChevronLeft, ChevronRight } from 'lucide-react'
// =============================================================================
@@ -24,6 +25,7 @@ import { Bell, BellOff, RefreshCw, AlertTriangle, AlertCircle, Info, ChevronLeft
const SEVERITY_ORDER: Record<string, number> = { P0: 0, P1: 1, P2: 2, P3: 3 }
const ALERTS_PAGE_SIZE = 50
const SOURCE_COVERAGE_LIMIT = 100
const SEVERITY_STYLE: Record<string, { bg: string; text: string }> = {
P0: { bg: 'bg-status-critical/10', text: 'text-status-critical' },
@@ -72,6 +74,35 @@ function StatPill({ label, value, highlight }: { label: string; value: number; h
)
}
interface SourceCoverageSummary {
source_count: number
with_source_refs_total: number
missing_source_refs_total: number
sentry_ref_total: number
signoz_ref_total: number
alert_ref_total: number
latest_received_at?: string | null
}
interface SourceCoverageProvider {
provider: string
total: number
missing_source_refs_total: number
sentry_ref_total: number
signoz_ref_total: number
alert_ref_total: number
}
interface SourceCoverageResponse {
summary: SourceCoverageSummary
providers: SourceCoverageProvider[]
}
function percentLabel(value: number, total: number) {
if (total <= 0) return '--'
return `${Math.round((value / total) * 100)}%`
}
// =============================================================================
// Page
// =============================================================================
@@ -84,6 +115,13 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
pollInterval: 15000,
enablePolling: true,
})
const [sourceCoverage, setSourceCoverage] = useState<SourceCoverageResponse | null>(null)
const [providerCoverage, setProviderCoverage] = useState<Record<string, SourceCoverageResponse | null>>({
sentry: null,
signoz: null,
})
const [sourceCoverageError, setSourceCoverageError] = useState<string | null>(null)
const [sourceCoverageLoading, setSourceCoverageLoading] = useState(false)
const sorted = useMemo(() => {
return [...(incidents ?? [])].sort(
@@ -115,10 +153,59 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
return Boolean(chain?.source_id && !chain.fetch_error)
}).length
useEffect(() => {
let active = true
const controller = new AbortController()
async function loadSourceCoverage() {
setSourceCoverageLoading(true)
setSourceCoverageError(null)
try {
const buildCoverageUrl = (provider?: string) => {
const params = new URLSearchParams()
params.set('project_id', 'awoooi')
params.set('limit', String(SOURCE_COVERAGE_LIMIT))
if (provider) params.set('provider', provider)
return `${API_V1_URL}/platform/events/dossier/coverage?${params.toString()}`
}
const [overallResponse, sentryResponse, signozResponse] = await Promise.all([
fetch(buildCoverageUrl(), { signal: controller.signal }),
fetch(buildCoverageUrl('sentry'), { signal: controller.signal }),
fetch(buildCoverageUrl('signoz'), { signal: controller.signal }),
])
if (!overallResponse.ok) throw new Error(`overall HTTP ${overallResponse.status}`)
if (!sentryResponse.ok) throw new Error(`sentry HTTP ${sentryResponse.status}`)
if (!signozResponse.ok) throw new Error(`signoz HTTP ${signozResponse.status}`)
const [overallData, sentryData, signozData] = await Promise.all([
overallResponse.json() as Promise<SourceCoverageResponse>,
sentryResponse.json() as Promise<SourceCoverageResponse>,
signozResponse.json() as Promise<SourceCoverageResponse>,
])
if (active) {
setSourceCoverage(overallData)
setProviderCoverage({ sentry: sentryData, signoz: signozData })
}
} catch (err) {
if (controller.signal.aborted) return
if (active) setSourceCoverageError(err instanceof Error ? err.message : String(err))
} finally {
if (active) setSourceCoverageLoading(false)
}
}
loadSourceCoverage()
return () => {
active = false
controller.abort()
}
}, [lastUpdated])
const p0Count = sorted.filter(i => i.severity === 'P0').length
const p1Count = sorted.filter(i => i.severity === 'P1').length
const p2Count = sorted.filter(i => i.severity === 'P2').length
const p3Count = sorted.filter(i => i.severity === 'P3').length
const coverageSummary = sourceCoverage?.summary
const topCoverageProviders = sourceCoverage?.providers?.slice(0, 3) ?? []
const sentryCoverageSummary = providerCoverage.sentry?.summary
const signozCoverageSummary = providerCoverage.signoz?.summary
return (
<AppLayout locale={params.locale}>
@@ -156,6 +243,85 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
<StatPill label={t('incident.severity.P3')} value={p3Count} />
</div>
<div className="mb-6 rounded-lg border border-nothing-gray-200 bg-white px-4 py-3">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<p className="font-body text-sm font-semibold text-nothing-black">
{tAlerts('sourceCoverageTitle')}
</p>
<p className="mt-1 font-body text-xs text-nothing-gray-500">
{sourceCoverageError
? tAlerts('sourceCoverageError', { error: sourceCoverageError })
: tAlerts('sourceCoverageSubtitle', { limit: SOURCE_COVERAGE_LIMIT })}
{sourceCoverageLoading ? ` · ${t('common.loading')}` : ''}
</p>
</div>
<div className="grid grid-cols-2 gap-2 md:grid-cols-5">
<StatPill
label={tAlerts('sourceCoverageWithRefs')}
value={coverageSummary?.with_source_refs_total ?? 0}
/>
<StatPill
label={tAlerts('sourceCoverageMissing')}
value={coverageSummary?.missing_source_refs_total ?? 0}
highlight
/>
<StatPill
label={tAlerts('sourceCoverageAlert')}
value={coverageSummary?.alert_ref_total ?? 0}
/>
<StatPill
label={tAlerts('sourceCoverageSentry')}
value={coverageSummary?.sentry_ref_total ?? 0}
highlight={(coverageSummary?.source_count ?? 0) > 0 && (coverageSummary?.sentry_ref_total ?? 0) === 0}
/>
<StatPill
label={tAlerts('sourceCoverageSigNoz')}
value={coverageSummary?.signoz_ref_total ?? 0}
highlight={(coverageSummary?.source_count ?? 0) > 0 && (coverageSummary?.signoz_ref_total ?? 0) === 0}
/>
</div>
</div>
<div className="mt-3 flex flex-wrap gap-2 font-body text-xs text-nothing-gray-500">
<span className="rounded border border-nothing-gray-200 bg-nothing-gray-50 px-2 py-1">
{tAlerts('sourceCoverageRatio', {
ratio: percentLabel(coverageSummary?.with_source_refs_total ?? 0, coverageSummary?.source_count ?? 0),
total: coverageSummary?.source_count ?? 0,
})}
</span>
{topCoverageProviders.map(provider => (
<span
key={provider.provider}
className="rounded border border-nothing-gray-200 bg-nothing-gray-50 px-2 py-1"
>
{tAlerts('sourceCoverageProvider', {
provider: provider.provider,
total: provider.total,
missing: provider.missing_source_refs_total,
sentry: provider.sentry_ref_total,
signoz: provider.signoz_ref_total,
})}
</span>
))}
<span className="rounded border border-nothing-gray-200 bg-nothing-gray-50 px-2 py-1">
{tAlerts('sourceCoverageProviderWindow', {
provider: 'sentry',
total: sentryCoverageSummary?.source_count ?? 0,
withRefs: sentryCoverageSummary?.with_source_refs_total ?? 0,
missing: sentryCoverageSummary?.missing_source_refs_total ?? 0,
})}
</span>
<span className="rounded border border-nothing-gray-200 bg-nothing-gray-50 px-2 py-1">
{tAlerts('sourceCoverageProviderWindow', {
provider: 'signoz',
total: signozCoverageSummary?.source_count ?? 0,
withRefs: signozCoverageSummary?.with_source_refs_total ?? 0,
missing: signozCoverageSummary?.missing_source_refs_total ?? 0,
})}
</span>
</div>
</div>
{sorted.length > 0 && (
<div className="mb-6 flex flex-wrap items-center justify-between gap-3 rounded-lg border border-nothing-gray-200 bg-white px-4 py-3">
<div>