From d411b2a4ea5e73c9dceafaa9bc0e734e21db5d01 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 18 Jun 2026 17:42:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=89=8D=E7=A7=BB=20Observability?= =?UTF-8?q?=20=E8=87=AA=E5=8B=95=E5=8C=96=E8=B3=87=E7=94=A2=E7=B8=BD?= =?UTF-8?q?=E5=B8=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/messages/en.json | 37 ++++- apps/web/messages/zh-TW.json | 37 ++++- .../src/app/[locale]/observability/page.tsx | 141 ++++++++++++++++++ 3 files changed, 201 insertions(+), 14 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index c3a8b6cd..38053928 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -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}", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index c3a8b6cd..38053928 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -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}", diff --git a/apps/web/src/app/[locale]/observability/page.tsx b/apps/web/src/app/[locale]/observability/page.tsx index f901d485..c6d7dffa 100644 --- a/apps/web/src/app/[locale]/observability/page.tsx +++ b/apps/web/src/app/[locale]/observability/page.tsx @@ -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 ( +
+
+
+

{label}

+

{value}

+
+
+ +
+
+

{detail}

+
+ +

{meta}

+
+
+ ) +} + 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
+
+ +
+ {assetLedgerRows.map(row => ( + + ))} +
+
+