feat: reveal log file from settings; quiet per-tick DHT peer log

The "DHT: got N peers" line fired at info every 20s per torrent and would
dominate the rotated desktop log; drop it to debug.

Add a Logs section to the Sources/settings dialog showing the resolved log
path with a "Reveal in file manager" button, backed by two new Tauri
commands (get_log_path, open_log_file). open_log_file reveals the file via
tauri-plugin-opener (reveal rather than open, so it doesn't depend on a .log
handler); the opener:allow-reveal-item-in-dir permission is granted. The
resolved log path is stored on AppState at construction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 18:15:01 +03:00
parent 7151889adc
commit 29e13db4b6
8 changed files with 93 additions and 5 deletions

View File

@@ -1081,7 +1081,7 @@ fn dht_peer_discovery(
let query = || {
let all_peers = dht.get_peers(info_hash_id);
for peers in all_peers {
log::info!("DHT: got {} peers", peers.len());
log::debug!("DHT: got {} peers", peers.len());
if cmd_tx.send(Command::ConnectToPeers(peers)).is_err() {
return false; // torrent stopped
}

View File

@@ -6,6 +6,7 @@
"core:default",
"dialog:default",
"notification:default",
"opener:default"
"opener:default",
"opener:allow-reveal-item-in-dir"
]
}

View File

@@ -1 +1 @@
{"default":{"identifier":"default","description":"Default capabilities for Monsoon","local":true,"windows":["main"],"permissions":["core:default","dialog:default","notification:default","opener:default"]}}
{"default":{"identifier":"default","description":"Default capabilities for Monsoon","local":true,"windows":["main"],"permissions":["core:default","dialog:default","notification:default","opener:default","opener:allow-reveal-item-in-dir"]}}

View File

@@ -166,3 +166,21 @@ pub fn remove_remote_server(name: String, state: State<'_, Arc<AppState>>) -> Re
pub fn take_pending_magnet(state: State<'_, Arc<AppState>>) -> Option<String> {
state.pending_magnet.lock().ok().and_then(|mut p| p.take())
}
/// The resolved path to the desktop app's rotating log file, for display.
#[tauri::command]
pub fn get_log_path(state: State<'_, Arc<AppState>>) -> String {
state.log_file.to_string_lossy().into_owned()
}
/// Reveal the log file in the system file manager. Revealing (rather than
/// opening) avoids depending on a `.log` handler being registered.
#[tauri::command]
pub fn open_log_file(state: State<'_, Arc<AppState>>) -> Result<(), String> {
use tauri_plugin_opener::OpenerExt;
state
.app
.opener()
.reveal_item_in_dir(&state.log_file)
.map_err(|e| e.to_string())
}

View File

@@ -107,6 +107,7 @@ pub fn run(initial_magnet: Option<String>) {
let event_rx = manager.event_rx.clone();
let desktop_cfg = desktop_config::DesktopConfig::load();
let log_file = paths.log_file.clone();
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
@@ -151,13 +152,15 @@ pub fn run(initial_magnet: Option<String>) {
commands::add_remote_server,
commands::remove_remote_server,
commands::take_pending_magnet,
commands::get_log_path,
commands::open_log_file,
])
.setup(move |app| {
let app_handle = app.handle().clone();
// Build and manage the unified AppState (local manager + config
// + active view). All commands resolve through this.
let state = source::AppState::new(manager, desktop_cfg, app_handle.clone());
let state = source::AppState::new(manager, desktop_cfg, app_handle.clone(), log_file);
app.manage(state.clone());
// Scheduler tick for the local manager: independent of the event

View File

@@ -20,6 +20,8 @@ pub struct AppState {
/// up yet. Set on first-launch (before the GUI mounts) and as a fallback
/// for second-instance handoff. Cleared by `take_pending_magnet`.
pub pending_magnet: Mutex<Option<String>>,
/// Resolved path to the rotating log file, surfaced in the UI.
pub log_file: std::path::PathBuf,
}
/// The "currently viewed source" — drives what `get_torrents` returns and
@@ -46,13 +48,19 @@ impl View {
}
impl AppState {
pub fn new(local: TorrentManager, config: DesktopConfig, app: AppHandle) -> Arc<Self> {
pub fn new(
local: TorrentManager,
config: DesktopConfig,
app: AppHandle,
log_file: std::path::PathBuf,
) -> Arc<Self> {
Arc::new(Self {
local: Mutex::new(local),
config: Mutex::new(config),
view: AsyncMutex::new(View::local()),
app,
pending_magnet: Mutex::new(None),
log_file,
})
}

View File

@@ -3,6 +3,8 @@
addRemoteServer,
removeRemoteServer,
setDefaultSource,
getLogPath,
openLogFile,
} from "./ipc";
import { defaultSource, refreshSources, sources } from "./stores";
@@ -12,6 +14,22 @@
let labelInput = $state("");
let urlInput = $state("");
let error = $state<string | null>(null);
let logPath = $state("");
$effect(() => {
getLogPath()
.then((p) => (logPath = p))
.catch(() => {});
});
async function revealLog() {
error = null;
try {
await openLogFile();
} catch (e) {
error = String(e);
}
}
async function setDefault(id: string) {
error = null;
@@ -123,6 +141,14 @@
<button class="primary" onclick={addRemote}>Add</button>
</div>
<h3>Logs</h3>
<div class="logs">
<code class="log-path">{logPath || "…"}</code>
<button onclick={revealLog} disabled={!logPath}>
Reveal in file manager
</button>
</div>
{#if error}
<p class="error">{error}</p>
{/if}
@@ -262,6 +288,28 @@
background: rgba(199, 67, 67, 0.1);
}
.logs {
display: flex;
align-items: center;
gap: 12px;
}
.log-path {
flex: 1;
font-size: 12px;
color: var(--fg-secondary);
background: var(--bg-secondary);
padding: 6px 10px;
border-radius: var(--radius);
overflow-x: auto;
white-space: nowrap;
user-select: text;
}
.logs button {
flex-shrink: 0;
}
.error {
color: #c74343;
font-size: 13px;

View File

@@ -138,3 +138,13 @@ export async function removeRemoteServer(name: string): Promise<void> {
export async function takePendingMagnet(): Promise<string | null> {
return invoke("take_pending_magnet");
}
/** The resolved path to the desktop app's rotating log file. */
export async function getLogPath(): Promise<string> {
return invoke("get_log_path");
}
/** Reveal the log file in the system file manager. */
export async function openLogFile(): Promise<void> {
return invoke("open_log_file");
}