Files
blackroad-os-web/app/(app)/logs/page.tsx
Alexa Amundson 458c2c044b feat: real-time live data integration
- lib/live-data.ts: Shared TypeScript client for blackroad-live-data Worker
- components/live-stats.tsx: LiveStatsBar, RecentRepos, AgentStatusGrid components
- app/page.tsx: Import LiveStatsBar in main page header

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-24 14:29:09 -06:00

121 lines
5.8 KiB
TypeScript

'use client'
import { useEffect, useState, useRef } from 'react'
import { Terminal, RefreshCw, Filter, Database, Radio, Cpu, AlertCircle } from 'lucide-react'
interface LogEntry {
type: string; level: string; source: string
message: string; timestamp: string; from?: string; hash?: string
}
const TYPE_COLORS: Record<string, string> = {
memory: '#9C27B0', mesh: '#2979FF', broadcast: '#FF1D6C',
error: '#ef4444', info: '#22c55e', warn: '#F5A623',
}
const TYPE_ICONS: Record<string, any> = {
memory: Database, mesh: Cpu, broadcast: Radio, error: AlertCircle,
}
function timeStr(iso: string) {
try { return new Date(iso).toLocaleTimeString() } catch { return iso }
}
export default function LogsPage() {
const [logs, setLogs] = useState<LogEntry[]>([])
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState('all')
const [autoRefresh, setAutoRefresh] = useState(true)
const [refreshing, setRefreshing] = useState(false)
const bottomRef = useRef<HTMLDivElement>(null)
const load = async () => {
setRefreshing(true)
try {
const r = await fetch(`/api/logs?filter=${filter}`)
const d = await r.json()
setLogs(d.logs || [])
} finally { setRefreshing(false); setLoading(false) }
}
useEffect(() => { load() }, [filter])
useEffect(() => {
if (!autoRefresh) return
const t = setInterval(load, 10000)
return () => clearInterval(t)
}, [autoRefresh, filter])
return (
<div style={{ padding: 32, maxWidth: 1000, height: 'calc(100vh - 40px)', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20, flexShrink: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Terminal size={28} style={{ color: '#22c55e' }} />
<div>
<h1 style={{ fontSize: 24, fontWeight: 700, color: '#fff', margin: 0 }}>Activity Log</h1>
<p style={{ color: 'rgba(255,255,255,0.4)', fontSize: 13, margin: 0 }}>
{logs.length} entries · memory · mesh · agents {autoRefresh && '· auto-refresh 10s'}
</p>
</div>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button
onClick={() => setAutoRefresh(a => !a)}
style={{ padding: '7px 12px', background: autoRefresh ? 'rgba(34,197,94,0.15)' : 'rgba(255,255,255,0.05)', border: `1px solid ${autoRefresh ? 'rgba(34,197,94,0.4)' : 'rgba(255,255,255,0.1)'}`, borderRadius: 8, color: autoRefresh ? '#22c55e' : '#aaa', fontSize: 12, cursor: 'pointer' }}
>
{autoRefresh ? '● Live' : '○ Paused'}
</button>
<button onClick={load} disabled={refreshing} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '7px 12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, color: '#aaa', fontSize: 12, cursor: 'pointer' }}>
<RefreshCw size={12} style={{ animation: refreshing ? 'spin 1s linear infinite' : 'none' }} />Refresh
</button>
</div>
</div>
{/* Filter bar */}
<div style={{ display: 'flex', gap: 8, marginBottom: 16, flexShrink: 0 }}>
{[
{ key: 'all', label: 'All', icon: Filter },
{ key: 'memory', label: 'Memory', icon: Database },
{ key: 'mesh', label: 'Mesh', icon: Cpu },
{ key: 'broadcast', label: 'Broadcast', icon: Radio },
].map(({ key, label, icon: Icon }) => (
<button key={key} onClick={() => setFilter(key)} style={{
display: 'flex', alignItems: 'center', gap: 6, padding: '5px 12px',
background: filter === key ? `${TYPE_COLORS[key] || '#FF1D6C'}22` : 'rgba(255,255,255,0.04)',
border: `1px solid ${filter === key ? (TYPE_COLORS[key] || '#FF1D6C') : 'rgba(255,255,255,0.08)'}`,
borderRadius: 20, color: filter === key ? (TYPE_COLORS[key] || '#FF1D6C') : '#888',
fontSize: 12, cursor: 'pointer',
}}>
<Icon size={11} />{label}
</button>
))}
</div>
{/* Log stream */}
<div style={{ flex: 1, overflowY: 'auto', background: 'rgba(0,0,0,0.4)', border: '1px solid rgba(255,255,255,0.07)', borderRadius: 12, fontFamily: 'monospace', fontSize: 12 }}>
{loading ? (
<div style={{ padding: 40, color: 'rgba(255,255,255,0.3)', textAlign: 'center' }}>Loading</div>
) : logs.length === 0 ? (
<div style={{ padding: 40, color: 'rgba(255,255,255,0.2)', textAlign: 'center' }}>No log entries found</div>
) : logs.map((log, i) => {
const color = TYPE_COLORS[log.type] || '#888'
const Icon = TYPE_ICONS[log.type] || Terminal
return (
<div key={i} style={{
display: 'flex', alignItems: 'flex-start', gap: 10, padding: '7px 14px',
borderBottom: i < logs.length - 1 ? '1px solid rgba(255,255,255,0.04)' : 'none',
transition: 'background .1s',
}}>
<span style={{ color: 'rgba(255,255,255,0.25)', minWidth: 70, flexShrink: 0 }}>{timeStr(log.timestamp)}</span>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 3, fontSize: 10, fontWeight: 700, minWidth: 70, color, background: `${color}15`, padding: '1px 6px', borderRadius: 4, flexShrink: 0 }}>
<Icon size={9} />{log.type}
</span>
<span style={{ color: 'rgba(255,255,255,0.35)', minWidth: 120, flexShrink: 0 }}>{log.source}</span>
<span style={{ color: 'rgba(255,255,255,0.8)', flex: 1, wordBreak: 'break-word' }}>{log.message}</span>
{log.hash && <span style={{ color: 'rgba(255,255,255,0.2)', fontSize: 10 }}>#{log.hash}</span>}
</div>
)
})}
<div ref={bottomRef} />
</div>
</div>
)
}