feat(web): add homepage blueprint drilldown
All checks were successful
CD Pipeline / tests (push) Successful in 1m19s
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / build-and-deploy (push) Successful in 4m5s
CD Pipeline / post-deploy-checks (push) Successful in 1m33s

This commit is contained in:
Your Name
2026-05-26 10:44:45 +08:00
parent 87545bc7dd
commit 6aec9489d4
3 changed files with 255 additions and 3 deletions

View File

@@ -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}",

View File

@@ -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}",

View File

@@ -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<T>(url: string, signal?: AbortSignal): Promise<T | null> {
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<Host | null>(null)
const [compactViewport, setCompactViewport] = useState(false)
const [selectedBlueprintStageKey, setSelectedBlueprintStageKey] = useState<HomepageBlueprintStageKey>('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 (
<AppLayout locale={locale} showBackground={false} fullBleed>
@@ -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 (
<a
key={stage.key}
href={stage.href}
href={`/${locale}?blueprint_stage=${stage.key}#blueprint-stage-inspector`}
onClick={() => 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',
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
@@ -1657,6 +1718,95 @@ export default function Home({ params }: { params: { locale: string } }) {
)
})}
</div>
<div style={{
scrollMarginTop: 96,
marginTop: 10,
borderTop: '0.5px solid #e0ddd4',
paddingTop: 10,
display: 'grid',
gridTemplateColumns: compactViewport ? '1fr' : 'minmax(160px, 0.75fr) minmax(0, 1fr)',
gap: 10,
}}
id="blueprint-stage-inspector"
>
<div style={{
border: `0.5px solid ${selectedBlueprintTone.border}`,
background: selectedBlueprintTone.bg,
borderRadius: 8,
padding: '9px 10px',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
width: 24,
height: 24,
borderRadius: 999,
border: `0.5px solid ${selectedBlueprintTone.border}`,
background: '#fff',
color: selectedBlueprintTone.color,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 10,
fontWeight: 800,
}}>
{selectedBlueprintStageIndex + 1}
</div>
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 10, fontWeight: 800, color: '#77736a', textTransform: 'uppercase', letterSpacing: 0.5 }}>
{tDashboard('automationDiagrams.workspace.inspector.title')}
</div>
<div style={{ marginTop: 2, fontSize: 13, fontWeight: 800, color: '#141413', lineHeight: 1.25 }}>
{selectedBlueprintStage.title}
</div>
</div>
</div>
<div style={{ marginTop: 8, fontSize: 11, color: '#5f5b52', lineHeight: 1.45 }}>
{selectedBlueprintStage.detail}
</div>
<a
href={selectedBlueprintStage.href}
style={{
display: 'inline-flex',
marginTop: 9,
border: `0.5px solid ${selectedBlueprintTone.border}`,
background: '#fff',
color: selectedBlueprintTone.color,
borderRadius: 6,
padding: '5px 8px',
fontSize: 10,
fontWeight: 800,
textDecoration: 'none',
}}
>
{tDashboard('automationDiagrams.workspace.inspector.openTarget')}
</a>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: compactViewport ? '1fr' : 'repeat(auto-fit, minmax(190px, 1fr))',
gap: 8,
}}>
{[
[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]) => (
<div key={label} style={{
border: '0.5px solid #e0ddd4',
borderRadius: 7,
background: '#fff',
padding: '8px 9px',
}}>
<div style={{ fontSize: 9, fontWeight: 800, color: '#77736a', textTransform: 'uppercase', letterSpacing: 0.5 }}>
{label}
</div>
<div style={{ marginTop: 5, fontSize: 11, fontWeight: 700, color: '#2e2b26', lineHeight: 1.45, overflowWrap: 'anywhere' }}>
{value}
</div>
</div>
))}
</div>
</div>
</div>
<div style={{