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

@@ -101,28 +101,51 @@
"runtimeGateDetail": "重啟、reload、發通知、改 endpoint、讀 secret 與套用修復入口維持 0。"
},
"sections": {
"scopeEyebrow": "01 / Scope Matrix",
"assetLedgerEyebrow": "01 / Asset Ledger",
"assetLedgerTitle": "AI 自動化資產與訊號總帳",
"assetLedgerDetail": "先用一張總帳回答哪些主機、專案、網站、服務、監控訊號、KM / PlayBook / Verifier 與 SRE 路由已納管,哪些仍卡在批准或只讀 gate。",
"scopeEyebrow": "02 / Scope Matrix",
"scopeTitle": "全域範圍矩陣",
"scopeDetail": "用同一個矩陣把主機、專案、網站前後台、服務、套件、工具與學習鏈路納入;每一格都顯示可判讀程度與仍需批准的範圍。",
"topologyEyebrow": "02 / Topology",
"topologyEyebrow": "03 / Topology",
"topologyTitle": "訊號拓樸與 AI 接管路徑",
"topologyDetail": "從主機與產品面開始接到服務健康、監控訊號、AwoooI SRE 戰情室與 AI Agent 決策鏈,讓告警來源和下一步清楚可追。",
"flowEyebrow": "03 / Flow",
"flowEyebrow": "04 / Flow",
"flowTitle": "告警到處置的流程視圖",
"flowDetail": "用流程呈現收集、關聯、分類、路由與批准閘門;避免週報只有 0也避免批准後不知道下一步。",
"signalEyebrow": "04 / Signal Contracts",
"signalEyebrow": "05 / Signal Contracts",
"signalTitle": "監控合約與降噪候選",
"signalDetail": "Prometheus、Alertmanager、Grafana、SigNoz、Sentry、OTEL 等訊號只讀呈現;降噪與規則調整先形成 proposal。",
"gapEyebrow": "05 / Health Gaps",
"gapEyebrow": "06 / Health Gaps",
"gapTitle": "健康缺口與過期端點",
"gapDetail": "把服務健康、端點 truth drift、runner 證明與 provider 來源缺口整理成 SRE 可判斷的處置焦點。",
"boundaryEyebrow": "06 / Guardrails",
"boundaryEyebrow": "07 / Guardrails",
"boundaryTitle": "不可誤讀合約",
"boundaryDetail": "此頁是監控與決策視圖,不是 runtime 授權;任何會改正式環境的動作仍需獨立批准與驗證。",
"drilldownEyebrow": "07 / Drilldown",
"drilldownEyebrow": "08 / Drilldown",
"drilldownTitle": "細節分頁",
"drilldownDetail": "保留原本監控、APM、錯誤、應用與服務目錄作為總圖後的細節查證入口。"
},
"assetLedger": {
"scopeLabel": "全域資產",
"scopeDetail": "主機 {hosts}、專案 {projects}、網站前後台 {websites} 已進同一張納管總帳。",
"scopeMeta": "{domains} 類 domain需批准 {approval}",
"signalLabel": "監控訊號",
"signalDetail": "訊號合約含分類缺口 {gaps}、降噪候選 {noise};先產生 proposal。",
"signalMeta": "需處置 {required};需批准 {approval}",
"healthLabel": "服務健康",
"healthDetail": "健康缺口 {health}、過期端點 {stale};不把心跳當成功。",
"healthMeta": "重啟允許 {restart};通知允許 {notify}",
"learningLabel": "KM / PlayBook / Verifier",
"learningDetail": "自動化資產 {assets}、任務 {tasks};用 owner gate 控制沉澱與回寫。",
"learningMeta": "需明確批准 {approval};阻擋操作 {blocked}",
"sreLabel": "SRE 戰情室",
"sreDetail": "action-required {action}、approval-required {approval} 才進集中路由。",
"sreMeta": "目標AwoooI SRE 戰情室,其他路由需例外批准",
"runtimeLabel": "Runtime Gate",
"runtimeDetail": "發通知、reload、restart、endpoint change、secret read 與修復套用仍鎖住。",
"runtimeMeta": "只讀顯示;不代表自動修復已授權"
},
"scope": {
"targets": "{count} 個目標",
"approval": "需批准 {count}",

View File

@@ -101,28 +101,51 @@
"runtimeGateDetail": "重啟、reload、發通知、改 endpoint、讀 secret 與套用修復入口維持 0。"
},
"sections": {
"scopeEyebrow": "01 / Scope Matrix",
"assetLedgerEyebrow": "01 / Asset Ledger",
"assetLedgerTitle": "AI 自動化資產與訊號總帳",
"assetLedgerDetail": "先用一張總帳回答哪些主機、專案、網站、服務、監控訊號、KM / PlayBook / Verifier 與 SRE 路由已納管,哪些仍卡在批准或只讀 gate。",
"scopeEyebrow": "02 / Scope Matrix",
"scopeTitle": "全域範圍矩陣",
"scopeDetail": "用同一個矩陣把主機、專案、網站前後台、服務、套件、工具與學習鏈路納入;每一格都顯示可判讀程度與仍需批准的範圍。",
"topologyEyebrow": "02 / Topology",
"topologyEyebrow": "03 / Topology",
"topologyTitle": "訊號拓樸與 AI 接管路徑",
"topologyDetail": "從主機與產品面開始接到服務健康、監控訊號、AwoooI SRE 戰情室與 AI Agent 決策鏈,讓告警來源和下一步清楚可追。",
"flowEyebrow": "03 / Flow",
"flowEyebrow": "04 / Flow",
"flowTitle": "告警到處置的流程視圖",
"flowDetail": "用流程呈現收集、關聯、分類、路由與批准閘門;避免週報只有 0也避免批准後不知道下一步。",
"signalEyebrow": "04 / Signal Contracts",
"signalEyebrow": "05 / Signal Contracts",
"signalTitle": "監控合約與降噪候選",
"signalDetail": "Prometheus、Alertmanager、Grafana、SigNoz、Sentry、OTEL 等訊號只讀呈現;降噪與規則調整先形成 proposal。",
"gapEyebrow": "05 / Health Gaps",
"gapEyebrow": "06 / Health Gaps",
"gapTitle": "健康缺口與過期端點",
"gapDetail": "把服務健康、端點 truth drift、runner 證明與 provider 來源缺口整理成 SRE 可判斷的處置焦點。",
"boundaryEyebrow": "06 / Guardrails",
"boundaryEyebrow": "07 / Guardrails",
"boundaryTitle": "不可誤讀合約",
"boundaryDetail": "此頁是監控與決策視圖,不是 runtime 授權;任何會改正式環境的動作仍需獨立批准與驗證。",
"drilldownEyebrow": "07 / Drilldown",
"drilldownEyebrow": "08 / Drilldown",
"drilldownTitle": "細節分頁",
"drilldownDetail": "保留原本監控、APM、錯誤、應用與服務目錄作為總圖後的細節查證入口。"
},
"assetLedger": {
"scopeLabel": "全域資產",
"scopeDetail": "主機 {hosts}、專案 {projects}、網站前後台 {websites} 已進同一張納管總帳。",
"scopeMeta": "{domains} 類 domain需批准 {approval}",
"signalLabel": "監控訊號",
"signalDetail": "訊號合約含分類缺口 {gaps}、降噪候選 {noise};先產生 proposal。",
"signalMeta": "需處置 {required};需批准 {approval}",
"healthLabel": "服務健康",
"healthDetail": "健康缺口 {health}、過期端點 {stale};不把心跳當成功。",
"healthMeta": "重啟允許 {restart};通知允許 {notify}",
"learningLabel": "KM / PlayBook / Verifier",
"learningDetail": "自動化資產 {assets}、任務 {tasks};用 owner gate 控制沉澱與回寫。",
"learningMeta": "需明確批准 {approval};阻擋操作 {blocked}",
"sreLabel": "SRE 戰情室",
"sreDetail": "action-required {action}、approval-required {approval} 才進集中路由。",
"sreMeta": "目標AwoooI SRE 戰情室,其他路由需例外批准",
"runtimeLabel": "Runtime Gate",
"runtimeDetail": "發通知、reload、restart、endpoint change、secret read 與修復套用仍鎖住。",
"runtimeMeta": "只讀顯示;不代表自動修復已授權"
},
"scope": {
"targets": "{count} 個目標",
"approval": "需批准 {count}",

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')}