feat(phase-6.4g-6.5b): API Synaptic Integration + Dual-State WarRoom UI
Phase 6.4g (API 突觸對接):
- lewooogo-brain dependency binding in apps/api/pyproject.toml
- POST /api/v1/incidents/{id}/propose route (proposals.py)
- Guardrails integration (8/8 tests passed)
Phase 6.5a (視覺皮層建置):
- DualStateIncidentCard.tsx with Nothing.tech visual compliance
- Ping radar animation for alert state
- Tier-based decision layer UI (AI 執行中 / 等待親核)
Phase 6.5b (神經網路串接):
- Main warroom page integration (page.tsx)
- IncidentResponse → DualState mapper function
- Empty state: "系統穩定。0 活躍異常。"
Tests:
- test_guardrails.py (8/8)
- test_incident_engine.py (6/6)
- test_skill_loader.py (6/6)
- Frontend build: 0 errors
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,8 +25,58 @@ import { OpenClawStateMachine } from '@/components/ai/openclaw-state-machine'
|
||||
import { GlobalPulseChart } from '@/components/charts/global-pulse-chart'
|
||||
import { useGlobalPulseMetrics } from '@/hooks/useGlobalPulseMetrics'
|
||||
import { useIncidents } from '@/hooks/useIncidents'
|
||||
import { IncidentCard, IncidentCardGrid, IncidentEmptyState, ThinkingTerminal, DEMO_DECISION_CHAIN } from '@/components/incident'
|
||||
import { Activity, AlertTriangle } from 'lucide-react'
|
||||
import {
|
||||
IncidentCard,
|
||||
IncidentCardGrid,
|
||||
IncidentEmptyState,
|
||||
ThinkingTerminal,
|
||||
DEMO_DECISION_CHAIN,
|
||||
DualStateIncidentCard,
|
||||
} from '@/components/incident'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
import type { IncidentResponse } from '@/lib/api-client'
|
||||
|
||||
// =============================================================================
|
||||
// Utility: Map IncidentResponse to DualStateIncidentCard props
|
||||
// =============================================================================
|
||||
|
||||
function mapToDualState(incident: IncidentResponse): {
|
||||
id: string
|
||||
serviceName: string
|
||||
status: 'normal' | 'alert'
|
||||
tier?: 1 | 2 | 3
|
||||
message: string
|
||||
timestamp: string
|
||||
} {
|
||||
// P0/P1 視為異常 (alert),P2/P3 視為正常 (normal)
|
||||
const isAlert = incident.severity === 'P0' || incident.severity === 'P1'
|
||||
|
||||
// Tier 判定: proposal_count > 0 且為 P0 = Tier 3, P1 = Tier 2, else Tier 1
|
||||
let tier: 1 | 2 | 3 | undefined = undefined
|
||||
if (isAlert && incident.proposal_count > 0) {
|
||||
tier = incident.severity === 'P0' ? 3 : 2
|
||||
} else if (isAlert) {
|
||||
tier = 1
|
||||
}
|
||||
|
||||
// 格式化時間
|
||||
const date = new Date(incident.created_at)
|
||||
const timestamp = date.toLocaleString('zh-TW', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
|
||||
return {
|
||||
id: incident.incident_id,
|
||||
serviceName: incident.affected_services[0] || 'unknown',
|
||||
status: isAlert ? 'alert' : 'normal',
|
||||
tier,
|
||||
message: `${incident.signal_count} 筆告警 | ${incident.status}`,
|
||||
timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main Page
|
||||
@@ -103,7 +153,7 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
<LiveDashboard locale={locale} />
|
||||
</DataPincerPanel>
|
||||
|
||||
{/* Active Incidents Section (Phase 7: 真實血脈) */}
|
||||
{/* Active Incidents Section (Phase 7: 真實血脈 + Phase 6.5b 雙態卡片) */}
|
||||
<DataPincerPanel
|
||||
title={t('incident.activeIncidents')}
|
||||
status={incidents.length > 0 ? 'critical' : 'healthy'}
|
||||
@@ -121,16 +171,31 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
<span className="font-mono text-sm">{incidentsError}</span>
|
||||
</div>
|
||||
) : incidents.length === 0 ? (
|
||||
<IncidentEmptyState />
|
||||
/* Nothing.tech 風格平靜態: 系統穩定 */
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="w-3 h-3 rounded-full bg-green-500 mb-4 animate-pulse" />
|
||||
<p className="font-mono text-sm text-neutral-600">
|
||||
{t('incident.systemStable', { defaultValue: '系統穩定' })}
|
||||
</p>
|
||||
<p className="font-mono text-xs text-neutral-400 mt-1">
|
||||
0 {t('incident.activeAlerts', { defaultValue: '活躍異常' })}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<IncidentCardGrid>
|
||||
{incidents.map((incident) => (
|
||||
<IncidentCard
|
||||
key={incident.incident_id}
|
||||
incident={incident}
|
||||
/>
|
||||
))}
|
||||
</IncidentCardGrid>
|
||||
<div className="space-y-4">
|
||||
{/* Phase 6.5b: 雙態戰情室卡片 (脈衝雷達 + Tier 決策層) */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{incidents.map((incident) => {
|
||||
const dualProps = mapToDualState(incident)
|
||||
return (
|
||||
<DualStateIncidentCard
|
||||
key={`dual-${incident.incident_id}`}
|
||||
{...dualProps}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DataPincerPanel>
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* DualStateIncidentCard - Phase 6.5a 雙態戰情室卡片
|
||||
* ==================================================
|
||||
*
|
||||
* Nothing.tech 視覺憲法:
|
||||
* - 純白極簡 (bg-white/90)
|
||||
* - 無深色模式
|
||||
* - 嚴禁陰影 (shadow-none)
|
||||
* - 細邊框 (border-[0.5px])
|
||||
*
|
||||
* 雙態設計:
|
||||
* - normal: 淺灰邊框,靜態
|
||||
* - alert: 紅色邊框,脈衝雷達動畫
|
||||
*
|
||||
* 統帥鐵律: 禁止假數據!
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export interface DualStateIncidentCardProps {
|
||||
id: string
|
||||
serviceName: string
|
||||
status: 'normal' | 'alert'
|
||||
tier?: 1 | 2 | 3
|
||||
message: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export const DualStateIncidentCard: React.FC<DualStateIncidentCardProps> = ({
|
||||
id,
|
||||
serviceName,
|
||||
status,
|
||||
tier,
|
||||
message,
|
||||
timestamp,
|
||||
}) => {
|
||||
const isAlert = status === 'alert'
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
relative p-4 w-full max-w-md font-mono text-sm transition-all duration-300
|
||||
bg-white/90 backdrop-blur-md
|
||||
${isAlert ? 'border border-red-500' : 'border-[0.5px] border-neutral-200'}
|
||||
shadow-none
|
||||
`}
|
||||
>
|
||||
{/* 異常脈衝雷達 (Ping Animation) */}
|
||||
{isAlert && (
|
||||
<span className="absolute top-4 right-4 flex h-2.5 w-2.5">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-red-600"></span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* 標頭資訊 */}
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<span className="text-neutral-400 text-xs">{id}</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 text-xs tracking-wider border-[0.5px] ${
|
||||
isAlert
|
||||
? 'bg-red-50 text-red-600 border-red-200'
|
||||
: 'bg-neutral-50 text-neutral-500 border-neutral-200'
|
||||
}`}
|
||||
>
|
||||
{serviceName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 核心數據與訊息 */}
|
||||
<div
|
||||
className={`mt-2 font-bold tracking-wide ${isAlert ? 'text-red-600' : 'text-neutral-800'}`}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-neutral-400">{timestamp}</div>
|
||||
|
||||
{/* 大腦決策層 (Proposal UI) */}
|
||||
{isAlert && tier && (
|
||||
<div className="mt-4 pt-3 border-t-[0.5px] border-red-200 flex justify-between items-center">
|
||||
<span className="text-xs text-neutral-500">
|
||||
{tier === 1 ? '>_ AI 執行中 (Tier 1)' : `>_ 等待統帥親核 (Tier ${tier})`}
|
||||
</span>
|
||||
{tier > 1 && (
|
||||
<button className="px-3 py-1 bg-neutral-900 text-white text-xs hover:bg-black transition-colors cursor-pointer">
|
||||
[ Y / n ]
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DualStateIncidentCard
|
||||
@@ -1,8 +1,12 @@
|
||||
/**
|
||||
* Incident Components - Phase 7
|
||||
* Incident Components - Phase 7 + 6.5a
|
||||
*/
|
||||
|
||||
export { IncidentCard, IncidentCardGrid, IncidentEmptyState } from './incident-card'
|
||||
export {
|
||||
DualStateIncidentCard,
|
||||
type DualStateIncidentCardProps,
|
||||
} from './dual-state-incident-card'
|
||||
export {
|
||||
ThinkingTerminal,
|
||||
DEMO_DECISION_CHAIN,
|
||||
|
||||
Reference in New Issue
Block a user