From 6aec9489d41dc83f2ea7fdbf18c982b6517c8e5d Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 10:44:45 +0800 Subject: [PATCH] feat(web): add homepage blueprint drilldown --- apps/web/messages/en.json | 51 ++++++++++ apps/web/messages/zh-TW.json | 51 ++++++++++ apps/web/src/app/[locale]/page.tsx | 156 ++++++++++++++++++++++++++++- 3 files changed, 255 insertions(+), 3 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 4e41ab33..9822e0f1 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -474,6 +474,57 @@ "km": "KM / PlayBook" } }, + "inspector": { + "title": "Stage Inspector", + "openTarget": "Open Work Surface", + "fields": { + "owner": "Owner", + "evidence": "Evidence Source", + "nextAction": "Next Step" + }, + "stages": { + "signal": { + "owner": "OpenClaw + AlertChain", + "evidence": "Alertmanager / Sentry / SigNoz / Telegram callback trace", + "nextAction": "Attach the signal to an AwoooP run dossier and produce a traceable fingerprint" + }, + "intake": { + "owner": "AwoooP Run Monitor", + "evidence": "Runs list / timeline / alert_operation_log / callback evidence", + "nextAction": "Link incident_id, trace_ref, and run_id so the alert does not stop at Telegram" + }, + "ai": { + "owner": "OpenClaw leads decisions; Hermes drafts KM", + "evidence": "AI route status / selected provider / skipped lanes", + "nextAction": "Keep GCP-A -> GCP-B -> 111 -> Gemini fallback order and record the lane" + }, + "mcp": { + "owner": "MCP Gateway", + "evidence": "K8s / Prometheus / Sentry / SigNoz / Gitea / self-hosted MCP results", + "nextAction": "Write MCP evidence back to the dossier so the LLM does not decide by guessing" + }, + "playbook": { + "owner": "OpenClaw + PlayBook trust gate", + "evidence": "Quality gate / work items / playbook match / execution history", + "nextAction": "Fill execution, repair, approval, and learning evidence before promotion" + }, + "ansible": { + "owner": "AwoooP Executor + Ansible lane", + "evidence": "ansible_runtime / check-mode count / pending check-mode / blockers", + "nextAction": "Clear ansible_playbook_binary_missing first, then run check-mode without direct apply" + }, + "approval": { + "owner": "Approval Coordinator + SRE owner", + "evidence": "Approvals / risk gate / run timeline / manual_required reason", + "nextAction": "Allow low-risk automation, keep high-risk work under human approval and audit trail" + }, + "verify": { + "owner": "Hermes + KM owner", + "evidence": "KM stale candidates / post-execution verification / playbook learning", + "nextAction": "Hermes drafts updates, owners review before KM write, then stale ratio is monitored" + } + } + }, "values": { "verified": "verified {verified}/{evaluated}", "topGate": "{gate} missing {count}", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 7b03fa84..7f4c294c 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -475,6 +475,57 @@ "km": "KM / PlayBook" } }, + "inspector": { + "title": "Stage Inspector", + "openTarget": "打開工作面", + "fields": { + "owner": "主責", + "evidence": "證據來源", + "nextAction": "下一步" + }, + "stages": { + "signal": { + "owner": "OpenClaw + AlertChain", + "evidence": "Alertmanager / Sentry / SigNoz / Telegram callback trace", + "nextAction": "把 signal 併入 AwoooP run dossier,產生可追蹤 fingerprint" + }, + "intake": { + "owner": "AwoooP Run Monitor", + "evidence": "runs list / timeline / alert_operation_log / callback evidence", + "nextAction": "關聯 incident_id、trace_ref、run_id,避免告警只停在 Telegram" + }, + "ai": { + "owner": "OpenClaw 主判斷;Hermes 產 KM 草稿", + "evidence": "AI route status / selected provider / skipped lanes", + "nextAction": "維持 GCP-A → GCP-B → 111 → Gemini fallback 順序並記錄 lane" + }, + "mcp": { + "owner": "MCP Gateway", + "evidence": "K8s / Prometheus / Sentry / SigNoz / Gitea / 自建 MCP 查證結果", + "nextAction": "把 MCP 查證結果寫回 dossier,讓 LLM 不靠猜測判斷" + }, + "playbook": { + "owner": "OpenClaw + PlayBook trust gate", + "evidence": "quality gate / work items / playbook match / execution history", + "nextAction": "補齊 execution、repair、approval、learning evidence 後才允許升級" + }, + "ansible": { + "owner": "AwoooP Executor + Ansible lane", + "evidence": "ansible_runtime / check-mode count / pending check-mode / blockers", + "nextAction": "先解除 ansible_playbook_binary_missing,再跑 check-mode,不直接 apply" + }, + "approval": { + "owner": "Approval Coordinator + SRE owner", + "evidence": "approvals / risk gate / run timeline / manual_required reason", + "nextAction": "低風險才進自動化,高風險保留人工審批與 audit trail" + }, + "verify": { + "owner": "Hermes + KM owner", + "evidence": "KM stale candidates / post-execution verification / playbook learning", + "nextAction": "Hermes 產草稿,owner 審核後寫入 KM,並觀察 stale ratio 下降" + } + } + }, "values": { "verified": "verified {verified}/{evaluated}", "topGate": "{gate} 缺 {count}", diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx index 17fb5056..ea5e5d76 100644 --- a/apps/web/src/app/[locale]/page.tsx +++ b/apps/web/src/app/[locale]/page.tsx @@ -35,6 +35,8 @@ import { AutomationEvidenceCard } from '@/components/dashboard/automation-eviden const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' const STATUS_CHAIN_PREFETCH_LIMIT = 25 const HOMEPAGE_INCIDENT_LIMIT = STATUS_CHAIN_PREFETCH_LIMIT +const HOMEPAGE_BLUEPRINT_STAGE_KEYS = ['signal', 'intake', 'ai', 'mcp', 'playbook', 'ansible', 'approval', 'verify'] as const +type HomepageBlueprintStageKey = typeof HOMEPAGE_BLUEPRINT_STAGE_KEYS[number] interface HomepageAutomationQualitySummary { average_score?: number @@ -119,6 +121,12 @@ interface HomepageWorkItemSummary { tone: HomepageWorkTone } +interface HomepageBlueprintStage extends HomepageWorkItemSummary { + owner: string + evidence: string + nextAction: string +} + async function fetchHomepageJson(url: string, signal?: AbortSignal): Promise { try { const response = await fetch(url, { signal, cache: 'no-store' }) @@ -963,7 +971,7 @@ export default function Home({ params }: { params: { locale: string } }) { ], }, ] - const automationFlowStages: HomepageWorkItemSummary[] = [ + const automationFlowStages: HomepageBlueprintStage[] = [ { key: 'signal', title: tDashboard('automationDiagrams.workspace.flow.stages.signal'), @@ -975,6 +983,9 @@ export default function Home({ params }: { params: { locale: string } }) { }), href: `/${locale}/awooop/runs?project_id=awoooi#tg-callback-evidence`, tone: 'live', + owner: tDashboard('automationDiagrams.workspace.inspector.stages.signal.owner'), + evidence: tDashboard('automationDiagrams.workspace.inspector.stages.signal.evidence'), + nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.signal.nextAction'), }, { key: 'intake', @@ -983,6 +994,9 @@ export default function Home({ params }: { params: { locale: string } }) { detail: tDashboard('automationDelivery.delivered.cicdTimeline.detail'), href: `/${locale}/awooop/runs`, tone: 'live', + owner: tDashboard('automationDiagrams.workspace.inspector.stages.intake.owner'), + evidence: tDashboard('automationDiagrams.workspace.inspector.stages.intake.evidence'), + nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.intake.nextAction'), }, { key: 'ai', @@ -994,6 +1008,9 @@ export default function Home({ params }: { params: { locale: string } }) { }), href: `/${locale}/awooop/work-items?project_id=awoooi`, tone: hasAiRouteStatus ? 'live' : 'watching', + owner: tDashboard('automationDiagrams.workspace.inspector.stages.ai.owner'), + evidence: tDashboard('automationDiagrams.workspace.inspector.stages.ai.evidence'), + nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.ai.nextAction'), }, { key: 'mcp', @@ -1002,6 +1019,9 @@ export default function Home({ params }: { params: { locale: string } }) { detail: tDashboard('automationDiagrams.cards.incidentFlow.detail'), href: `/${locale}/awooop/runs?project_id=awoooi`, tone: 'watching', + owner: tDashboard('automationDiagrams.workspace.inspector.stages.mcp.owner'), + evidence: tDashboard('automationDiagrams.workspace.inspector.stages.mcp.evidence'), + nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.mcp.nextAction'), }, { key: 'playbook', @@ -1013,6 +1033,9 @@ export default function Home({ params }: { params: { locale: string } }) { }), href: `/${locale}/awooop/work-items?project_id=awoooi`, tone: topAutomationGate ? 'blocked' : automationQualityAvailable ? 'live' : 'watching', + owner: tDashboard('automationDiagrams.workspace.inspector.stages.playbook.owner'), + evidence: tDashboard('automationDiagrams.workspace.inspector.stages.playbook.evidence'), + nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.playbook.nextAction'), }, { key: 'ansible', @@ -1025,6 +1048,9 @@ export default function Home({ params }: { params: { locale: string } }) { }), href: `/${locale}/awooop/work-items?project_id=awoooi`, tone: ansibleRuntime?.can_run_check_mode ? 'live' : automationQualityAvailable ? 'blocked' : 'watching', + owner: tDashboard('automationDiagrams.workspace.inspector.stages.ansible.owner'), + evidence: tDashboard('automationDiagrams.workspace.inspector.stages.ansible.evidence'), + nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.ansible.nextAction'), }, { key: 'approval', @@ -1036,6 +1062,9 @@ export default function Home({ params }: { params: { locale: string } }) { }), href: `/${locale}/awooop/approvals`, tone: canClaimFullAutoRepair ? 'live' : automationQualityAvailable ? 'blocked' : 'watching', + owner: tDashboard('automationDiagrams.workspace.inspector.stages.approval.owner'), + evidence: tDashboard('automationDiagrams.workspace.inspector.stages.approval.evidence'), + nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.approval.nextAction'), }, { key: 'verify', @@ -1047,6 +1076,9 @@ export default function Home({ params }: { params: { locale: string } }) { }), href: `/${locale}/knowledge-base`, tone: typeof kmStaleTotal === 'number' && kmStaleTotal > 0 ? 'progress' : hasKmStaleCandidates ? 'live' : 'watching', + owner: tDashboard('automationDiagrams.workspace.inspector.stages.verify.owner'), + evidence: tDashboard('automationDiagrams.workspace.inspector.stages.verify.evidence'), + nextAction: tDashboard('automationDiagrams.workspace.inspector.stages.verify.nextAction'), }, ] const runtimeTopologyLayers = [ @@ -1256,6 +1288,7 @@ export default function Home({ params }: { params: { locale: string } }) { const [infraView, setInfraView] = useState<'host' | 'topo'>('topo') const [selectedHost, setSelectedHost] = useState(null) const [compactViewport, setCompactViewport] = useState(false) + const [selectedBlueprintStageKey, setSelectedBlueprintStageKey] = useState('ansible') // I1 修正: popstate 取代 100ms 輪詢 useEffect(() => { @@ -1282,6 +1315,24 @@ export default function Home({ params }: { params: { locale: string } }) { } }, []) + useEffect(() => { + const syncBlueprintStage = () => { + const requestedStage = new URLSearchParams(window.location.search).get('blueprint_stage') + if (requestedStage && HOMEPAGE_BLUEPRINT_STAGE_KEYS.includes(requestedStage as HomepageBlueprintStageKey)) { + setSelectedBlueprintStageKey(requestedStage as HomepageBlueprintStageKey) + } + } + + syncBlueprintStage() + window.addEventListener('popstate', syncBlueprintStage) + window.addEventListener('hashchange', syncBlueprintStage) + + return () => { + window.removeEventListener('popstate', syncBlueprintStage) + window.removeEventListener('hashchange', syncBlueprintStage) + } + }, []) + const primarySectionMargin = compactViewport ? '10px 8px 0' : '14px 20px 0' const secondarySectionMargin = compactViewport ? '10px 8px 0' : '12px 20px 0' const sectionPadding = compactViewport ? 10 : 14 @@ -1292,6 +1343,9 @@ export default function Home({ params }: { params: { locale: string } }) { 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' + const selectedBlueprintStage = automationFlowStages.find(stage => stage.key === selectedBlueprintStageKey) ?? automationFlowStages[0] + const selectedBlueprintStageIndex = Math.max(automationFlowStages.findIndex(stage => stage.key === selectedBlueprintStage.key), 0) + const selectedBlueprintTone = automationWorkToneStyle[selectedBlueprintStage.tone] return ( @@ -1612,19 +1666,26 @@ export default function Home({ params }: { params: { locale: string } }) { }}> {automationFlowStages.map((stage, index) => { const tone = automationWorkToneStyle[stage.tone] + const isSelectedStage = stage.key === selectedBlueprintStage.key return ( setSelectedBlueprintStageKey(stage.key as HomepageBlueprintStageKey)} + aria-current={isSelectedStage ? 'step' : undefined} style={{ position: 'relative', minHeight: 104, - border: `0.5px solid ${tone.border}`, + border: `0.5px solid ${isSelectedStage ? tone.color : tone.border}`, borderRadius: 8, background: tone.bg, padding: '9px 10px', color: 'inherit', + cursor: 'pointer', + textAlign: 'left', + font: 'inherit', textDecoration: 'none', + boxShadow: isSelectedStage ? `inset 0 0 0 1px ${tone.color}` : 'none', }} >
@@ -1657,6 +1718,95 @@ export default function Home({ params }: { params: { locale: string } }) { ) })}
+
+ +
+ {[ + [tDashboard('automationDiagrams.workspace.inspector.fields.owner'), selectedBlueprintStage.owner], + [tDashboard('automationDiagrams.workspace.inspector.fields.evidence'), selectedBlueprintStage.evidence], + [tDashboard('automationDiagrams.workspace.inspector.fields.nextAction'), selectedBlueprintStage.nextAction], + ].map(([label, value]) => ( +
+
+ {label} +
+
+ {value} +
+
+ ))} +
+