feat(web): 前移 Observability 自動化資產總帳
All checks were successful
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m33s
CD Pipeline / build-and-deploy (push) Successful in 5m24s
CD Pipeline / post-deploy-checks (push) Successful in 1m36s

This commit is contained in:
Your Name
2026-06-18 17:42:27 +08:00
parent 17df979741
commit d411b2a4ea
3 changed files with 201 additions and 14 deletions

View File

@@ -169,6 +169,43 @@ function MiniBar({ value, tone = 'ok' }: { value: number; tone?: Tone }) {
)
}
function AssetLedgerCard({
label,
value,
detail,
meta,
Icon,
tone,
progress,
}: {
label: string
value: ReactNode
detail: string
meta: string
Icon: LucideIcon
tone: Tone
progress: number
}) {
return (
<div className="min-w-0 border border-[#e0ddd4] bg-white p-4">
<div className="flex min-w-0 items-start justify-between gap-3">
<div className="min-w-0">
<p className="truncate text-xs font-semibold uppercase text-[#87867f]">{label}</p>
<p className="mt-2 font-['Syne'] text-3xl font-bold leading-none text-[#141413]">{value}</p>
</div>
<div className={cn('flex h-9 w-9 shrink-0 items-center justify-center border', toneClass(tone))}>
<Icon className="h-5 w-5" />
</div>
</div>
<p className="mt-3 min-h-[42px] text-sm leading-5 text-[#5f5d57]">{detail}</p>
<div className="mt-4">
<MiniBar value={progress} tone={tone} />
<p className="mt-2 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-xs text-[#87867f]">{meta}</p>
</div>
</div>
)
}
export default function ObservabilityPage({ params }: { params: { locale: string } }) {
const nav = useTranslations('nav')
const t = useTranslations('observabilityCommand')
@@ -348,6 +385,97 @@ export default function ObservabilityPage({ params }: { params: { locale: string
},
]
const assetLedgerRows = [
{
key: 'scope',
label: t('assetLedger.scopeLabel'),
value: formatNumber(state.deployment?.rollups.total_targets),
detail: t('assetLedger.scopeDetail', {
hosts: state.deployment?.rollups.by_domain.hosts ?? 0,
projects: state.deployment?.rollups.by_domain.projects ?? 0,
websites: state.deployment?.rollups.by_domain.websites ?? 0,
}),
meta: t('assetLedger.scopeMeta', {
domains: state.deployment?.domains.length ?? 0,
approval: state.deployment?.rollups.approval_required_target_ids.length ?? 0,
}),
Icon: Network,
tone: 'ok' as Tone,
progress: decisionCoverage,
},
{
key: 'signals',
label: t('assetLedger.signalLabel'),
value: formatNumber(state.observability?.rollups.total_surfaces),
detail: t('assetLedger.signalDetail', {
gaps: state.observability?.rollups.classification_gap_ids.length ?? 0,
noise: state.observability?.rollups.noise_reduction_opportunities_total ?? 0,
}),
meta: t('assetLedger.signalMeta', {
required: state.observability?.rollups.surface_ids_requiring_action.length ?? 0,
approval: state.observability?.rollups.approval_required_opportunity_ids.length ?? 0,
}),
Icon: Activity,
tone: (state.observability?.rollups.surface_ids_requiring_action.length ?? 0) > 0 ? 'warn' as Tone : 'ok' as Tone,
progress: state.observability?.program_status.overall_completion_percent ?? 0,
},
{
key: 'health',
label: t('assetLedger.healthLabel'),
value: formatNumber(state.serviceHealth?.rollups.total_targets),
detail: t('assetLedger.healthDetail', {
health: state.serviceHealth?.rollups.health_gap_ids.length ?? 0,
stale: state.serviceHealth?.rollups.stale_endpoint_ids.length ?? 0,
}),
meta: t('assetLedger.healthMeta', {
restart: state.serviceHealth?.rollups.service_restart_allowed_count ?? 0,
notify: state.serviceHealth?.rollups.notification_send_allowed_count ?? 0,
}),
Icon: Workflow,
tone: (state.serviceHealth?.rollups.target_ids_requiring_action.length ?? 0) > 0 ? 'warn' as Tone : 'ok' as Tone,
progress: state.serviceHealth?.program_status.overall_completion_percent ?? 0,
},
{
key: 'learning',
label: t('assetLedger.learningLabel'),
value: `${state.inventory?.program_status.overall_completion_percent ?? 0}%`,
detail: t('assetLedger.learningDetail', {
assets: state.inventory?.assets.length ?? 0,
tasks: state.inventory?.tasks.length ?? 0,
}),
meta: t('assetLedger.learningMeta', {
approval: state.inventory?.task_approval_boundary_rollup.tasks_requiring_explicit_approval.length ?? 0,
blocked: state.inventory?.task_approval_boundary_rollup.tasks_with_blocked_operations.length ?? 0,
}),
Icon: CircuitBoard,
tone: 'warn' as Tone,
progress: state.inventory?.program_status.overall_completion_percent ?? 0,
},
{
key: 'sre',
label: t('assetLedger.sreLabel'),
value: state.deployment?.telegram_contract.primary_gateway ? '1' : '0',
detail: t('assetLedger.sreDetail', {
action: state.deployment?.rollups.by_telegram_policy.action_required ?? 0,
approval: state.deployment?.rollups.by_telegram_policy.approval_required ?? 0,
}),
meta: t('assetLedger.sreMeta'),
Icon: BellRing,
tone: state.deployment?.telegram_contract.primary_gateway ? 'ok' as Tone : 'warn' as Tone,
progress: state.deployment?.telegram_contract.primary_gateway ? 100 : 0,
},
{
key: 'runtime',
label: t('assetLedger.runtimeLabel'),
value: formatNumber(runtimeGateCount),
detail: t('assetLedger.runtimeDetail'),
meta: t('assetLedger.runtimeMeta'),
Icon: Lock,
tone: 'danger' as Tone,
progress: 0,
},
]
const tabs: TabConfig[] = [
{
id: 'monitoring',
@@ -444,6 +572,19 @@ export default function ObservabilityPage({ params }: { params: { locale: string
</section>
<div className="mx-auto flex max-w-[1600px] flex-col gap-6 px-4 py-6 md:px-8">
<section className="border-y border-[#e0ddd4] bg-transparent py-2">
<SectionHeader
eyebrow={t('sections.assetLedgerEyebrow')}
title={t('sections.assetLedgerTitle')}
detail={t('sections.assetLedgerDetail')}
/>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{assetLedgerRows.map(row => (
<AssetLedgerCard key={row.key} {...row} />
))}
</div>
</section>
<section className="border-y border-[#e0ddd4] bg-transparent py-2">
<SectionHeader
eyebrow={t('sections.scopeEyebrow')}