feat(web): show source provider freshness on alerts
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
CD Pipeline / tests (push) Successful in 3m55s
CD Pipeline / build-and-deploy (push) Successful in 3m45s
CD Pipeline / post-deploy-checks (push) Successful in 2m25s

This commit is contained in:
Your Name
2026-05-20 16:25:26 +08:00
parent d84bae95cf
commit c2bf579a99
3 changed files with 87 additions and 7 deletions

View File

@@ -747,8 +747,13 @@
"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}"
"sourceCoverageProvider": "{provider}: total {total}, missing {missing}, Sentry {sentry}, SigNoz {signoz}, latest {latest} ({age})",
"sourceCoverageProviderWindow": "{provider} window: total {total}, with refs {withRefs}, missing {missing}, latest {latest} ({age})",
"sourceCoverageFreshness": "{provider} latest {latest} ({age})",
"sourceCoverageFresh": "fresh",
"sourceCoverageStaleHours": "stale {hours}h",
"sourceCoverageStaleDays": "stale {days}d",
"sourceCoverageNoEvents": "no events"
},
"navSection": {
"aiCore": "AI Core",

View File

@@ -748,8 +748,13 @@
"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}"
"sourceCoverageProvider": "{provider}: total {total}, missing {missing}, Sentry {sentry}, SigNoz {signoz}, latest {latest} ({age})",
"sourceCoverageProviderWindow": "{provider} window: total {total}, with refs {withRefs}, missing {missing}, latest {latest} ({age})",
"sourceCoverageFreshness": "{provider} latest {latest} ({age})",
"sourceCoverageFresh": "fresh",
"sourceCoverageStaleHours": "stale {hours}h",
"sourceCoverageStaleDays": "stale {days}d",
"sourceCoverageNoEvents": "no events"
},
"navSection": {
"aiCore": "AI 核心",

View File

@@ -26,6 +26,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 SOURCE_FRESHNESS_STALE_HOURS = 24
const SEVERITY_STYLE: Record<string, { bg: string; text: string }> = {
P0: { bg: 'bg-status-critical/10', text: 'text-status-critical' },
@@ -91,6 +92,7 @@ interface SourceCoverageProvider {
sentry_ref_total: number
signoz_ref_total: number
alert_ref_total: number
latest_received_at?: string | null
}
interface SourceCoverageResponse {
@@ -103,6 +105,38 @@ function percentLabel(value: number, total: number) {
return `${Math.round((value / total) * 100)}%`
}
function parseApiTimestamp(value?: string | null) {
if (!value) return null
const normalized = /(?:z|[+-]\d{2}:?\d{2})$/i.test(value) ? value : `${value}Z`
const date = new Date(normalized)
return Number.isNaN(date.getTime()) ? null : date
}
function formatTimestamp(value: string | null | undefined, locale: string, emptyLabel: string) {
const date = parseApiTimestamp(value)
if (!date) return emptyLabel
return new Intl.DateTimeFormat(locale, {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
}).format(date)
}
function freshnessAgeHours(value?: string | null) {
const date = parseApiTimestamp(value)
if (!date) return null
return Math.max(0, (Date.now() - date.getTime()) / 3_600_000)
}
function freshnessToneClass(ageHours: number | null) {
if (ageHours === null || ageHours > SOURCE_FRESHNESS_STALE_HOURS) {
return 'border-status-warning/30 bg-status-warning/5 text-status-warning'
}
return 'border-status-healthy/30 bg-status-healthy/5 text-status-healthy'
}
// =============================================================================
// Page
// =============================================================================
@@ -206,6 +240,17 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
const topCoverageProviders = sourceCoverage?.providers?.slice(0, 3) ?? []
const sentryCoverageSummary = providerCoverage.sentry?.summary
const signozCoverageSummary = providerCoverage.signoz?.summary
const emptyFreshnessLabel = tAlerts('sourceCoverageNoEvents')
const formatFreshnessAge = (value?: string | null) => {
const ageHours = freshnessAgeHours(value)
if (ageHours === null) return emptyFreshnessLabel
if (ageHours <= SOURCE_FRESHNESS_STALE_HOURS) return tAlerts('sourceCoverageFresh')
if (ageHours < 48) return tAlerts('sourceCoverageStaleHours', { hours: Math.floor(ageHours) })
return tAlerts('sourceCoverageStaleDays', { days: Math.floor(ageHours / 24) })
}
const coverageFreshnessAge = freshnessAgeHours(coverageSummary?.latest_received_at)
const sentryFreshnessAge = freshnessAgeHours(sentryCoverageSummary?.latest_received_at)
const signozFreshnessAge = freshnessAgeHours(signozCoverageSummary?.latest_received_at)
return (
<AppLayout locale={params.locale}>
@@ -289,10 +334,23 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
total: coverageSummary?.source_count ?? 0,
})}
</span>
<span className={cn(
'rounded border px-2 py-1',
freshnessToneClass(coverageFreshnessAge)
)}>
{tAlerts('sourceCoverageFreshness', {
provider: 'overall',
latest: formatTimestamp(coverageSummary?.latest_received_at, params.locale, emptyFreshnessLabel),
age: formatFreshnessAge(coverageSummary?.latest_received_at),
})}
</span>
{topCoverageProviders.map(provider => (
<span
key={provider.provider}
className="rounded border border-nothing-gray-200 bg-nothing-gray-50 px-2 py-1"
className={cn(
'rounded border px-2 py-1',
freshnessToneClass(freshnessAgeHours(provider.latest_received_at))
)}
>
{tAlerts('sourceCoverageProvider', {
provider: provider.provider,
@@ -300,23 +358,35 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
missing: provider.missing_source_refs_total,
sentry: provider.sentry_ref_total,
signoz: provider.signoz_ref_total,
latest: formatTimestamp(provider.latest_received_at, params.locale, emptyFreshnessLabel),
age: formatFreshnessAge(provider.latest_received_at),
})}
</span>
))}
<span className="rounded border border-nothing-gray-200 bg-nothing-gray-50 px-2 py-1">
<span className={cn(
'rounded border px-2 py-1',
freshnessToneClass(sentryFreshnessAge)
)}>
{tAlerts('sourceCoverageProviderWindow', {
provider: 'sentry',
total: sentryCoverageSummary?.source_count ?? 0,
withRefs: sentryCoverageSummary?.with_source_refs_total ?? 0,
missing: sentryCoverageSummary?.missing_source_refs_total ?? 0,
latest: formatTimestamp(sentryCoverageSummary?.latest_received_at, params.locale, emptyFreshnessLabel),
age: formatFreshnessAge(sentryCoverageSummary?.latest_received_at),
})}
</span>
<span className="rounded border border-nothing-gray-200 bg-nothing-gray-50 px-2 py-1">
<span className={cn(
'rounded border px-2 py-1',
freshnessToneClass(signozFreshnessAge)
)}>
{tAlerts('sourceCoverageProviderWindow', {
provider: 'signoz',
total: signozCoverageSummary?.source_count ?? 0,
withRefs: signozCoverageSummary?.with_source_refs_total ?? 0,
missing: signozCoverageSummary?.missing_source_refs_total ?? 0,
latest: formatTimestamp(signozCoverageSummary?.latest_received_at, params.locale, emptyFreshnessLabel),
age: formatFreshnessAge(signozCoverageSummary?.latest_received_at),
})}
</span>
</div>