mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-04 00:18:41 +00:00
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:
@@ -14,6 +14,7 @@ import { useSettingsStore } from "./store/useSettingsStore";
|
||||
import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
|
||||
import SchedulerView from "./components/SchedulerView";
|
||||
import SpeedLimiterView from "./components/SpeedLimiterView";
|
||||
import DiagnosticsView from "./components/DiagnosticsView";
|
||||
|
||||
function App() {
|
||||
const [filter, setFilter] = useState<SidebarFilter>('all');
|
||||
@@ -305,6 +306,7 @@ function App() {
|
||||
{activeView === 'settings' && <SettingsView />}
|
||||
{activeView === 'scheduler' && <SchedulerView />}
|
||||
{activeView === 'speedLimiter' && <SpeedLimiterView />}
|
||||
{activeView === 'diagnostics' && <DiagnosticsView />}
|
||||
</div>
|
||||
|
||||
{/* Status Bar */}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ActiveView = "downloads" | "settings" | "scheduler" | "speedLimiter";
|
||||
export type ActiveView = "downloads" | "settings" | "scheduler" | "speedLimiter" | "diagnostics";
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
|
||||
@@ -1169,6 +1169,66 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Diagnostics Console */
|
||||
.diagnostics-toolbar {
|
||||
height: 42px;
|
||||
border-bottom: 1px solid hsl(var(--border-color));
|
||||
background: hsl(var(--statusbar-bg));
|
||||
}
|
||||
|
||||
.diagnostics-console {
|
||||
background: hsl(0 0% 7%);
|
||||
color: hsl(0 0% 82%);
|
||||
font-family: "SF Mono", Monaco, "Cascadia Code", "Fira Code", "JetBrains Mono", monospace;
|
||||
}
|
||||
|
||||
.diagnostics-console .log-line {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 0 4px;
|
||||
min-height: 18px;
|
||||
align-items: baseline;
|
||||
border-radius: 2px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.diagnostics-console .log-level-tag {
|
||||
flex-shrink: 0;
|
||||
font-weight: 600;
|
||||
min-width: 52px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.diagnostics-console .log-message {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.diagnostics-console .log-error .log-level-tag,
|
||||
.diagnostics-console .log-error .log-message {
|
||||
color: hsl(0 72% 58%);
|
||||
}
|
||||
|
||||
.diagnostics-console .log-warn .log-level-tag,
|
||||
.diagnostics-console .log-warn .log-message {
|
||||
color: hsl(45 100% 50%);
|
||||
}
|
||||
|
||||
.diagnostics-console .log-info .log-level-tag,
|
||||
.diagnostics-console .log-info .log-message {
|
||||
color: hsl(0 0% 75%);
|
||||
}
|
||||
|
||||
.diagnostics-console .log-debug .log-level-tag,
|
||||
.diagnostics-console .log-debug .log-message {
|
||||
color: hsl(0 0% 45%);
|
||||
}
|
||||
|
||||
.diagnostics-console .log-line:hover {
|
||||
background: hsl(0 0% 100% / 0.04);
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
|
||||
@@ -100,6 +100,7 @@ type CommandMap = {
|
||||
db_save_queue: { args: { id: string; data: string }; result: void };
|
||||
db_delete_queue: { args: { id: string }; result: void };
|
||||
create_category_directories: { args: { paths: string[] }; result: void };
|
||||
export_logs: { args: { destPath: string }; result: string };
|
||||
get_pending_order: { args: undefined; result: string[] };
|
||||
enqueue_download: { args: { item: any }; result: string };
|
||||
enqueue_many: { args: { items: any[] }; result: void };
|
||||
|
||||
Reference in New Issue
Block a user