Skip to content

Commit a4804e6

Browse files
pmaxhoganclaude
andcommitted
feat(app): opt-in debug logging mode and safer, richer diagnostic bundles
Closes #309, closes #204. instead of cloning GlobalSettings and patching one field, so a future secret-bearing field fails to compile here rather than leaking silently. Fixes three concrete leaks: pre/post_backup_hook command lines (a classic home for embedded secrets) are now redacted wholesale, custom_root_ca_path is hashed like every other path in the bundle, and proxy_url in PAC mode (a local file path, not a URL) is now hashed instead of passing the userinfo-strip-only path through untouched. The ProxyError Display leak the issue also flagged was already fixed by #208 - verified, not touched. always-visible amber warning, backed by a real runtime-reloadable tracing filter (logging.rs) that raises Driven's own crates to trace level while on. The toggle persists an epoch-ms expiry and auto-turns-off 24h after being enabled, enforced by a boot-time reconcile plus a periodic watchdog (debug_mode.rs) so the window is honoured even across a restart. The rolling log cap widens from 25 MB to 250 MB while debug mode is on. The diagnostic bundle gains a DEBUG_MODE.txt notice and an unredacted debug/engine_state.txt when debug mode is on - the one deliberate exception to the #204 redaction rules, gated on the user's explicit opt-in. Every bundle now also ships manifest.txt (entry name + size). Activity's export button shows an amber "Debug data included" chip while debug mode is on. Also closes the long-documented gap where global.log_level only exported RUST_LOG for the next launch - it now reloads the live filter too. Testing: 18 Rust redaction tests (leak-shaped fixtures for hooks, CA path, PAC-mode path, PAC-mode URL, and one full end-to-end fixture asserting the serialized bundle JSON), 5 debug_mode watchdog/expiry tests, 5 settings-persistence round-trip tests. cargo test -p driven-app --lib: 458 passed. cargo clippy --workspace --all-targets -- -D warnings: clean. cargo fmt --all -- --check: clean. pnpm vitest run: 793 passed across 60 files (new: activity-debug-chip.test.ts, plus PrivacyPage toggle tests in settings-pages.test.ts). vue-tsc --noEmit: clean. Linux visual baselines regenerated via `just visual-update` (privacy.png light+dark) and pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019xKUm9vH4ifb5LHR5szy1v
1 parent d462592 commit a4804e6

19 files changed

Lines changed: 1595 additions & 47 deletions

