mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-27 11:07:08 +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)
|
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]
|
#[tauri::command]
|
||||||
async fn export_logs(
|
async fn export_logs(
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
@@ -4081,6 +4089,15 @@ pub fn run() {
|
|||||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None }),
|
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 })
|
.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)
|
.max_file_size(10_000_000)
|
||||||
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepSome(3))
|
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepSome(3))
|
||||||
.timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal)
|
.timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal)
|
||||||
@@ -4116,7 +4133,7 @@ pub fn run() {
|
|||||||
parity::create_category_directories,
|
parity::create_category_directories,
|
||||||
db_save_settings, db_load_settings, db_get_all_downloads, db_replace_downloads,
|
db_save_settings, db_load_settings, db_get_all_downloads, db_replace_downloads,
|
||||||
db_get_all_queues, db_replace_queues,
|
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!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while building tauri application")
|
.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 { invokeCommand as invoke } from '../ipc';
|
||||||
import { save } from '@tauri-apps/plugin-dialog';
|
import { save } from '@tauri-apps/plugin-dialog';
|
||||||
import { attachLogger } from '@tauri-apps/plugin-log';
|
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 { WindowDragRegion } from './WindowDragRegion';
|
||||||
import { useToast } from '../contexts/ToastContext';
|
import { useToast } from '../contexts/ToastContext';
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ export default function LogsView() {
|
|||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
const [levelFilter, setLevelFilter] = useState<LogEntry['level'] | 'All'>('All');
|
const [levelFilter, setLevelFilter] = useState<LogEntry['level'] | 'All'>('All');
|
||||||
const [isPaused, setIsPaused] = useState(false);
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
|
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; text: string } | null>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const rawLineCountRef = useRef(0);
|
const rawLineCountRef = useRef(0);
|
||||||
|
|
||||||
@@ -86,6 +87,35 @@ export default function LogsView() {
|
|||||||
}
|
}
|
||||||
}, [logs]);
|
}, [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 () => {
|
const handleExport = async () => {
|
||||||
try {
|
try {
|
||||||
const path = await save({
|
const path = await save({
|
||||||
@@ -101,8 +131,9 @@ export default function LogsView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = async () => {
|
||||||
setLogs([]);
|
setLogs([]);
|
||||||
|
await invoke('clear_logs').catch(console.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
const severityClass = (level: string) => {
|
const severityClass = (level: string) => {
|
||||||
@@ -181,7 +212,12 @@ export default function LogsView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Console */}
|
{/* 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 && (
|
{logs.length === 0 && (
|
||||||
<div className="text-text-muted italic select-none">No persisted log entries are available yet.</div>
|
<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>
|
||||||
))}
|
))}
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ type CommandMap = {
|
|||||||
};
|
};
|
||||||
export_logs: { args: { destPath: string }; result: string };
|
export_logs: { args: { destPath: string }; result: string };
|
||||||
read_logs: { args: { limit: number }; result: string[] };
|
read_logs: { args: { limit: number }; result: string[] };
|
||||||
|
clear_logs: { args: undefined; result: void };
|
||||||
toggle_log_pause: { args: { pause: boolean }; result: void };
|
toggle_log_pause: { args: { pause: boolean }; result: void };
|
||||||
is_log_paused: { args: undefined; result: boolean };
|
is_log_paused: { args: undefined; result: boolean };
|
||||||
get_pending_order: { args: { queueId: string | null }; result: string[] };
|
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 { createRoot } from "react-dom/client";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import { ErrorBoundary } from "./ErrorBoundary";
|
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||||
import { ToastProvider } from "./contexts/ToastContext";
|
import { ToastProvider } from "./contexts/ToastContext";
|
||||||
import { error as logError, warn as logWarn } from "@tauri-apps/plugin-log";
|
import { error as logError, warn as logWarn } from "@tauri-apps/plugin-log";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user