feat(observability): add centralized diagnostics logging engine and native log viewer panel

- Configure tauri-plugin-log with 10MB rotation and 3-file retention
- Add high-frequency format filter to strip [download]% progress ticks
- Hook yt-dlp, aria2c, and native reqwest runners with lifecycle log macros
- Add export_logs Tauri command for async log file export via save dialog
- Create DiagnosticsView React component with streaming monospace console
- Apply severity highlighting: ERROR=red, WARN=yellow, INFO=grey, DEBUG=dim
- Wire Diagnostics tab into sidebar navigation and ActiveView routing
- Log native download failures to log::error! in download.rs
This commit is contained in:
NimBold
2026-06-17 10:57:32 +03:30
parent 69a7192fda
commit cb39113117
9 changed files with 207 additions and 4 deletions
+105
View File
@@ -0,0 +1,105 @@
import { useEffect, useRef, useState } from 'react';
import { listen } from '@tauri-apps/api/event';
import { invokeCommand as invoke } from '../ipc';
import { save } from '@tauri-apps/plugin-dialog';
import { FileDown, Trash2, Terminal } from 'lucide-react';
import { WindowDragRegion } from './WindowDragRegion';
interface LogEntry {
level: 'Trace' | 'Debug' | 'Info' | 'Warn' | 'Error';
message: string;
}
export default function DiagnosticsView() {
const [logs, setLogs] = useState<LogEntry[]>([]);
const scrollRef = useRef<HTMLDivElement>(null);
const MAX_LOG_LINES = 2000;
useEffect(() => {
const unlisten = listen<{ level: string; message: string }>('log', (event) => {
const level = event.payload.level as LogEntry['level'];
const message = event.payload.message;
if (message.includes('[download]') && message.includes('%')) return;
setLogs(prev => {
const next = [...prev, { level, message }];
return next.length > MAX_LOG_LINES ? next.slice(-MAX_LOG_LINES) : next;
});
});
return () => { unlisten.then(f => f()); };
}, []);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [logs]);
const handleExport = async () => {
try {
const path = await save({
defaultPath: 'Firelink-Diagnostics.log',
filters: [{ name: 'Log Files', extensions: ['log'] }],
});
if (!path) return;
await invoke('export_logs', { destPath: path });
} catch (e) {
console.error('Export failed:', e);
}
};
const handleClear = () => setLogs([]);
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 (
<div className="diagnostics-view flex-1 flex flex-col h-full overflow-hidden">
<WindowDragRegion />
{/* Toolbar */}
<div className="diagnostics-toolbar flex items-center justify-between px-4 py-2 shrink-0">
<div className="flex items-center gap-2 text-text-secondary">
<Terminal size={16} strokeWidth={1.8} />
<span className="text-[13px] font-semibold text-text-primary">Diagnostics Console</span>
<span className="text-[11px] text-text-muted">({logs.length} entries)</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleClear}
className="app-icon-button"
title="Clear console"
>
<Trash2 size={14} />
</button>
<button
onClick={handleExport}
className="app-button px-3 text-[11px] gap-1.5"
title="Export logs"
>
<FileDown size={13} />
Export Logs
</button>
</div>
</div>
{/* Console */}
<div ref={scrollRef} className="diagnostics-console flex-1 overflow-y-auto p-3 font-mono text-[11px] leading-[1.5]">
{logs.length === 0 && (
<div className="text-text-muted italic select-none">Waiting for log entries...</div>
)}
{logs.map((entry, i) => (
<div key={i} className={`log-line ${severityClass(entry.level)}`}>
<span className="log-level-tag">[{entry.level}]</span>
<span className="log-message">{entry.message}</span>
</div>
))}
</div>
</div>
);
}
+2 -1
View File
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import {
Inbox, Zap, CheckCircle2, CircleDashed,
Film, Music, FileText, Box, Image as ImageIcon, Archive, FileQuestion,
List, CalendarClock, Gauge, Settings, Plus, Play, Pause, Edit2, Trash2, PanelLeft,
List, CalendarClock, Gauge, Bug, Settings, Plus, Play, Pause, Edit2, Trash2, PanelLeft,
type LucideIcon
} from 'lucide-react';
import { useDownloadStore, DownloadCategory, Queue } from '../store/useDownloadStore';
@@ -229,6 +229,7 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
<div className="sidebar-section-label">Tools</div>
<ToolItem icon={CalendarClock} label="Scheduler" view="scheduler" />
<ToolItem icon={Gauge} label="Speed Limiter" view="speedLimiter" />
<ToolItem icon={Bug} label="Diagnostics" view="diagnostics" />
</section>
</div>