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

@@ -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={{