feat(web): 前移 Observability 自動化資產總帳
This commit is contained in:
@@ -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')}
|
||||
|
||||
Reference in New Issue
Block a user