File tree

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ Notes:
9696
- ³¹ rclone and restic are command-line tools; their GUIs are separate third-party projects (for example RcloneView, Backrest).
9797
- ³² Duplicati runs as a background service with a local web UI plus a tray helper, not a native desktop app.
9898
- ³³ restic search and selective restore are driven from the CLI (or a mounted snapshot), not an in-app browser.
99-
- ³⁴ Driven writes daily rolling logs (pruned at 14 days / 25 MB) that interleave backend tracing with the webview's own console output, and the in-app diagnostics export bundles them.
99+
- ³⁴ Driven writes daily rolling logs (pruned at 14 days / 25 MB, widened to 250 MB while the opt-in Debug logging toggle is on) that interleave backend tracing with the webview's own console output, and the in-app diagnostics export bundles them.
100100
- ³⁵ rclone logs to a file only when you pass `--log-file` (rotation via `--log-file-max-size` and friends), and has no bundle export; its bug template asks you to attach a log you produced by hand.
101101
- ³⁶ Duplicati's "Create bug report" export is a genuine one-click bundle (system info plus an obfuscated copy of the local database), and its web UI has a live log view; file logging is opt-in via `--log-file`, defaults to warnings only, and does not rotate.
102102
- ³⁷ restic has no log-file option at all - output goes to stdout, and the only file logging is an unrotated `DEBUG_LOG` env var that its contributing guide asks you to redact yourself.
@@ -189,7 +189,12 @@ These move: check each project's current docs before relying on a cell.
189189
- Rolling local log files covering both the backend and the webview console,
190190
collected into a one-click diagnostics bundle alongside a redacted summary of
191191
in-flight upload recovery state and a trailing window of process-memory
192-
samples.
192+
samples. An opt-in Debug logging toggle (Settings > Privacy & Data) raises
193+
logs to per-file / IPC-trace / engine-state detail and widens the bundle
194+
with an unredacted engine-state snapshot for the hardest bugs - it warns you
195+
up front that this logs file names, full paths, and timing data, can slow
196+
backups down, and turns itself off automatically after 24 hours; every other
197+
part of the bundle stays redacted regardless.
193198
- In-app auto-update with signed update manifests and a stable / dev channel
194199
selector.
195200
- Anonymous, opt-out telemetry (coarse counts only; never file names, paths, or

src-tauri/src/app_state.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,12 @@ pub struct AppState {
308308
/// issue #308 (2026-08-17 follow-up): live bottleneck-classification
309309
/// sampling runtime (the Activity dashboard's Bottleneck stat tile).
310310
bottleneck: BottleneckRuntime,
311+
/// issue #309: the debug-logging-mode 24h auto-off watchdog's task handle
312+
/// + shutdown signal, so the app-quit drain joins it with no orphan
313+
/// (mirrors [`UpdaterRuntime`]/[`IostatRuntime`]). No shared "hub" field
314+
/// like those two - the watchdog only reads/writes the persisted settings
315+
/// KV directly, nothing else on `AppState` needs to observe it.
316+
debug_mode: DebugModeRuntime,
311317
/// The ONE in-flight streaming exclusion preview
312318
/// ([`crate::commands::exclusion_stream`]). The exclusion editor re-previews
313319
/// on every rule edit, so without a single-slot registry a user tweaking
@@ -440,6 +446,22 @@ pub struct BottleneckRuntime {
440446
shutdown: std::sync::Mutex<Option<watch::Sender<bool>>>,
441447
}
442448

449+
/// issue #309: the debug-logging-mode 24h auto-off watchdog's runtime state
450+
/// held on [`AppState`] - just the task's lifecycle slots (mirrors
451+
/// [`UpdaterRuntime`]/[`TelemetryRuntime`]'s task+shutdown pair). No "hub"
452+
/// field like [`BottleneckRuntime`]/[`IostatRuntime`]: the watchdog reads and
453+
/// writes the persisted `global.debug_logging_*` settings directly via its
454+
/// `StateRepo` handle, so there is nothing else on `AppState` for another
455+
/// caller to read.
456+
#[derive(Default)]
457+
pub struct DebugModeRuntime {
458+
/// The spawned watchdog task, behind `Option` so the shutdown drain can
459+
/// TAKE + await it by value; `None` once drained / never spawned.
460+
task: std::sync::Mutex<Option<JoinHandle<()>>>,
461+
/// The shutdown signal the watchdog `select!`s on.
462+
shutdown: std::sync::Mutex<Option<watch::Sender<bool>>>,
463+
}
464+
443465
/// M9b (SPEC s16): the anonymous-telemetry runtime state held on [`AppState`].
444466
///
445467
/// `task` + `shutdown` track the single app-wide periodic-ping task so the quit
@@ -702,6 +724,7 @@ impl AppState {
702724
updater: UpdaterRuntime::default(),
703725
iostat: IostatRuntime::default(),
704726
bottleneck: BottleneckRuntime::default(),
727+
debug_mode: DebugModeRuntime::default(),
705728
exclusion_previews: Arc::default(),
706729
preview_tree_cache: Arc::default(),
707730
telemetry: TelemetryRuntime::default(),
@@ -1104,6 +1127,44 @@ impl AppState {
11041127
.take()
11051128
}
11061129

1130+
// --- issue #309: debug-logging-mode watchdog runtime ----------------------
1131+
1132+
/// Register the spawned watchdog task + its shutdown sender so the
1133+
/// app-quit drain can stop + join it with no orphan (mirrors
1134+
/// [`Self::set_bottleneck_task`]).
1135+
pub fn set_debug_mode_task(&self, task: JoinHandle<()>, shutdown: watch::Sender<bool>) {
1136+
*self
1137+
.debug_mode
1138+
.task
1139+
.lock()
1140+
.unwrap_or_else(|e| e.into_inner()) = Some(task);
1141+
*self
1142+
.debug_mode
1143+
.shutdown
1144+
.lock()
1145+
.unwrap_or_else(|e| e.into_inner()) = Some(shutdown);
1146+
}
1147+
1148+
/// Signal the watchdog to stop and TAKE its handle so the quit drain can
1149+
/// await it. Mirrors [`Self::shutdown_bottleneck_task`].
1150+
#[must_use]
1151+
pub fn shutdown_debug_mode_task(&self) -> Option<JoinHandle<()>> {
1152+
if let Some(tx) = self
1153+
.debug_mode
1154+
.shutdown
1155+
.lock()
1156+
.unwrap_or_else(|e| e.into_inner())
1157+
.take()
1158+
{
1159+
let _ = tx.send(true);
1160+
}
1161+
self.debug_mode
1162+
.task
1163+
.lock()
1164+
.unwrap_or_else(|e| e.into_inner())
1165+
.take()
1166+
}
1167+
11071168
// --- M9b telemetry runtime (SPEC s16) ----------------------------------
11081169

11091170
/// M9b: register the spawned periodic-ping task + its shutdown sender so the

src-tauri/src/commands/dtos.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,23 @@ pub struct GlobalSettings {
968968
/// the unchanged V1 behaviour; `false` exempts EXACTLY the reachability
969969
/// pause-reason family from the gate (a captive portal still pauses).
970970
pub pause_when_offline: bool,
971+
/// Issue #309: debug logging mode. When `true` the live `tracing` filter
972+
/// is raised to a verbose directive (per-file activity, IPC traces, state
973+
/// transitions, reconcile/queue decisions) and the diagnostic bundle
974+
/// gains a debug section; both cost noticeable performance and log
975+
/// verbose paths/timings, so this defaults `false` and auto-clears itself
976+
/// (see [`Self::debug_logging_expires_at_ms`]). `serde(default)` so a
977+
/// `global` blob persisted before this field still deserialises as off.
978+
#[serde(default)]
979+
pub debug_logging_enabled: bool,
980+
/// Issue #309: the epoch-ms deadline [`Self::debug_logging_enabled`]
981+
/// auto-turns-off at (set to `now + 24h` whenever the toggle is switched
982+
/// on). Persisted (not just an in-memory timer) so a restart mid-window
983+
/// still honours the original deadline rather than granting a fresh 24h.
984+
/// `None` when debug logging is off. `serde(default)` for the same
985+
/// pre-#309 backward-compat reason as the toggle itself.
986+
#[serde(default)]
987+
pub debug_logging_expires_at_ms: Option<i64>,
971988
}
972989

973990
/// V2 schedule-window settings (DESIGN s17). Mirrors
@@ -1288,6 +1305,11 @@ pub struct GlobalSettingsPatch {
12881305
pub proxy_url: Option<Option<String>>,
12891306
/// See [`GlobalSettings::pause_when_offline`]. Absent = unchanged.
12901307
pub pause_when_offline: Option<bool>,
1308+
/// See [`GlobalSettings::debug_logging_enabled`]. Absent = unchanged;
1309+
/// present = set it (the backend computes/clears
1310+
/// `debug_logging_expires_at_ms` itself - that field is not directly
1311+
/// patchable from the webview).
1312+
pub debug_logging_enabled: Option<bool>,
12911313
}
12921314

12931315
/// Partial SPEC s22 `telemetry` settings.

0 commit comments

Comments
 (0)