import { useEffect, useRef, useState } from 'react'; import { invokeCommand as invoke } from '../ipc'; import { save } from '@tauri-apps/plugin-dialog'; import { attachLogger } from '@tauri-apps/plugin-log'; import { FileDown, Trash2, Terminal, Filter, Play, Pause, Info, Copy } from 'lucide-react'; import { WindowDragRegion } from './WindowDragRegion'; import { useToast } from '../contexts/ToastContext'; interface LogEntry { level: 'Trace' | 'Debug' | 'Info' | 'Warn' | 'Error'; message: string; } export default function LogsView() { const { addToast } = useToast(); const [logs, setLogs] = useState([]); const [levelFilter, setLevelFilter] = useState('All'); const [isPaused, setIsPaused] = useState(false); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; text: string } | null>(null); const scrollRef = useRef(null); const rawLineCountRef = useRef(0); const MAX_LOG_LINES = 2000; useEffect(() => { let active = true; let unlisten: (() => void) | undefined; const init = async () => { try { const [lines, currentPauseState] = await Promise.all([ invoke('read_logs', { limit: MAX_LOG_LINES }), invoke('is_log_paused') ]); if (!active) return; setIsPaused(currentPauseState); if (!active) return; const initialLogs = lines.map(message => { const level: LogEntry['level'] = message.includes('[ERROR]') ? 'Error' : message.includes('[WARN]') ? 'Warn' : message.includes('[INFO]') ? 'Info' : message.includes('[TRACE]') ? 'Trace' : 'Debug'; return { level, message }; }); setLogs(initialLogs); rawLineCountRef.current = initialLogs.length; unlisten = await attachLogger((log) => { if (!active) return; const levelStr: LogEntry['level'] = log.level === 5 ? 'Error' : log.level === 4 ? 'Warn' : log.level === 3 ? 'Info' : log.level === 1 ? 'Trace' : 'Debug'; const timeStr = new Date().toISOString().replace('T', ' ').substring(0, 19); const formattedMsg = `[${timeStr}] [${levelStr.toUpperCase()}] ${log.message}`; setLogs(prev => { const newLogs = [...prev, { level: levelStr, message: formattedMsg }]; rawLineCountRef.current = newLogs.length; if (newLogs.length > MAX_LOG_LINES + 500) { const trimmed = newLogs.slice(newLogs.length - MAX_LOG_LINES); return trimmed; } return newLogs; }); }); } catch (e) { console.error('Failed to init logs:', e); } }; void init(); return () => { active = false; if (unlisten) unlisten(); }; }, []); useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [logs]); useEffect(() => { const handleCloseMenu = () => setContextMenu(null); const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape') setContextMenu(null); }; window.addEventListener('click', handleCloseMenu); window.addEventListener('keydown', handleEscape); return () => { window.removeEventListener('click', handleCloseMenu); window.removeEventListener('keydown', handleEscape); }; }, []); const handleContextMenu = (e: React.MouseEvent) => { e.preventDefault(); const selection = window.getSelection()?.toString(); if (selection && selection.trim().length > 0) { setContextMenu({ x: e.clientX, y: e.clientY, text: selection }); } else { setContextMenu(null); } }; const handleCopy = async () => { if (contextMenu?.text) { try { await navigator.clipboard.writeText(contextMenu.text); addToast({ message: 'Copied to clipboard', variant: 'success' }); } catch (err) { console.error('Clipboard write error:', err); addToast({ message: 'Failed to copy to clipboard', variant: 'error' }); } } setContextMenu(null); }; const handleExport = async () => { try { const path = await save({ defaultPath: 'Firelink-Support-Logs.log', filters: [{ name: 'Log Files', extensions: ['log'] }], }); if (!path) return; await invoke('export_logs', { destPath: path }); addToast({ message: 'Support logs exported', variant: 'success' }); } catch (e) { console.error('Export failed:', e); addToast({ message: `Could not export logs: ${String(e)}`, variant: 'error', isActionable: true }); } }; const handleClear = async () => { setLogs([]); await invoke('clear_logs').catch(console.error); }; const severityClass = (level: string) => { switch (level) { case 'Error': return 'log-error'; case 'Warn': return 'log-warn'; case 'Info': return 'log-info'; default: return 'log-debug'; } }; return (
{/* Toolbar */}
Logs ({logs.length} entries)
{/* Privacy Hint */}
Privacy Note: Telemetry securely captures basic hardware capabilities (OS, CPU, RAM) exclusively for troubleshooting. No unique identifiers or sensitive paths are collected. Logs remain entirely offline on your device until manually exported.
{/* Console */}
{logs.length === 0 && (
No persisted log entries are available yet.
)} {logs.filter(entry => levelFilter === 'All' || entry.level === levelFilter).map((entry, i) => (
[{entry.level}] {entry.message}
))}
{/* Context Menu */} {contextMenu && (
e.stopPropagation()} >
)}
); }