feat(web): 前移 Observability 自動化資產總帳
This commit is contained in:
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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