feat(web): add homepage blueprint drilldown
This commit is contained in:
@@ -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