87 lines
3.1 KiB
TypeScript
87 lines
3.1 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* RecentActivity — 最近活動時間線
|
|
* Sprint 5R S5: 設計稿 L416-429
|
|
* @created 2026-04-09 Claude Opus 4.6 Asia/Taipei
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useTranslations } from 'next-intl'
|
|
|
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
|
|
|
|
interface LogEntry {
|
|
id: string
|
|
event_type: string
|
|
action_detail: string | null
|
|
actor: string | null
|
|
created_at: string
|
|
}
|
|
|
|
const EVENT_COLOR: Record<string, string> = {
|
|
RESOLVED: '#22C55E',
|
|
EXECUTION_COMPLETED: '#22C55E',
|
|
ALERT_RECEIVED: '#cc2200',
|
|
AUTO_REPAIR_TRIGGERED: '#4A90D9',
|
|
TELEGRAM_SENT: '#4A90D9',
|
|
EXECUTION_STARTED: '#F59E0B',
|
|
}
|
|
|
|
export function RecentActivity() {
|
|
const t = useTranslations('dashboard')
|
|
const [logs, setLogs] = useState<LogEntry[]>([])
|
|
|
|
useEffect(() => {
|
|
fetch(`${API_BASE}/api/v1/alert-operation-logs?limit=5`)
|
|
.then(r => r.ok ? r.json() : { items: [] })
|
|
.then(d => setLogs(d.items ?? []))
|
|
.catch(() => {})
|
|
}, [])
|
|
|
|
if (logs.length === 0) return null
|
|
|
|
return (
|
|
<div style={{
|
|
background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 10,
|
|
overflow: 'hidden', boxShadow: '0 1px 3px rgba(0,0,0,0.04)', flexShrink: 0,
|
|
}}>
|
|
<div style={{
|
|
padding: '10px 14px', borderBottom: '0.5px solid #e0ddd4',
|
|
display: 'flex', alignItems: 'center', gap: 8, background: '#faf9f3',
|
|
}}>
|
|
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757' }} />
|
|
<span style={{ fontSize: 14, fontWeight: 700, color: '#141413', letterSpacing: '0.5px' }}>{t('activityStream')}</span>
|
|
<span style={{ marginLeft: 'auto', fontSize: 11, color: '#4A90D9', cursor: 'pointer', fontWeight: 500 }}>{t('viewAllAlerts')} →</span>
|
|
</div>
|
|
<div style={{ padding: '10px 14px' }}>
|
|
{logs.map((log, i) => {
|
|
const time = (() => {
|
|
try {
|
|
return new Date(log.created_at).toLocaleTimeString('zh-TW', { timeZone: 'Asia/Taipei', hour: '2-digit', minute: '2-digit' })
|
|
} catch { return '--' }
|
|
})()
|
|
const dotColor = EVENT_COLOR[log.event_type] ?? '#87867f'
|
|
const detail = log.action_detail || log.event_type.replace(/_/g, ' ').toLowerCase()
|
|
|
|
return (
|
|
<div key={log.id || i} style={{
|
|
display: 'flex', gap: 8, padding: '6px 0',
|
|
borderBottom: i < logs.length - 1 ? '0.5px solid #f0ede5' : 'none',
|
|
fontSize: 12,
|
|
}}>
|
|
<span style={{ fontSize: 10, color: '#87867f', fontFamily: "'JetBrains Mono', monospace", width: 40, flexShrink: 0 }}>{time}</span>
|
|
<span style={{ width: 4, height: 4, borderRadius: '50%', background: dotColor, marginTop: 5, flexShrink: 0 }} />
|
|
<span style={{ flex: 1, minWidth: 0, lineHeight: 1.4, color: '#555550', overflowWrap: 'anywhere', wordBreak: 'break-word' }}>
|
|
{log.actor && <b style={{ fontWeight: 600 }}>{log.actor}</b>}
|
|
{log.actor && ' · '}
|
|
{detail}
|
|
</span>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|