diagnostics: panic hook + frontend error capture + Settings → About diagnostic report

THE BRIEF:
For the friends beta we need verbal feedback AND technical feedback —
some bugs the user cannot describe but a stack trace can. Built it in
two layers, with Layer 3 deferred until there is real volume.

PRIVACY POSTURE (matches Kon's local-first positioning):
- All capture is to disk only. Nothing is transmitted.
- The manual report bundler shows the user exactly what would be
  shared and lets them choose to copy / save it.
- No remote endpoint, no Sentry, no opt-out telemetry.

LAYER 1 — Always-on local capture:
- Rust panic hook (commands/diagnostics.rs::install_panic_hook) writes
  each panic to ~/.kon/crashes/<unix-ts>-<short-id>.crash. Captures
  thread name, OS/arch, RUST_BACKTRACE state, and the panic info.
  Installed before tauri::Builder so it catches setup-time panics too.
- Frontend window.onerror + window.unhandledrejection in
  src/routes/+layout.svelte forward to the new log_frontend_error
  Tauri command, which inserts into the existing error_log SQLite
  table. Best-effort: errors in the error handler itself are swallowed
  so logging can never crash the app.
- Existing error_log table (migration v1) is now actually used —
  closes the TODO from crates/storage/src/database.rs:409.

LAYER 2 — Manual diagnostic report:
- Settings → About → Diagnostics section: Generate report → Preview →
  Copy / Save.
- Report is plain markdown so it pastes cleanly into email, Discord,
  GitHub issues. Sections: app version + OS, sanitised settings JSON,
  recent error_log rows, last 5 crash dumps with previews, log file
  tail (8KB).
- Preview is shown in a <details> with the full text the user can
  inspect before deciding to share.
- Save writes to ~/.kon/diagnostic-reports/kon-diagnostic-<ts>.md.

NEW FILES:
- src-tauri/src/commands/diagnostics.rs (panic hook + 5 Tauri commands)

CHANGES:
- crates/storage/src/file_storage.rs: crashes_dir() + logs_dir() helpers
- crates/storage/src/database.rs: ErrorLogRow + list_recent_errors()
- crates/storage/src/lib.rs: re-exports
- src-tauri/src/commands/mod.rs: + diagnostics module
- src-tauri/src/lib.rs: install_panic_hook() before Builder; 5 new
  Tauri commands registered (log_frontend_error,
  list_recent_errors_command, list_crash_files,
  generate_diagnostic_report, save_diagnostic_report)
- src/routes/+layout.svelte: installGlobalErrorCapture() at mount
- src/lib/pages/SettingsPage.svelte: diagnostic state + buttons +
  preview block at the end of the About section

cargo check -p kon-storage clean. Settings.svelte if/each balanced.

LAYER 3 (deferred):
- Optional opt-in remote reporting (self-hosted Sentry on Tartarus or
  similar). Not for friends beta. Open up after volume justifies it.
This commit is contained in:
2026-04-17 13:47:20 +01:00
parent 524cab0d98
commit 62ea58d95a
10 changed files with 810 additions and 61 deletions

View File

@@ -6,6 +6,7 @@
import TaskSidebar from "$lib/components/TaskSidebar.svelte";
import Titlebar from "$lib/components/Titlebar.svelte";
import ToastViewport from "$lib/components/ToastViewport.svelte";
import { hasTauriRuntime } from "$lib/utils/runtime.js";
import { page, settings, saveSettings } from "$lib/stores/page.svelte.js";
import { getPreferences, updatePreferences } from "$lib/stores/preferences.svelte.js";
import { hasTauriRuntime } from "$lib/utils/runtime.js";
@@ -165,11 +166,43 @@
}
}
// Capture global frontend errors and forward to the Rust error_log via
// log_frontend_error. Best-effort: never let the error handler itself
// throw, never crash the app over a logging failure.
// (Diagnostics layer 1 — local only, never transmitted)
function installGlobalErrorCapture() {
if (!hasTauriRuntime()) return;
const safeLog = (context, message, stack) => {
try {
invoke("log_frontend_error", { context, message: String(message ?? ""), stack: stack ?? null })
.catch(() => { /* swallow — diagnostic logging must never throw */ });
} catch { /* same */ }
};
window.addEventListener("error", (ev) => {
safeLog(
"window.onerror",
ev?.message || ev?.error?.message || "Unknown error",
ev?.error?.stack || null,
);
});
window.addEventListener("unhandledrejection", (ev) => {
const reason = ev?.reason;
const msg = (reason && (reason.message || String(reason))) || "Unhandled rejection";
safeLog("unhandledrejection", msg, reason?.stack || null);
});
}
onMount(async () => {
// Auto-collapse if window is already narrow on first load
handleResize();
window.addEventListener("resize", handleResize);
// Diagnostics: capture every uncaught frontend error to error_log.
installGlobalErrorCapture();
if (!tauriRuntimeAvailable) {
hotkeyBackend = "unavailable";
return;