feat(web): S6 OpenClaw AI Terminal + 狀態數據 — Sprint 5R
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 13m15s

- 分隔線下方新增:模型名稱 + 運行狀態
- 即時統計:今日分析數 / 成功率 / MTTR
- AI 推理終端:#141413 背景 + #a0e8a0 螢光綠 + JetBrains Mono
- 最後一行黃色閃爍游標 ▎
- 資料來源:/api/v1/alert-operation-logs + /api/v1/stats/disposition

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-09 15:56:03 +08:00
parent b85a0e232e
commit a0f3a7d532

View File

@@ -192,9 +192,95 @@ export function OpenClawPanel({
</span>
</div>
)}
{/* Sprint 5R S6: 狀態數據 + AI Terminal */}
<div style={{ marginTop: 8, paddingTop: 8, borderTop: '0.5px solid #e0ddd4' }}>
<div style={{ display: 'flex', gap: 8, marginBottom: 4 }}>
<div style={{ flex: 1, fontSize: 10, color: '#555550' }}>
: <span style={{ fontWeight: 600, color: '#141413' }}>openclaw_nemo</span>
</div>
<div style={{ fontSize: 10, color: '#22C55E', fontWeight: 500 }}> </div>
</div>
<OpenClawStats />
<OpenClawTerminal />
</div>
</div>
</div>
)
}
/** AI 即時統計 */
function OpenClawStats() {
const [stats, setStats] = useState<{ analyses: number; successRate: string; mttr: string }>({ analyses: 0, successRate: '--', mttr: '--' })
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
useEffect(() => {
Promise.all([
fetch(`${API_BASE}/api/v1/stats/disposition`).then(r => r.ok ? r.json() : null).catch(() => null),
fetch(`${API_BASE}/api/v1/incidents`).then(r => r.ok ? r.json() : null).catch(() => null),
]).then(([disp, inc]) => {
const total = disp?.summary?.total ?? 0
const rate = disp?.summary?.auto_rate != null ? `${Math.round(disp.summary.auto_rate * 100)}%` : '--'
const incidents = inc?.incidents ?? inc ?? []
const resolved = incidents.filter((i: any) => i.updated_at && (i.status === 'resolved' || i.status === 'closed'))
let mttr = '--'
if (resolved.length > 0) {
const avg = resolved.reduce((sum: number, i: any) => sum + (new Date(i.updated_at).getTime() - new Date(i.created_at).getTime()), 0) / resolved.length
mttr = `${(avg / 60000).toFixed(1)}m`
}
setStats({ analyses: total, successRate: rate, mttr })
})
}, [API_BASE])
return (
<div style={{ display: 'flex', gap: 12, fontSize: 10, color: '#555550' }}>
<span>: <b style={{ color: '#141413' }}>{stats.analyses}</b></span>
<span>: <b style={{ color: '#22C55E' }}>{stats.successRate}</b></span>
<span>MTTR: <b style={{ color: '#141413' }}>{stats.mttr}</b></span>
</div>
)
}
/** AI 推理終端 */
function OpenClawTerminal() {
const [logs, setLogs] = useState<string[]>([])
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
useEffect(() => {
fetch(`${API_BASE}/api/v1/alert-operation-logs?limit=4`)
.then(r => r.ok ? r.json() : { items: [] })
.then(d => {
const items = (d.items ?? []).reverse()
setLogs(items.map((item: any) => {
const time = (() => { try { return new Date(item.created_at).toLocaleTimeString('zh-TW', { timeZone: 'Asia/Taipei', hour: '2-digit', minute: '2-digit' }) } catch { return '--:--' } })()
return `[${time}] ${item.action_detail || item.event_type.replace(/_/g, ' ')}`
}))
})
.catch(() => {})
}, [API_BASE])
return (
<div style={{
background: '#141413', borderRadius: 6, padding: '8px 10px', marginTop: 8,
fontFamily: "'JetBrains Mono', monospace", fontSize: 10, color: '#a0e8a0',
lineHeight: 1.6, maxHeight: 80, overflowY: 'auto',
}}>
<style>{`@keyframes cursor-blink { 0%,100%{opacity:1} 50%{opacity:0} }`}</style>
{logs.length === 0 ? (
<span style={{ color: '#555' }}>[--:--] Scanning cluster state...</span>
) : (
logs.map((line, i) => (
<div key={i}>
{i < logs.length - 1 ? (
<><span style={{ color: '#555' }}>{line.slice(0, 7)}</span>{line.slice(7)}</>
) : (
<span style={{ color: '#ffd700' }}>{line} <span style={{ animation: 'cursor-blink 1s step-end infinite' }}></span></span>
)}
</div>
))
)}
</div>
)
}
export default OpenClawPanel