feat(web): add homepage blueprint drilldown
This commit is contained in:
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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={{
|
||||
|
||||
Reference in New Issue
Block a user