From 55d1df24e7d90c1d330517d1dd7a9e11af74660f Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 10:15:07 +0800 Subject: [PATCH] feat(web): render automation blueprint diagrams --- apps/web/messages/en.json | 76 +++ apps/web/messages/zh-TW.json | 76 +++ apps/web/src/app/[locale]/page.tsx | 494 +++++++++++++++++- apps/web/src/components/layout/app-layout.tsx | 21 +- 4 files changed, 650 insertions(+), 17 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index fdfafcaa..4e41ab33 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -406,6 +406,82 @@ "km": "KM / PlayBook" } } + }, + "workspace": { + "eyebrow": "Live Blueprint", + "title": "AI Automation Operating Map", + "subtitle": "This view puts process, runtime, decision table, and evidence lineage in one operating surface so the homepage can show where work is, what is blocked, and who continues it.", + "flow": { + "title": "BPMN / Swimlane Flow", + "subtitle": "The main path from alert intake through analysis, investigation, approval, execution, verification, and learning.", + "stages": { + "signal": "Alert / Sentry / SigNoz", + "intake": "AwoooP Intake", + "ai": "OpenClaw / Hermes", + "mcp": "MCP Evidence", + "playbook": "PlayBook Gate", + "ansible": "Ansible Check", + "approval": "Approval / Apply", + "verify": "Verify / KM" + } + }, + "topology": { + "title": "C4 / Runtime Topology", + "subtitle": "Runtime relationships across product, data, executors, MCP, and model providers.", + "layers": { + "channels": "Channels", + "product": "Product", + "data": "Data", + "execution": "Execution", + "providers": "AI Providers" + } + }, + "decision": { + "title": "DMN Decision Table", + "subtitle": "Auditable conditions for whether AI can safely auto-repair.", + "headers": { + "signal": "Signal", + "value": "Current value", + "outcome": "Decision" + }, + "rows": { + "claim": "Production claim", + "qualityGate": "Quality gate", + "ansible": "Ansible runtime", + "aiRoute": "AI route", + "km": "KM freshness", + "callback": "Callback trace" + }, + "outcomes": { + "claimReady": "Full loop can be claimed", + "claimBlocked": "Full loop cannot be claimed", + "fillEvidence": "Fill execution / repair / approval / learning evidence", + "ansibleReady": "Ready for check-mode", + "ansibleBlocked": "Fix Ansible runtime first", + "monitor": "Primary lane is monitored", + "ownerReview": "Hermes drafts, owner reviews", + "watchDecay": "Wait for 24h backlog decay" + } + }, + "lineage": { + "title": "Trace / Lineage Evidence", + "subtitle": "Every Telegram alert, button, Run, KM, and PlayBook should link back to one evidence chain.", + "nodes": { + "telegram": "Telegram Message", + "callback": "Callback Evidence", + "db": "DB Truth", + "run": "Run Timeline", + "km": "KM / PlayBook" + } + }, + "values": { + "verified": "verified {verified}/{evaluated}", + "topGate": "{gate} missing {count}", + "ansible": "check-mode {checkMode}, pending {pending}, blocker {blocker}", + "aiRoute": "{lane} / {provider}", + "km": "{stale} stale over {days} days", + "callback": "missing {missing}, 1h {recent1h}, 24h {recent24h}" + } } } }, diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 38ee66c7..7b03fa84 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -407,6 +407,82 @@ "km": "KM / PlayBook" } } + }, + "workspace": { + "eyebrow": "Live Blueprint", + "title": "AI 自動化完整作戰圖", + "subtitle": "這一區把流程、Runtime、決策表與證據鏈放在同一個作戰視圖,讓首頁能直接回答目前跑到哪裡、卡在哪一關、該由誰接續。", + "flow": { + "title": "BPMN / Swimlane 流程", + "subtitle": "告警進來後,從分析、調查、審批、執行到驗證的主幹流程。", + "stages": { + "signal": "Alert / Sentry / SigNoz", + "intake": "AwoooP Intake", + "ai": "OpenClaw / Hermes", + "mcp": "MCP Evidence", + "playbook": "PlayBook Gate", + "ansible": "Ansible Check", + "approval": "Approval / Apply", + "verify": "Verify / KM" + } + }, + "topology": { + "title": "C4 / Runtime 拓樸", + "subtitle": "產品、資料、執行器、MCP 與模型供應商的 runtime 關係。", + "layers": { + "channels": "Channels", + "product": "Product", + "data": "Data", + "execution": "Execution", + "providers": "AI Providers" + } + }, + "decision": { + "title": "DMN 決策表", + "subtitle": "把 AI 是否能自動修復的判斷拆成可稽核條件。", + "headers": { + "signal": "Signal", + "value": "Current value", + "outcome": "Decision" + }, + "rows": { + "claim": "Production claim", + "qualityGate": "Quality gate", + "ansible": "Ansible runtime", + "aiRoute": "AI route", + "km": "KM freshness", + "callback": "Callback trace" + }, + "outcomes": { + "claimReady": "可宣稱完整閉環", + "claimBlocked": "不可宣稱完整閉環", + "fillEvidence": "補 execution / repair / approval / learning evidence", + "ansibleReady": "可進 check-mode", + "ansibleBlocked": "先修 Ansible runtime", + "monitor": "Primary lane 監控中", + "ownerReview": "Hermes 產草稿,owner 審核", + "watchDecay": "等待 24h backlog 歸零" + } + }, + "lineage": { + "title": "Trace / Lineage 證據鏈", + "subtitle": "每一則 Telegram 告警、按鈕、Run、KM 與 PlayBook 都要能串回同一條證據。", + "nodes": { + "telegram": "Telegram Message", + "callback": "Callback Evidence", + "db": "DB Truth", + "run": "Run Timeline", + "km": "KM / PlayBook" + } + }, + "values": { + "verified": "verified {verified}/{evaluated}", + "topGate": "{gate} 缺 {count}", + "ansible": "check-mode {checkMode},pending {pending},blocker {blocker}", + "aiRoute": "{lane} / {provider}", + "km": "{stale} stale over {days} days", + "callback": "missing {missing},1h {recent1h},24h {recent24h}" + } } } }, diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx index 52988469..17fb5056 100644 --- a/apps/web/src/app/[locale]/page.tsx +++ b/apps/web/src/app/[locale]/page.tsx @@ -963,6 +963,220 @@ export default function Home({ params }: { params: { locale: string } }) { ], }, ] + const automationFlowStages: HomepageWorkItemSummary[] = [ + { + key: 'signal', + title: tDashboard('automationDiagrams.workspace.flow.stages.signal'), + status: tDashboard('automationDelivery.status.live'), + detail: tDashboard('automationDiagrams.workspace.values.callback', { + missing: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_total, automationBriefLoaded), + recent1h: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_1h_total, automationBriefLoaded), + recent24h: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_24h_total, automationBriefLoaded), + }), + href: `/${locale}/awooop/runs?project_id=awoooi#tg-callback-evidence`, + tone: 'live', + }, + { + key: 'intake', + title: tDashboard('automationDiagrams.workspace.flow.stages.intake'), + status: tDashboard('automationDelivery.status.live'), + detail: tDashboard('automationDelivery.delivered.cicdTimeline.detail'), + href: `/${locale}/awooop/runs`, + tone: 'live', + }, + { + key: 'ai', + title: tDashboard('automationDiagrams.workspace.flow.stages.ai'), + status: hasAiRouteStatus ? tDashboard('automationDelivery.status.live') : unavailableStatus, + detail: tDashboard('automationDiagrams.workspace.values.aiRoute', { + lane: aiRouteLaneMode, + provider: aiRouteSelectedProvider, + }), + href: `/${locale}/awooop/work-items?project_id=awoooi`, + tone: hasAiRouteStatus ? 'live' : 'watching', + }, + { + key: 'mcp', + title: tDashboard('automationDiagrams.workspace.flow.stages.mcp'), + status: tDashboard('automationDelivery.status.watching'), + detail: tDashboard('automationDiagrams.cards.incidentFlow.detail'), + href: `/${locale}/awooop/runs?project_id=awoooi`, + tone: 'watching', + }, + { + key: 'playbook', + title: tDashboard('automationDiagrams.workspace.flow.stages.playbook'), + status: topAutomationGate ? tDashboard('automationDelivery.status.blocked') : tDashboard('automationDelivery.status.live'), + detail: tDashboard('automationDiagrams.workspace.values.topGate', { + gate: topAutomationGate?.gate ?? (automationQualityLoaded ? unavailableValue : loadingStatus), + count: formatAutomationNumber(topAutomationGate?.total, automationQualityLoaded), + }), + href: `/${locale}/awooop/work-items?project_id=awoooi`, + tone: topAutomationGate ? 'blocked' : automationQualityAvailable ? 'live' : 'watching', + }, + { + key: 'ansible', + title: tDashboard('automationDiagrams.workspace.flow.stages.ansible'), + status: ansibleRuntime?.can_run_check_mode ? tDashboard('automationDelivery.status.live') : tDashboard('automationDelivery.status.blocked'), + detail: tDashboard('automationDiagrams.workspace.values.ansible', { + checkMode: formatAutomationNumber(executionBackend?.ansible_check_mode_total, automationQualityLoaded), + pending: formatAutomationNumber(executionBackend?.ansible_pending_check_mode_total, automationQualityLoaded), + blocker: ansibleRuntime?.blockers?.join(' / ') || (automationQualityLoaded ? unavailableValue : loadingStatus), + }), + href: `/${locale}/awooop/work-items?project_id=awoooi`, + tone: ansibleRuntime?.can_run_check_mode ? 'live' : automationQualityAvailable ? 'blocked' : 'watching', + }, + { + key: 'approval', + title: tDashboard('automationDiagrams.workspace.flow.stages.approval'), + status: canClaimFullAutoRepair ? tDashboard('automationDelivery.status.live') : tDashboard('automationDelivery.status.blocked'), + detail: tDashboard('automationDiagrams.workspace.values.verified', { + verified: formatAutomationNumber(verifiedAutomationTotal, automationQualityLoaded), + evaluated: formatAutomationNumber(evaluatedAutomationTotal, automationQualityLoaded), + }), + href: `/${locale}/awooop/approvals`, + tone: canClaimFullAutoRepair ? 'live' : automationQualityAvailable ? 'blocked' : 'watching', + }, + { + key: 'verify', + title: tDashboard('automationDiagrams.workspace.flow.stages.verify'), + status: typeof kmStaleTotal === 'number' && kmStaleTotal > 0 ? tDashboard('automationDelivery.status.progress') : tDashboard('automationDelivery.status.live'), + detail: tDashboard('automationDiagrams.workspace.values.km', { + stale: formatAutomationNumber(kmStaleTotal, automationBriefLoaded), + days: formatAutomationNumber(kmStaleThreshold, automationBriefLoaded), + }), + href: `/${locale}/knowledge-base`, + tone: typeof kmStaleTotal === 'number' && kmStaleTotal > 0 ? 'progress' : hasKmStaleCandidates ? 'live' : 'watching', + }, + ] + const runtimeTopologyLayers = [ + { + key: 'channels', + title: tDashboard('automationDiagrams.workspace.topology.layers.channels'), + items: [ + tDashboard('automationDiagrams.cards.incidentFlow.nodes.alert'), + tDashboard('automationDiagrams.cards.evidenceLineage.nodes.telegram'), + ], + }, + { + key: 'product', + title: tDashboard('automationDiagrams.workspace.topology.layers.product'), + items: [ + tDashboard('automationDiagrams.cards.c4Runtime.nodes.web'), + tDashboard('automationDiagrams.cards.c4Runtime.nodes.api'), + tDashboard('automationDiagrams.cards.evidenceLineage.nodes.trace'), + ], + }, + { + key: 'data', + title: tDashboard('automationDiagrams.workspace.topology.layers.data'), + items: [ + tDashboard('automationDiagrams.cards.evidenceLineage.nodes.db'), + tDashboard('automationDiagrams.cards.evidenceLineage.nodes.km'), + ], + }, + { + key: 'execution', + title: tDashboard('automationDiagrams.workspace.topology.layers.execution'), + items: [ + tDashboard('automationDiagrams.cards.incidentFlow.nodes.playbook'), + tDashboard('automationDiagrams.workspace.flow.stages.ansible'), + tDashboard('automationDiagrams.cards.c4Runtime.nodes.k8s'), + ], + }, + { + key: 'providers', + title: tDashboard('automationDiagrams.workspace.topology.layers.providers'), + items: [ + aiRouteSelectedProvider, + tDashboard('automationDelivery.delivered.aiRoute.detail', { + lane: aiRouteLaneMode, + provider: aiRouteSelectedProvider, + }), + ], + }, + ] + const decisionRows: Array<{ + key: string + signal: string + value: string + outcome: string + tone: HomepageWorkTone + }> = [ + { + key: 'claim', + signal: tDashboard('automationDiagrams.workspace.decision.rows.claim'), + value: tDashboard('automationDiagrams.workspace.values.verified', { + verified: formatAutomationNumber(verifiedAutomationTotal, automationQualityLoaded), + evaluated: formatAutomationNumber(evaluatedAutomationTotal, automationQualityLoaded), + }), + outcome: canClaimFullAutoRepair + ? tDashboard('automationDiagrams.workspace.decision.outcomes.claimReady') + : tDashboard('automationDiagrams.workspace.decision.outcomes.claimBlocked'), + tone: canClaimFullAutoRepair ? 'live' : automationQualityAvailable ? 'blocked' : 'watching', + }, + { + key: 'qualityGate', + signal: tDashboard('automationDiagrams.workspace.decision.rows.qualityGate'), + value: tDashboard('automationDiagrams.workspace.values.topGate', { + gate: topAutomationGate?.gate ?? (automationQualityLoaded ? unavailableValue : loadingStatus), + count: formatAutomationNumber(topAutomationGate?.total, automationQualityLoaded), + }), + outcome: tDashboard('automationDiagrams.workspace.decision.outcomes.fillEvidence'), + tone: topAutomationGate ? 'blocked' : automationQualityAvailable ? 'live' : 'watching', + }, + { + key: 'ansible', + signal: tDashboard('automationDiagrams.workspace.decision.rows.ansible'), + value: tDashboard('automationDiagrams.workspace.values.ansible', { + checkMode: formatAutomationNumber(executionBackend?.ansible_check_mode_total, automationQualityLoaded), + pending: formatAutomationNumber(executionBackend?.ansible_pending_check_mode_total, automationQualityLoaded), + blocker: ansibleRuntime?.blockers?.join(' / ') || (automationQualityLoaded ? unavailableValue : loadingStatus), + }), + outcome: ansibleRuntime?.can_run_check_mode + ? tDashboard('automationDiagrams.workspace.decision.outcomes.ansibleReady') + : tDashboard('automationDiagrams.workspace.decision.outcomes.ansibleBlocked'), + tone: ansibleRuntime?.can_run_check_mode ? 'live' : automationQualityAvailable ? 'blocked' : 'watching', + }, + { + key: 'aiRoute', + signal: tDashboard('automationDiagrams.workspace.decision.rows.aiRoute'), + value: tDashboard('automationDiagrams.workspace.values.aiRoute', { + lane: aiRouteLaneMode, + provider: aiRouteSelectedProvider, + }), + outcome: tDashboard('automationDiagrams.workspace.decision.outcomes.monitor'), + tone: hasAiRouteStatus ? 'live' : 'watching', + }, + { + key: 'km', + signal: tDashboard('automationDiagrams.workspace.decision.rows.km'), + value: tDashboard('automationDiagrams.workspace.values.km', { + stale: formatAutomationNumber(kmStaleTotal, automationBriefLoaded), + days: formatAutomationNumber(kmStaleThreshold, automationBriefLoaded), + }), + outcome: tDashboard('automationDiagrams.workspace.decision.outcomes.ownerReview'), + tone: typeof kmStaleTotal === 'number' && kmStaleTotal > 0 ? 'progress' : hasKmStaleCandidates ? 'live' : 'watching', + }, + { + key: 'callback', + signal: tDashboard('automationDiagrams.workspace.decision.rows.callback'), + value: tDashboard('automationDiagrams.workspace.values.callback', { + missing: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_total, automationBriefLoaded), + recent1h: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_1h_total, automationBriefLoaded), + recent24h: formatAutomationNumber(callbackTraceSummary?.outbound_reply_markup_missing_trace_ref_recent_24h_total, automationBriefLoaded), + }), + outcome: tDashboard('automationDiagrams.workspace.decision.outcomes.watchDecay'), + tone: callbackTraceSummary && missingTraceRecent24h > 0 ? 'progress' : callbackTraceSummary ? 'live' : 'watching', + }, + ] + const lineageNodes = [ + tDashboard('automationDiagrams.workspace.lineage.nodes.telegram'), + tDashboard('automationDiagrams.workspace.lineage.nodes.callback'), + tDashboard('automationDiagrams.workspace.lineage.nodes.db'), + tDashboard('automationDiagrams.workspace.lineage.nodes.run'), + tDashboard('automationDiagrams.workspace.lineage.nodes.km'), + ] // ── 5 KPI Cards (Sprint 5R 設計稿批准版) ──────────────────────────────────── @@ -1041,6 +1255,7 @@ export default function Home({ params }: { params: { locale: string } }) { const [activeTabId, setActiveTabId] = useState('overview') const [infraView, setInfraView] = useState<'host' | 'topo'>('topo') const [selectedHost, setSelectedHost] = useState(null) + const [compactViewport, setCompactViewport] = useState(false) // I1 修正: popstate 取代 100ms 輪詢 useEffect(() => { @@ -1055,6 +1270,29 @@ export default function Home({ params }: { params: { locale: string } }) { return () => { window.removeEventListener('popstate', syncTab); clearInterval(fallback) } }, []) + useEffect(() => { + const media = window.matchMedia('(max-width: 720px)') + const syncViewport = () => setCompactViewport(media.matches) + + syncViewport() + media.addEventListener('change', syncViewport) + + return () => { + media.removeEventListener('change', syncViewport) + } + }, []) + + const primarySectionMargin = compactViewport ? '10px 8px 0' : '14px 20px 0' + const secondarySectionMargin = compactViewport ? '10px 8px 0' : '12px 20px 0' + const sectionPadding = compactViewport ? 10 : 14 + const dashboardGridTemplate = compactViewport ? '1fr' : 'repeat(auto-fit, minmax(320px, 1fr))' + const diagramCardGridTemplate = compactViewport ? '1fr' : 'repeat(auto-fit, minmax(240px, 1fr))' + const diagramWorkspaceGridTemplate = compactViewport ? '1fr' : 'repeat(auto-fit, minmax(min(360px, 100%), 1fr))' + const flowStageGridTemplate = compactViewport ? '1fr' : 'repeat(auto-fit, minmax(138px, 1fr))' + const topologyGridTemplate = compactViewport ? '1fr' : 'repeat(auto-fit, minmax(150px, 1fr))' + const decisionGridTemplate = compactViewport ? '1fr' : '0.8fr 1.15fr 1.05fr' + const workItemGridTemplate = compactViewport ? '1fr' : '1fr auto' + return ( {/* Sprint 5: Tab Bar */} @@ -1078,7 +1316,7 @@ export default function Home({ params }: { params: { locale: string } }) { }}>
@@ -1099,7 +1338,7 @@ export default function Home({ params }: { params: { locale: string } }) {
{tDashboard('automationDelivery.eyebrow')}
-

+

{tDashboard('automationDelivery.title')}

@@ -1107,7 +1346,8 @@ export default function Home({ params }: { params: { locale: string } }) {

-
+

@@ -1150,7 +1390,7 @@ export default function Home({ params }: { params: { locale: string } }) { href={item.href} style={{ display: 'grid', - gridTemplateColumns: '1fr auto', + gridTemplateColumns: workItemGridTemplate, gap: 10, padding: '10px 12px', border: '0.5px solid #e0ddd4', @@ -1166,6 +1406,7 @@ export default function Home({ params }: { params: { locale: string } }) {

@@ -1266,7 +1509,7 @@ export default function Home({ params }: { params: { locale: string } }) { {tDashboard('automationDiagrams.openTopology')}
-
+
{productDiagramCards.map(card => ( ))}
+
+
+
+ {tDashboard('automationDiagrams.workspace.eyebrow')} +
+

+ {tDashboard('automationDiagrams.workspace.title')} +

+

+ {tDashboard('automationDiagrams.workspace.subtitle')} +

+
+ +
+
+ + +
+
+ {tDashboard('automationDiagrams.workspace.topology.title')} +
+
+ {tDashboard('automationDiagrams.workspace.topology.subtitle')} +
+
+ {runtimeTopologyLayers.map((layer) => ( +
+
+ {layer.title} +
+
+ {layer.items.map((item) => ( +
+ {item} +
+ ))} +
+
+ ))} +
+
+
+ +
+
+
+ {tDashboard('automationDiagrams.workspace.decision.title')} +
+
+ {tDashboard('automationDiagrams.workspace.decision.subtitle')} +
+
+
+
{tDashboard('automationDiagrams.workspace.decision.headers.signal')}
+
{tDashboard('automationDiagrams.workspace.decision.headers.value')}
+
{tDashboard('automationDiagrams.workspace.decision.headers.outcome')}
+
+ {decisionRows.map((row) => { + const tone = automationWorkToneStyle[row.tone] + return ( +
+
+ {row.signal} +
+
+ {row.value} +
+
+ {row.outcome} +
+
+ ) + })} +
+
+ +
+
+ {tDashboard('automationDiagrams.workspace.lineage.title')} +
+
+ {tDashboard('automationDiagrams.workspace.lineage.subtitle')} +
+
+ {lineageNodes.map((node, index) => ( +
+
+ {index + 1} +
+
+ {node} +
+
+ ))} +
+
+
+
+
{/* ── KPI Strip (5 卡片 — Sprint 5R 設計稿) ──────────────────────── */} -
+
{/* 系統健康 */}
{tDashboard('serviceHealth')}
@@ -1400,11 +1866,11 @@ export default function Home({ params }: { params: { locale: string } }) {
{/* ── 主體 2 欄 ─────────────────────────────────────────────────────── */} -
+
{/* ── 左欄 (60%): 活躍事件 + 處置統計 + 最近活動 ─────────────── */}
@@ -1510,7 +1976,7 @@ export default function Home({ params }: { params: { locale: string } }) { {/* ── 右欄 (40%): OpenClaw + 基礎架構 + 監控工具 ─────────────── */}
{ + const media = window.matchMedia(MOBILE_SHELL_MEDIA) + const updateMobileShell = () => setMobileShell(media.matches) + + updateMobileShell() + media.addEventListener('change', updateMobileShell) + + return () => { + media.removeEventListener('change', updateMobileShell) + } + }, []) + // Phase 19 修復: 全局啟動 SSE 連接 (所有頁面共享) useEffect(() => { const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || '' @@ -88,6 +102,7 @@ export function AppLayout({ // Keep the navigation shell in the server-rendered HTML. If a rolling deploy // or stale browser cache delays hydration, the operator still has navigation. const effectiveCollapsed = mounted ? collapsed : false + const shellCollapsed = mobileShell || effectiveCollapsed return (
@@ -104,14 +119,14 @@ export function AppLayout({ {/* Sidebar */} {/* Header */}
{/* Main Content */} @@ -120,7 +135,7 @@ export function AppLayout({ 'relative z-10', 'pt-[68px]', 'transition-all duration-300 ease-out', - effectiveCollapsed ? 'ml-16' : 'ml-[224px]' + shellCollapsed ? 'ml-16' : 'ml-[224px]' )} > {fullBleed ? (