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

@@ -428,6 +428,41 @@ pub async fn log_error(
Ok(())
}
/// Read the most recent N rows from `error_log`, newest first. Used by the
/// diagnostic-report bundler in Settings → About.
#[derive(Debug, Clone)]
pub struct ErrorLogRow {
pub id: i64,
pub timestamp: String,
pub context: String,
pub error_code: Option<String>,
pub message: String,
pub metadata: Option<String>,
}
pub async fn list_recent_errors(pool: &SqlitePool, limit: i64) -> Result<Vec<ErrorLogRow>> {
let rows = sqlx::query(
"SELECT id, timestamp, context, error_code, message, metadata \
FROM error_log ORDER BY id DESC LIMIT ?",
)
.bind(limit)
.fetch_all(pool)
.await
.map_err(|e| KonError::StorageError(format!("Read error_log failed: {e}")))?;
Ok(rows
.into_iter()
.map(|r| ErrorLogRow {
id: r.get("id"),
timestamp: r.get("timestamp"),
context: r.get("context"),
error_code: r.get("error_code"),
message: r.get("message"),
metadata: r.get("metadata"),
})
.collect())
}
#[cfg(test)]
mod tests {
use super::*;