import React, { useEffect, useState, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; import { Terminal, Search, Activity, ChevronLeft, ChevronRight, Play, Pause } from 'lucide-react'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from 'recharts'; import api from '../utils/api'; import './Dashboard.css'; interface LogEntry { id: number; timestamp: string; decky: string; service: string; event_type: string; attacker_ip: string; raw_line: string; fields: string; msg: string; } interface HistogramData { time: string; count: number; } const LiveLogs: React.FC = () => { const [searchParams, setSearchParams] = useSearchParams(); // URL-synced state const query = searchParams.get('q') || ''; const timeRange = searchParams.get('time') || '1h'; const isLive = searchParams.get('live') !== 'false'; const page = parseInt(searchParams.get('page') || '1'); // Local state const [logs, setLogs] = useState([]); const [histogram, setHistogram] = useState([]); const [totalLogs, setTotalLogs] = useState(0); const [loading, setLoading] = useState(true); const [streaming, setStreaming] = useState(isLive); const [searchInput, setSearchInput] = useState(query); const eventSourceRef = useRef(null); const limit = 50; // Sync search input if URL changes (e.g. back button) useEffect(() => { setSearchInput(query); }, [query]); const fetchData = async () => { if (streaming) return; // Don't fetch historical if streaming setLoading(true); try { const offset = (page - 1) * limit; let url = `/logs?limit=${limit}&offset=${offset}&search=${encodeURIComponent(query)}`; // Calculate time bounds for historical fetch const now = new Date(); let startTime: string | null = null; if (timeRange !== 'all') { const minutes = timeRange === '15m' ? 15 : timeRange === '1h' ? 60 : timeRange === '24h' ? 1440 : 0; if (minutes > 0) { startTime = new Date(now.getTime() - minutes * 60000).toISOString().replace('T', ' ').substring(0, 19); url += `&start_time=${startTime}`; } } const res = await api.get(url); setLogs(res.data.data); setTotalLogs(res.data.total); // Fetch histogram for historical view let histUrl = `/logs/histogram?search=${encodeURIComponent(query)}`; if (startTime) histUrl += `&start_time=${startTime}`; const histRes = await api.get(histUrl); setHistogram(histRes.data); } catch (err) { console.error('Failed to fetch historical logs', err); } finally { setLoading(false); } }; const setupSSE = () => { if (eventSourceRef.current) { eventSourceRef.current.close(); } const token = localStorage.getItem('token'); const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000/api/v1'; let url = `${baseUrl}/stream?token=${token}&search=${encodeURIComponent(query)}`; if (timeRange !== 'all') { const minutes = timeRange === '15m' ? 15 : timeRange === '1h' ? 60 : timeRange === '24h' ? 1440 : 0; if (minutes > 0) { const startTime = new Date(Date.now() - minutes * 60000).toISOString().replace('T', ' ').substring(0, 19); url += `&start_time=${startTime}`; } } const es = new EventSource(url); eventSourceRef.current = es; es.onmessage = (event) => { try { const payload = JSON.parse(event.data); if (payload.type === 'logs') { setLogs(prev => [...payload.data, ...prev].slice(0, 500)); } else if (payload.type === 'histogram') { setHistogram(payload.data); } else if (payload.type === 'stats') { setTotalLogs(payload.data.total_logs); } } catch (err) { console.error('Failed to parse SSE payload', err); } }; es.onerror = () => { console.error('SSE connection lost, reconnecting...'); }; }; useEffect(() => { if (streaming) { setupSSE(); setLoading(false); } else { if (eventSourceRef.current) { eventSourceRef.current.close(); eventSourceRef.current = null; } fetchData(); } return () => { if (eventSourceRef.current) { eventSourceRef.current.close(); } }; }, [query, timeRange, streaming, page]); const handleSearch = (e: React.FormEvent) => { e.preventDefault(); setSearchParams({ q: searchInput, time: timeRange, live: streaming.toString(), page: '1' }); }; const handleToggleLive = () => { const newStreaming = !streaming; setStreaming(newStreaming); setSearchParams({ q: query, time: timeRange, live: newStreaming.toString(), page: '1' }); }; const handleTimeChange = (newTime: string) => { setSearchParams({ q: query, time: newTime, live: streaming.toString(), page: '1' }); }; const changePage = (newPage: number) => { setSearchParams({ q: query, time: timeRange, live: 'false', page: newPage.toString() }); }; return (
{/* Control Bar */}
setSearchInput(e.target.value)} />
{/* Histogram Chart */}
ATTACK VOLUME OVER TIME
MATCHES: {totalLogs.toLocaleString()}
Math.floor(val).toString()} /> {histogram.map((entry, index) => ( h.count)) || 1)) * 0.4} /> ))}
{/* Logs Table */}

LOG EXPLORER

{!streaming && (
Page {page} of {Math.ceil(totalLogs / limit)}
)}
{logs.length > 0 ? logs.map(log => { let parsedFields: Record = {}; if (log.fields) { try { parsedFields = JSON.parse(log.fields); } catch (e) {} } return ( ); }) : ( )}
TIMESTAMP DECKY SERVICE ATTACKER EVENT
{new Date(log.timestamp).toLocaleString()} {log.decky} {log.service} {log.attacker_ip}
{log.event_type} {log.msg && log.msg !== '-' && — {log.msg}}
{Object.keys(parsedFields).length > 0 && (
{Object.entries(parsedFields).map(([k, v]) => ( {k}: {typeof v === 'object' ? JSON.stringify(v) : v} ))}
)}
{loading ? 'RETRIEVING DATA...' : 'NO LOGS MATCHING CRITERIA'}
); }; export default LiveLogs;