abbashkyt-creator 7d8ce0e322 V0.1
2026-03-14 04:02:22 +03:00

166 lines
5.7 KiB
TypeScript

'use client'
import { useEffect, useRef, useState, useCallback } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { useEngineStore } from '@/store/engineStore'
interface LogEntry {
id: number
time: string
msg: string
level: 'info' | 'success' | 'warn' | 'error'
}
let _counter = 1
function makeEntry(msg: string, level: LogEntry['level'] = 'info'): LogEntry {
return { id: _counter++, time: new Date().toLocaleTimeString(), msg, level }
}
const LEVEL_STYLES: Record<LogEntry['level'], string> = {
info: 'text-g-muted',
success: 'text-g-green',
warn: 'text-g-amber',
error: 'text-g-red',
}
const LEVEL_DOT: Record<LogEntry['level'], string> = {
info: 'bg-g-faint/40',
success: 'bg-g-green',
warn: 'bg-g-amber',
error: 'bg-g-red',
}
export default function ActivityLog() {
const [entries, setEntries] = useState<LogEntry[]>(() => [
makeEntry('Ghost Node dashboard initialised.', 'success'),
])
const [filter, setFilter] = useState('')
const bottomRef = useRef<HTMLDivElement>(null)
const { status, last_cycle, total_alerts } = useEngineStore()
const prevStatus = useRef<string | null>(null)
const prevCycle = useRef<string | null>(null)
const prevAlerts = useRef<number>(-1)
const push = useCallback((msg: string, level: LogEntry['level'] = 'info') => {
setEntries(prev => [...prev.slice(-199), makeEntry(msg, level)])
}, [])
// Track engine status transitions
useEffect(() => {
if (prevStatus.current === null) { prevStatus.current = status; return }
if (prevStatus.current === status) return
if (status === 'Running') push('Engine started.', 'success')
else if (status === 'Paused') push('Engine paused.', 'warn')
else if (status === 'Idle') push('Engine stopped.', 'warn')
prevStatus.current = status
}, [status, push])
// Track scan cycles
useEffect(() => {
if (prevCycle.current === null) { prevCycle.current = last_cycle; return }
if (prevCycle.current === last_cycle) return
if (last_cycle && last_cycle !== 'Never') {
push('Scan cycle completed.', 'info')
}
prevCycle.current = last_cycle
}, [last_cycle, push])
// Track new alerts
useEffect(() => {
if (prevAlerts.current === -1) { prevAlerts.current = total_alerts; return }
if (total_alerts > prevAlerts.current) {
const delta = total_alerts - prevAlerts.current
push(`${delta} new alert${delta > 1 ? 's' : ''} fired!`, 'success')
}
prevAlerts.current = total_alerts
}, [total_alerts, push])
// Auto-scroll to bottom
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [entries])
const filtered = filter
? entries.filter(e => e.msg.toLowerCase().includes(filter.toLowerCase()))
: entries
return (
<motion.div
initial={{ opacity: 1, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.4, ease: [0.22, 1, 0.36, 1] }}
className="g-card overflow-hidden"
>
{/* ── Header ── */}
<div className="flex items-center gap-3 px-5 py-3.5 border-b border-g-border/40">
<div className="flex items-center gap-2.5">
<span className="g-pulse-dot" />
<span className="text-sm font-bold text-g-text tracking-tight">Activity Log</span>
</div>
<span className="text-[10px] text-g-faint font-mono tabular-nums">
{entries.length} events
</span>
<div className="ml-auto flex items-center gap-2">
<input
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="Filter events…"
className="g-input w-36 h-7 text-[11px] py-0 px-2.5"
/>
<button
onClick={() => bottomRef.current?.scrollIntoView({ behavior: 'smooth' })}
className="g-btn h-7 px-2 text-xs"
title="Scroll to bottom"
>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/>
</svg>
</button>
<button
onClick={() => setEntries([])}
className="g-btn h-7 px-2.5 text-xs hover:!text-g-red hover:!border-g-red/30"
>
Clear
</button>
</div>
</div>
{/* ── Body ── */}
<div className="h-48 overflow-y-auto p-4 font-mono">
{filtered.length === 0 ? (
<p className="text-g-faint/30 text-[11px] text-center py-8">No events</p>
) : (
<AnimatePresence mode="popLayout" initial={false}>
{filtered.map(e => (
<motion.div
key={e.id}
initial={{ opacity: 1, x: -6 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
className="flex items-start gap-3 py-1.5 group"
>
<span className="shrink-0 text-[10px] text-g-faint/40 pt-px tabular-nums leading-relaxed">
{e.time}
</span>
<span
className={`w-1 h-1 rounded-full mt-1.5 shrink-0 ${LEVEL_DOT[e.level]}`}
/>
<span
className={`text-[11px] leading-relaxed transition-opacity group-hover:opacity-100 opacity-80 ${LEVEL_STYLES[e.level]}`}
>
{e.msg}
</span>
</motion.div>
))}
</AnimatePresence>
)}
<div ref={bottomRef} />
</div>
</motion.div>
)
}