diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 6eeccff7a..a15fab0d1 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -80,6 +80,104 @@ "iwooos": "IwoooS", "iwooosSecurityCompliance": "IwoooS 安全合規" }, + "observabilityCommand": { + "eyebrow": "AWOOOI 可觀測性指揮面板", + "title": "主機、專案、網站、服務與工具全域監控", + "subtitle": "把已納管的主機、專案、網站前後台、服務、套件、工具、監控訊號與 AI Agent 決策鏈收斂在同一個只讀操作視角;先看總圖、缺口與邊界,再往下鑽監控、APM、錯誤、應用與服務細節。", + "source": "{generated} · {current} → {next}", + "loading": "資料讀取中", + "liveRead": "正式 API 讀回", + "partialData": "部分資料待回讀", + "apiReady": "資料鏈正常", + "runtimeGate": "執行閘門 {count}", + "metrics": { + "decisionCoverage": "決策支援覆蓋率", + "decisionCoverageDetail": "{readable}/{total} 個目標已有可判讀合約或正式證據;仍不代表自動修復已開放。", + "totalScope": "全域納管目標", + "totalScopeDetail": "主機 {hosts}、專案 {projects}、網站前後台 {websites} 已進入同一張佈局圖。", + "actionRequired": "需處置焦點", + "actionRequiredDetail": "健康缺口 {health}、過期端點 {stale},先進 owner / SRE 判讀,不直接改線上。", + "runtimeGate": "自動執行授權", + "runtimeGateDetail": "重啟、reload、發通知、改 endpoint、讀 secret 與套用修復入口維持 0。" + }, + "sections": { + "scopeEyebrow": "01 / Scope Matrix", + "scopeTitle": "全域範圍矩陣", + "scopeDetail": "用同一個矩陣把主機、專案、網站前後台、服務、套件、工具與學習鏈路納入;每一格都顯示可判讀程度與仍需批准的範圍。", + "topologyEyebrow": "02 / Topology", + "topologyTitle": "訊號拓樸與 AI 接管路徑", + "topologyDetail": "從主機與產品面開始,接到服務健康、監控訊號、AwoooI SRE 戰情室與 AI Agent 決策鏈,讓告警來源和下一步清楚可追。", + "flowEyebrow": "03 / Flow", + "flowTitle": "告警到處置的流程視圖", + "flowDetail": "用流程呈現收集、關聯、分類、路由與批准閘門;避免週報只有 0,也避免批准後不知道下一步。", + "signalEyebrow": "04 / Signal Contracts", + "signalTitle": "監控合約與降噪候選", + "signalDetail": "Prometheus、Alertmanager、Grafana、SigNoz、Sentry、OTEL 等訊號只讀呈現;降噪與規則調整先形成 proposal。", + "gapEyebrow": "05 / Health Gaps", + "gapTitle": "健康缺口與過期端點", + "gapDetail": "把服務健康、端點 truth drift、runner 證明與 provider 來源缺口整理成 SRE 可判斷的處置焦點。", + "boundaryEyebrow": "06 / Guardrails", + "boundaryTitle": "不可誤讀合約", + "boundaryDetail": "此頁是監控與決策視圖,不是 runtime 授權;任何會改正式環境的動作仍需獨立批准與驗證。", + "drilldownEyebrow": "07 / Drilldown", + "drilldownTitle": "細節分頁", + "drilldownDetail": "保留原本監控、APM、錯誤、應用與服務目錄,作為總圖後的細節查證入口。" + }, + "scope": { + "targets": "{count} 個目標", + "approval": "需批准 {count}", + "readable": "可判讀" + }, + "topology": { + "hosts": "主機層", + "hostsDetail": "110 / 111 / 112 / 120 / 121 / 188 等主機進入佈局視野。", + "products": "專案與網站", + "productsDetail": "前台、後台與相鄰專案使用同一組治理視角。", + "services": "服務健康", + "servicesDetail": "API、Web、AI provider、備份與安全邊界健康目標。", + "signals": "監控訊號", + "signalsDetail": "Prometheus、Alertmanager、SigNoz、Sentry、Grafana 與 OTEL。", + "sre": "AwoooI SRE 戰情室", + "sreDetail": "失敗或需處置才進戰情室,避免心跳與成功訊息洗版。", + "ai": "AI Agent 決策鏈", + "aiDetail": "分類、證據、PlayBook、KM 與 verifier 仍受 runtime gate 控制。" + }, + "flow": { + "collect": "收集", + "collectDetail": "把主機、專案、網站、服務、工具與套件整理成統一納管目標。", + "correlate": "關聯", + "correlateDetail": "用健康目標和端點合約判斷資料是否新鮮與可用。", + "classify": "分類", + "classifyDetail": "保留分類缺口,避免把心跳或週報誤判成真實事件。", + "route": "路由", + "routeDetail": "真正需處置才進 AwoooI SRE 戰情室與 AwoooP。", + "gate": "閘門", + "gateDetail": "批准前只產生候選、證據與操作建議,不直接執行。" + }, + "boundary": { + "readOnly": "只讀證據", + "readOnlyDetail": "此頁只讀 committed snapshot 與正式 API,不主動打 live probe 或修改設定。", + "noRuntime": "Runtime gate 0", + "noRuntimeDetail": "不重啟服務、不 reload Alertmanager、不發 Telegram 測試通知、不改 endpoint。", + "sreRoom": "戰情室集中", + "sreRoomDetail": "後續告警治理要把產品告警收斂到 AwoooI SRE 戰情室,其他 Bot / 群組只保留經批准例外。" + }, + "values": { + "activeGoverned": "主動治理", + "readOnlyLayout": "只讀佈局", + "blockedByGate": "閘門阻擋", + "planned": "規劃中", + "candidateOnly": "候選", + "verified": "已驗證", + "actionRequired": "需處置", + "blocked": "阻擋", + "failureOnly": "失敗才通知", + "actionRequiredPolicy": "需處置才通知", + "approvalRequired": "需批准", + "dailySummaryOnly": "只進日報", + "noDirectNotify": "不直接通知" + } + }, "locale": { "switch": "切換語系", "zhTW": "繁體中文", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 6eeccff7a..a15fab0d1 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -80,6 +80,104 @@ "iwooos": "IwoooS", "iwooosSecurityCompliance": "IwoooS 安全合規" }, + "observabilityCommand": { + "eyebrow": "AWOOOI 可觀測性指揮面板", + "title": "主機、專案、網站、服務與工具全域監控", + "subtitle": "把已納管的主機、專案、網站前後台、服務、套件、工具、監控訊號與 AI Agent 決策鏈收斂在同一個只讀操作視角;先看總圖、缺口與邊界,再往下鑽監控、APM、錯誤、應用與服務細節。", + "source": "{generated} · {current} → {next}", + "loading": "資料讀取中", + "liveRead": "正式 API 讀回", + "partialData": "部分資料待回讀", + "apiReady": "資料鏈正常", + "runtimeGate": "執行閘門 {count}", + "metrics": { + "decisionCoverage": "決策支援覆蓋率", + "decisionCoverageDetail": "{readable}/{total} 個目標已有可判讀合約或正式證據;仍不代表自動修復已開放。", + "totalScope": "全域納管目標", + "totalScopeDetail": "主機 {hosts}、專案 {projects}、網站前後台 {websites} 已進入同一張佈局圖。", + "actionRequired": "需處置焦點", + "actionRequiredDetail": "健康缺口 {health}、過期端點 {stale},先進 owner / SRE 判讀,不直接改線上。", + "runtimeGate": "自動執行授權", + "runtimeGateDetail": "重啟、reload、發通知、改 endpoint、讀 secret 與套用修復入口維持 0。" + }, + "sections": { + "scopeEyebrow": "01 / Scope Matrix", + "scopeTitle": "全域範圍矩陣", + "scopeDetail": "用同一個矩陣把主機、專案、網站前後台、服務、套件、工具與學習鏈路納入;每一格都顯示可判讀程度與仍需批准的範圍。", + "topologyEyebrow": "02 / Topology", + "topologyTitle": "訊號拓樸與 AI 接管路徑", + "topologyDetail": "從主機與產品面開始,接到服務健康、監控訊號、AwoooI SRE 戰情室與 AI Agent 決策鏈,讓告警來源和下一步清楚可追。", + "flowEyebrow": "03 / Flow", + "flowTitle": "告警到處置的流程視圖", + "flowDetail": "用流程呈現收集、關聯、分類、路由與批准閘門;避免週報只有 0,也避免批准後不知道下一步。", + "signalEyebrow": "04 / Signal Contracts", + "signalTitle": "監控合約與降噪候選", + "signalDetail": "Prometheus、Alertmanager、Grafana、SigNoz、Sentry、OTEL 等訊號只讀呈現;降噪與規則調整先形成 proposal。", + "gapEyebrow": "05 / Health Gaps", + "gapTitle": "健康缺口與過期端點", + "gapDetail": "把服務健康、端點 truth drift、runner 證明與 provider 來源缺口整理成 SRE 可判斷的處置焦點。", + "boundaryEyebrow": "06 / Guardrails", + "boundaryTitle": "不可誤讀合約", + "boundaryDetail": "此頁是監控與決策視圖,不是 runtime 授權;任何會改正式環境的動作仍需獨立批准與驗證。", + "drilldownEyebrow": "07 / Drilldown", + "drilldownTitle": "細節分頁", + "drilldownDetail": "保留原本監控、APM、錯誤、應用與服務目錄,作為總圖後的細節查證入口。" + }, + "scope": { + "targets": "{count} 個目標", + "approval": "需批准 {count}", + "readable": "可判讀" + }, + "topology": { + "hosts": "主機層", + "hostsDetail": "110 / 111 / 112 / 120 / 121 / 188 等主機進入佈局視野。", + "products": "專案與網站", + "productsDetail": "前台、後台與相鄰專案使用同一組治理視角。", + "services": "服務健康", + "servicesDetail": "API、Web、AI provider、備份與安全邊界健康目標。", + "signals": "監控訊號", + "signalsDetail": "Prometheus、Alertmanager、SigNoz、Sentry、Grafana 與 OTEL。", + "sre": "AwoooI SRE 戰情室", + "sreDetail": "失敗或需處置才進戰情室,避免心跳與成功訊息洗版。", + "ai": "AI Agent 決策鏈", + "aiDetail": "分類、證據、PlayBook、KM 與 verifier 仍受 runtime gate 控制。" + }, + "flow": { + "collect": "收集", + "collectDetail": "把主機、專案、網站、服務、工具與套件整理成統一納管目標。", + "correlate": "關聯", + "correlateDetail": "用健康目標和端點合約判斷資料是否新鮮與可用。", + "classify": "分類", + "classifyDetail": "保留分類缺口,避免把心跳或週報誤判成真實事件。", + "route": "路由", + "routeDetail": "真正需處置才進 AwoooI SRE 戰情室與 AwoooP。", + "gate": "閘門", + "gateDetail": "批准前只產生候選、證據與操作建議,不直接執行。" + }, + "boundary": { + "readOnly": "只讀證據", + "readOnlyDetail": "此頁只讀 committed snapshot 與正式 API,不主動打 live probe 或修改設定。", + "noRuntime": "Runtime gate 0", + "noRuntimeDetail": "不重啟服務、不 reload Alertmanager、不發 Telegram 測試通知、不改 endpoint。", + "sreRoom": "戰情室集中", + "sreRoomDetail": "後續告警治理要把產品告警收斂到 AwoooI SRE 戰情室,其他 Bot / 群組只保留經批准例外。" + }, + "values": { + "activeGoverned": "主動治理", + "readOnlyLayout": "只讀佈局", + "blockedByGate": "閘門阻擋", + "planned": "規劃中", + "candidateOnly": "候選", + "verified": "已驗證", + "actionRequired": "需處置", + "blocked": "阻擋", + "failureOnly": "失敗才通知", + "actionRequiredPolicy": "需處置才通知", + "approvalRequired": "需批准", + "dailySummaryOnly": "只進日報", + "noDirectNotify": "不直接通知" + } + }, "locale": { "switch": "切換語系", "zhTW": "繁體中文", diff --git a/apps/web/src/app/[locale]/observability/page.tsx b/apps/web/src/app/[locale]/observability/page.tsx index a2f792bea..7a795ee9e 100644 --- a/apps/web/src/app/[locale]/observability/page.tsx +++ b/apps/web/src/app/[locale]/observability/page.tsx @@ -1,18 +1,37 @@ 'use client' /** - * 可觀測性 (/observability) — Sprint 5 整合頁面 - * ================================================ - * 整合: 服務監控 + APM + 錯誤追蹤 + 應用 + 服務目錄 - * 全部使用 Panel 元件 (無雙重 AppLayout) - * - * 零假數據: 全部串接真實 API - * - * 建立時間: 2026-04-08 (台北時區) - * 更新時間: 2026-04-09 — 全部 Tab Panel 抽取完成 + * 可觀測性 (/observability) + * ======================== + * 首屏整合只讀 AI Agent deployment layout、監控合約矩陣與服務健康缺口, + * 下方保留既有監控 / APM / 錯誤 / 應用 / 服務目錄分頁作為鑽取細節。 */ +import { useEffect, useMemo, useState, type ReactNode } from 'react' import { useTranslations } from 'next-intl' +import { + Activity, + AlertTriangle, + BellRing, + Boxes, + CheckCircle2, + CircuitBoard, + Database, + GitBranch, + Globe2, + Layers3, + Lock, + Network, + PackageCheck, + Radar, + Route, + Server, + ShieldCheck, + Workflow, + Wrench, + XCircle, + type LucideIcon, +} from 'lucide-react' import { AppLayout } from '@/components/layout' import { PageTabs, type TabConfig } from '@/components/layout/page-tabs' import { MonitoringPanel } from '@/components/panels/MonitoringPanel' @@ -20,41 +39,599 @@ import { ApmPanel } from '@/components/panels/ApmPanel' import { ErrorsPanel } from '@/components/panels/ErrorsPanel' import { AppsPanel } from '@/components/panels/AppsPanel' import { ServicesPanel } from '@/components/panels/ServicesPanel' +import { + apiClient, + type AiAgentAutomationInventorySnapshot, + type AiAgentDeploymentLayoutSnapshot, + type ObservabilityContractMatrixSnapshot, + type ServiceHealthGapMatrixSnapshot, +} from '@/lib/api-client' +import { cn } from '@/lib/utils' + +type CommandState = { + deployment: AiAgentDeploymentLayoutSnapshot | null + observability: ObservabilityContractMatrixSnapshot | null + serviceHealth: ServiceHealthGapMatrixSnapshot | null + inventory: AiAgentAutomationInventorySnapshot | null + loading: boolean + error: boolean +} + +type Tone = 'ok' | 'warn' | 'danger' | 'neutral' + +const DOMAIN_ICONS: Record = { + hosts: Server, + projects: GitBranch, + websites: Globe2, + services: Workflow, + packages: PackageCheck, + tools: Wrench, + learning: CircuitBoard, +} + +const DOMAIN_ORDER = ['hosts', 'projects', 'websites', 'services', 'packages', 'tools', 'learning'] + +function toneClass(tone: Tone) { + if (tone === 'ok') return 'border-emerald-200 bg-emerald-50 text-emerald-700' + if (tone === 'warn') return 'border-amber-200 bg-amber-50 text-amber-700' + if (tone === 'danger') return 'border-rose-200 bg-rose-50 text-rose-700' + return 'border-[#e0ddd4] bg-white text-[#141413]' +} + +function riskTone(status?: string): Tone { + if (!status) return 'neutral' + if (status === 'verified' || status === 'active_governed' || status === 'read_only_layout') return 'ok' + if (status === 'action_required' || status === 'planned' || status === 'candidate_only') return 'warn' + if (status === 'blocked' || status === 'blocked_by_gate' || status === 'critical') return 'danger' + return 'neutral' +} + +function formatNumber(value?: number | null) { + if (typeof value !== 'number' || Number.isNaN(value)) return '--' + return new Intl.NumberFormat('zh-TW').format(value) +} + +function formatDateTime(value?: string) { + if (!value) return '--' + const date = new Date(value) + if (Number.isNaN(date.getTime())) return '--' + return date.toLocaleString('zh-TW', { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) +} + +function MetricTile({ + label, + value, + detail, + icon, + tone = 'neutral', +}: { + label: string + value: ReactNode + detail: string + icon: ReactNode + tone?: Tone +}) { + return ( +
+
+
+

{label}

+

+ {value} +

+
+
+ {icon} +
+
+

{detail}

+
+ ) +} + +function SectionHeader({ + eyebrow, + title, + detail, +}: { + eyebrow: string + title: string + detail: string +}) { + return ( +
+

{eyebrow}

+

{title}

+

{detail}

+
+ ) +} + +function StatusChip({ children, tone = 'neutral' }: { children: ReactNode; tone?: Tone }) { + return ( + + {children} + + ) +} + +function MiniBar({ value, tone = 'ok' }: { value: number; tone?: Tone }) { + const color = tone === 'danger' ? 'bg-rose-500' : tone === 'warn' ? 'bg-amber-500' : 'bg-emerald-600' + return ( +
+
+
+ ) +} export default function ObservabilityPage({ params }: { params: { locale: string } }) { - const t = useTranslations('nav') + const nav = useTranslations('nav') + const t = useTranslations('observabilityCommand') + const [state, setState] = useState({ + deployment: null, + observability: null, + serviceHealth: null, + inventory: null, + loading: true, + error: false, + }) + + useEffect(() => { + let alive = true + + async function load() { + setState(prev => ({ ...prev, loading: true, error: false })) + const [deployment, observability, serviceHealth, inventory] = await Promise.allSettled([ + apiClient.getAiAgentDeploymentLayout(), + apiClient.getObservabilityContractMatrix(), + apiClient.getServiceHealthGapMatrix(), + apiClient.getAiAgentAutomationInventorySnapshot(), + ]) + + if (!alive) return + + setState({ + deployment: deployment.status === 'fulfilled' ? deployment.value : null, + observability: observability.status === 'fulfilled' ? observability.value : null, + serviceHealth: serviceHealth.status === 'fulfilled' ? serviceHealth.value : null, + inventory: inventory.status === 'fulfilled' ? inventory.value : null, + loading: false, + error: [deployment, observability, serviceHealth, inventory].some(result => result.status === 'rejected'), + }) + } + + load() + return () => { + alive = false + } + }, []) + + const statusLabels = useMemo( + () => ({ + active_governed: t('values.activeGoverned'), + read_only_layout: t('values.readOnlyLayout'), + blocked_by_gate: t('values.blockedByGate'), + planned: t('values.planned'), + candidate_only: t('values.candidateOnly'), + verified: t('values.verified'), + action_required: t('values.actionRequired'), + blocked: t('values.blocked'), + failure_only: t('values.failureOnly'), + action_required_policy: t('values.actionRequiredPolicy'), + approval_required: t('values.approvalRequired'), + daily_summary_only: t('values.dailySummaryOnly'), + no_direct_notify: t('values.noDirectNotify'), + }), + [t] + ) + + const valueLabel = (value?: string) => { + if (!value) return '--' + return statusLabels[value as keyof typeof statusLabels] ?? value.replaceAll('_', ' ') + } + + const deploymentTargets = state.deployment?.deployment_targets ?? [] + const serviceTargets = state.serviceHealth?.service_health_targets ?? [] + const observabilitySurfaces = state.observability?.observability_surfaces ?? [] + const totalDecisionTargets = deploymentTargets.length + serviceTargets.length + observabilitySurfaces.length + const readableTargets = + deploymentTargets.filter(target => target.deployment_state !== 'planned' && target.deployment_state !== 'candidate_only').length + + serviceTargets.filter(target => target.status === 'verified').length + + observabilitySurfaces.filter(surface => surface.status === 'verified').length + const decisionCoverage = totalDecisionTargets === 0 ? 0 : Math.round((readableTargets / totalDecisionTargets) * 100) + const actionRequired = + (state.observability?.rollups.surface_ids_requiring_action.length ?? 0) + + (state.serviceHealth?.rollups.target_ids_requiring_action.length ?? 0) + + (state.deployment?.rollups.approval_required_target_ids.length ?? 0) + const runtimeGateCount = + Object.values(state.observability?.operation_boundaries ?? {}).filter(Boolean).length + + Object.values(state.serviceHealth?.operation_boundaries ?? {}).filter(Boolean).length + + Object.values(state.deployment?.approval_boundaries ?? {}).filter(Boolean).length + + const domainRows = DOMAIN_ORDER.map(domainId => { + const domain = state.deployment?.domains.find(item => item.domain_id === domainId) + const targets = deploymentTargets.filter(target => target.domain_id === domainId) + const approvalRequired = targets.filter(target => state.deployment?.rollups.approval_required_target_ids.includes(target.target_id)).length + const verified = targets.filter(target => target.deployment_state === 'active_governed' || target.deployment_state === 'read_only_layout').length + return { + domainId, + label: domain?.display_name ?? domainId, + description: domain?.description ?? '', + count: targets.length, + verified, + approvalRequired, + Icon: DOMAIN_ICONS[domainId] ?? Boxes, + } + }) + + const topologyNodes = [ + { + key: 'hosts', + label: t('topology.hosts'), + value: state.deployment?.rollups.by_domain.hosts ?? 0, + detail: t('topology.hostsDetail'), + Icon: Server, + tone: 'ok' as Tone, + }, + { + key: 'products', + label: t('topology.products'), + value: (state.deployment?.rollups.by_domain.projects ?? 0) + (state.deployment?.rollups.by_domain.websites ?? 0), + detail: t('topology.productsDetail'), + Icon: Globe2, + tone: 'ok' as Tone, + }, + { + key: 'services', + label: t('topology.services'), + value: state.serviceHealth?.rollups.total_targets ?? 0, + detail: t('topology.servicesDetail'), + Icon: Workflow, + tone: 'warn' as Tone, + }, + { + key: 'signals', + label: t('topology.signals'), + value: state.observability?.rollups.total_surfaces ?? 0, + detail: t('topology.signalsDetail'), + Icon: Activity, + tone: 'warn' as Tone, + }, + { + key: 'sre', + label: t('topology.sre'), + value: state.deployment?.telegram_contract.primary_gateway ? 1 : 0, + detail: t('topology.sreDetail'), + Icon: BellRing, + tone: 'neutral' as Tone, + }, + { + key: 'ai', + label: t('topology.ai'), + value: state.inventory?.program_status.overall_completion_percent ?? '--', + detail: t('topology.aiDetail'), + Icon: CircuitBoard, + tone: 'danger' as Tone, + }, + ] + + const flowSteps = [ + { + label: t('flow.collect'), + value: formatNumber(state.deployment?.rollups.total_targets), + detail: t('flow.collectDetail'), + }, + { + label: t('flow.correlate'), + value: formatNumber(state.serviceHealth?.rollups.total_targets), + detail: t('flow.correlateDetail'), + }, + { + label: t('flow.classify'), + value: formatNumber(state.observability?.rollups.classification_gap_ids.length), + detail: t('flow.classifyDetail'), + }, + { + label: t('flow.route'), + value: formatNumber(state.deployment?.rollups.by_telegram_policy.action_required), + detail: t('flow.routeDetail'), + }, + { + label: t('flow.gate'), + value: formatNumber(runtimeGateCount), + detail: t('flow.gateDetail'), + }, + ] const tabs: TabConfig[] = [ { id: 'monitoring', - label: t('monitoring'), + label: nav('monitoring'), content: , }, { id: 'apm', - label: t('apm'), + label: nav('apm'), content: , }, { id: 'errors', - label: t('errors'), + label: nav('errors'), content: , }, { id: 'apps', - label: t('apps'), + label: nav('apps'), content: , }, { id: 'services', - label: t('services'), + label: nav('services'), content: , }, ] + const sourceLabel = t('source', { + generated: formatDateTime(state.observability?.generated_at ?? state.deployment?.generated_at ?? state.serviceHealth?.generated_at), + current: state.observability?.program_status.current_task_id ?? state.serviceHealth?.program_status.current_task_id ?? '--', + next: state.observability?.program_status.next_task_id ?? state.serviceHealth?.program_status.next_task_id ?? '--', + }) + return ( - +
+
+
+
+
+

{t('eyebrow')}

+

+ {t('title')} +

+

{t('subtitle')}

+
+
+ {state.loading ? t('loading') : t('liveRead')} + {state.error ? t('partialData') : t('apiReady')} + {t('runtimeGate', { count: runtimeGateCount })} + {sourceLabel} +
+
+ +
+ } + tone={decisionCoverage >= 80 ? 'ok' : decisionCoverage >= 60 ? 'warn' : 'danger'} + /> + } + tone="ok" + /> + } + tone={actionRequired > 0 ? 'warn' : 'ok'} + /> + } + tone="danger" + /> +
+
+
+ +
+
+ +
+ {domainRows.map(row => { + const coverage = row.count === 0 ? 0 : Math.round((row.verified / row.count) * 100) + const tone: Tone = row.approvalRequired > 0 ? 'warn' : 'ok' + return ( +
+
+
+
+ +
+
+

{row.label}

+

{t('scope.targets', { count: row.count })}

+
+
+ {t('scope.approval', { count: row.approvalRequired })} +
+

{row.description}

+
+
+ {t('scope.readable')} + {coverage}% +
+ +
+
+ ) + })} +
+
+ +
+
+ +
+ {topologyNodes.map((node, index) => ( +
+ {index < topologyNodes.length - 1 && ( +
+ )} +
+
+

{node.label}

+

+ {formatNumber(typeof node.value === 'number' ? node.value : Number.NaN)} +

+
+
+ +
+
+

{node.detail}

+
+ ))} +
+
+ +
+ +
+ {flowSteps.map((step, index) => ( +
+
+
+ {index + 1} +
+ {index < flowSteps.length - 1 &&
} +
+
+
+

{step.label}

+ {step.value} +
+

{step.detail}

+
+
+ ))} +
+
+
+ +
+
+ +
+ {observabilitySurfaces.map(surface => ( +
+
+
+

{surface.display_name}

+

{valueLabel(surface.kind)}

+
+ {valueLabel(surface.status)} +
+

{surface.next_action}

+
+ {valueLabel(surface.noise_policy_status)} + {valueLabel(surface.evidence_status)} +
+
+ ))} +
+
+ +
+ +
+ {state.serviceHealth?.health_gaps.slice(0, 4).map(gap => ( +
+
+

{gap.display_name}

+ {valueLabel(gap.severity)} +
+

{gap.summary}

+
+ ))} +
+
+
+ +
+ +
+
+
+ + {t('boundary.readOnly')} +
+

{t('boundary.readOnlyDetail')}

+
+
+
+ + {t('boundary.noRuntime')} +
+

{t('boundary.noRuntimeDetail')}

+
+
+
+ + {t('boundary.sreRoom')} +
+

{t('boundary.sreRoomDetail')}

+
+
+
+ +
+ + +
+
+
) } + +function GaugeIcon() { + return +}