mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
feat: enhance log system with true deletion, noise filtering, and error boundaries
- Implemented `clear_logs` Tauri command to physically wipe local log files. - Added a global `ErrorBoundary` to cleanly intercept React render loops (e.g. Error #185) and provide readable stack traces. - Hardened `tauri_plugin_log` to filter out `webview`, `hyper`, `reqwest`, `rustls` log noise. - Replaced the default browser right-click menu in LogsView with a custom native React menu for copying text securely.
This commit is contained in:
+18
-1
@@ -3183,6 +3183,14 @@ async fn read_logs(app_handle: tauri::AppHandle, limit: usize) -> Result<Vec<Str
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn clear_logs(app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
for file in log_files(&app_handle).await? {
|
||||
let _ = tokio::fs::write(&file, "").await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn export_logs(
|
||||
app_handle: tauri::AppHandle,
|
||||
@@ -4081,6 +4089,15 @@ pub fn run() {
|
||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None }),
|
||||
])
|
||||
.level(if cfg!(debug_assertions) { log::LevelFilter::Debug } else { log::LevelFilter::Info })
|
||||
.filter(|metadata| {
|
||||
let target = metadata.target();
|
||||
!target.starts_with("webview::")
|
||||
&& !target.starts_with("hyper")
|
||||
&& !target.starts_with("reqwest")
|
||||
&& !target.starts_with("rustls")
|
||||
&& !target.starts_with("h2")
|
||||
&& !target.starts_with("tower")
|
||||
})
|
||||
.max_file_size(10_000_000)
|
||||
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepSome(3))
|
||||
.timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal)
|
||||
@@ -4116,7 +4133,7 @@ pub fn run() {
|
||||
parity::create_category_directories,
|
||||
db_save_settings, db_load_settings, db_get_all_downloads, db_replace_downloads,
|
||||
db_get_all_queues, db_replace_queues,
|
||||
read_logs, export_logs, toggle_log_pause, is_log_paused
|
||||
read_logs, export_logs, toggle_log_pause, is_log_paused, clear_logs
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Component, ErrorInfo, ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
errorInfo: ErrorInfo | null;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
public state: State = {
|
||||
hasError: false,
|
||||
error: null,
|
||||
errorInfo: null
|
||||
};
|
||||
|
||||
public static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error, errorInfo: null };
|
||||
}
|
||||
|
||||
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo
|
||||
});
|
||||
console.error('Uncaught error:', error, errorInfo);
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="h-screen w-screen flex flex-col items-center justify-center bg-bg-primary text-text-primary p-8 overflow-auto">
|
||||
<div className="max-w-2xl w-full bg-bg-secondary p-6 rounded-lg border border-red-900/50 shadow-2xl">
|
||||
<div className="flex items-center gap-3 mb-4 text-red-500">
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<h1 className="text-xl font-bold">Something went wrong.</h1>
|
||||
</div>
|
||||
<div className="text-sm text-text-secondary mb-6">
|
||||
A critical error occurred in the React component tree. The error details below can help identify the root cause.
|
||||
</div>
|
||||
<div className="bg-black/50 p-4 rounded text-xs font-mono text-red-300 overflow-x-auto mb-4">
|
||||
{this.state.error && this.state.error.toString()}
|
||||
<br />
|
||||
{this.state.errorInfo && this.state.errorInfo.componentStack}
|
||||
</div>
|
||||
<button
|
||||
className="app-button px-4 py-2 bg-red-600/20 text-red-400 border border-red-600/50 hover:bg-red-600/30 transition-colors rounded"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
Reload App
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ 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 } from 'lucide-react';
|
||||
import { FileDown, Trash2, Terminal, Filter, Play, Pause, Info, Copy } from 'lucide-react';
|
||||
import { WindowDragRegion } from './WindowDragRegion';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function LogsView() {
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
const [levelFilter, setLevelFilter] = useState<LogEntry['level'] | 'All'>('All');
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; text: string } | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const rawLineCountRef = useRef(0);
|
||||
|
||||
@@ -86,6 +87,35 @@ export default function LogsView() {
|
||||
}
|
||||
}, [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) {
|
||||
await navigator.clipboard.writeText(contextMenu.text);
|
||||
addToast({ message: 'Copied to clipboard', variant: 'success' });
|
||||
}
|
||||
setContextMenu(null);
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
const path = await save({
|
||||
@@ -101,8 +131,9 @@ export default function LogsView() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
const handleClear = async () => {
|
||||
setLogs([]);
|
||||
await invoke('clear_logs').catch(console.error);
|
||||
};
|
||||
|
||||
const severityClass = (level: string) => {
|
||||
@@ -181,7 +212,12 @@ export default function LogsView() {
|
||||
</div>
|
||||
|
||||
{/* Console */}
|
||||
<div ref={scrollRef} className="logs-console flex-1 overflow-y-auto p-3 font-mono text-[11px] leading-[1.5] select-text" style={{ userSelect: 'text', WebkitUserSelect: 'text' }}>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onContextMenu={handleContextMenu}
|
||||
className="logs-console flex-1 overflow-y-auto p-3 font-mono text-[11px] leading-[1.5] select-text"
|
||||
style={{ userSelect: 'text', WebkitUserSelect: 'text' }}
|
||||
>
|
||||
{logs.length === 0 && (
|
||||
<div className="text-text-muted italic select-none">No persisted log entries are available yet.</div>
|
||||
)}
|
||||
@@ -192,6 +228,22 @@ export default function LogsView() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Context Menu */}
|
||||
{contextMenu && (
|
||||
<div
|
||||
className="fixed z-[100] bg-bg-secondary border border-border-modal rounded-md shadow-lg py-1 min-w-[120px]"
|
||||
style={{ left: Math.min(contextMenu.x, window.innerWidth - 120), top: Math.min(contextMenu.y, window.innerHeight - 40) }}
|
||||
>
|
||||
<button
|
||||
className="w-full text-left px-3 py-1.5 flex items-center hover:bg-item-hover text-[12px] text-text-primary"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<Copy size={13} className="mr-2 text-text-secondary" />
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ type CommandMap = {
|
||||
};
|
||||
export_logs: { args: { destPath: string }; result: string };
|
||||
read_logs: { args: { limit: number }; result: string[] };
|
||||
clear_logs: { args: undefined; result: void };
|
||||
toggle_log_pause: { args: { pause: boolean }; result: void };
|
||||
is_log_paused: { args: undefined; result: boolean };
|
||||
get_pending_order: { args: { queueId: string | null }; result: string[] };
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { ErrorBoundary } from "./ErrorBoundary";
|
||||
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||
import { ToastProvider } from "./contexts/ToastContext";
|
||||
import { error as logError, warn as logWarn } from "@tauri-apps/plugin-log";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user