From 58fb0155b7097bca559aa01bcae0711d3ed6680c Mon Sep 17 00:00:00 2001 From: NimBold Date: Mon, 22 Jun 2026 21:43:49 +0330 Subject: [PATCH] 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. --- src-tauri/src/lib.rs | 19 +++++++++- src/components/ErrorBoundary.tsx | 64 ++++++++++++++++++++++++++++++++ src/components/LogsView.tsx | 58 +++++++++++++++++++++++++++-- src/ipc.ts | 1 + src/main.tsx | 2 +- 5 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 src/components/ErrorBoundary.tsx diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e4a5820..df8726d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3183,6 +3183,14 @@ async fn read_logs(app_handle: tauri::AppHandle, limit: usize) -> Result 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") diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..e00dd93 --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -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 { + 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 ( +
+
+
+ + + +

Something went wrong.

+
+
+ A critical error occurred in the React component tree. The error details below can help identify the root cause. +
+
+ {this.state.error && this.state.error.toString()} +
+ {this.state.errorInfo && this.state.errorInfo.componentStack} +
+ +
+
+ ); + } + + return this.props.children; + } +} diff --git a/src/components/LogsView.tsx b/src/components/LogsView.tsx index dde5971..055d265 100644 --- a/src/components/LogsView.tsx +++ b/src/components/LogsView.tsx @@ -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([]); 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); @@ -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() { {/* Console */} -
+
{logs.length === 0 && (
No persisted log entries are available yet.
)} @@ -192,6 +228,22 @@ export default function LogsView() {
))}
+ + {/* Context Menu */} + {contextMenu && ( +
+ +
+ )} ); } diff --git a/src/ipc.ts b/src/ipc.ts index 9c6f90f..a3f5dc2 100644 --- a/src/ipc.ts +++ b/src/ipc.ts @@ -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[] }; diff --git a/src/main.tsx b/src/main.tsx index 57e05fb..8e128c7 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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";