From f325402902ab85d258e2def40cbbd7dd35937ecf Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 25 Jul 2026 10:18:06 -0400 Subject: [PATCH] desktop: robust startup + operation logging (portability troubleshooting) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There was essentially no logging — useless for proving the app renders across different environments. Add real observability: - tauri-plugin-log -> stdout (so `2>&1 | tee` captures a run) AND a persistent file in the app log dir (grabbable after the fact on any machine). Level Info. - Startup diagnostics: app version, OS/arch, the Linux display/session stack (XDG_SESSION_TYPE, desktop, Wayland/X11, GDK_BACKEND), the WebKit render- hardening vars actually in effect, resolved log + data dirs, DB open/migrate result, and note/label counts. - log_event command + a frontend logEvent() helper: boot line (data source + WebKit user-agent) from main.ts, first-route config/session/destination from the router guard, and — via the bridge invoke() wrapper — every failed Tauri command named with its error, so a broken basic function is self-identifying. Task 2040. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm --- desktop/src-tauri/Cargo.toml | 4 ++ desktop/src-tauri/src/lib.rs | 75 +++++++++++++++++++++++++++++- desktop/src-tauri/src/local/mod.rs | 14 ++++++ frontend/src/desktop/bridge.ts | 13 +++++- frontend/src/main.ts | 5 ++ frontend/src/router/index.ts | 12 +++++ 6 files changed, 121 insertions(+), 2 deletions(-) diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index cfb7de3..b60ecfc 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -24,6 +24,10 @@ rusqlite = { version = "0.32", features = ["bundled"] } uuid = { version = "1", features = ["v4"] } # RFC3339 timestamps for created_at/updated_at/remind_at (Date.parse-able on the JS side). chrono = { version = "0.4", default-features = false, features = ["clock"] } +# Startup + operation logging to stdout AND a persistent file, so portability +# issues are diagnosable from any environment. `log` is the facade the code uses. +tauri-plugin-log = "2" +log = "0.4" # Tauri's default release profile: smaller, faster shipped binaries. [profile.release] diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 8af6499..cfec12a 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -11,21 +11,41 @@ mod local; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + use tauri_plugin_log::{Target, TargetKind}; + #[cfg(target_os = "linux")] harden_linux_webkit_rendering(); tauri::Builder::default() + // Logging first, so startup diagnostics (and any setup error) are captured to + // stdout AND a persistent file from the very beginning — the basis for + // troubleshooting portability across environments. + .plugin( + tauri_plugin_log::Builder::new() + .level(log::LevelFilter::Info) + .targets([ + Target::new(TargetKind::Stdout), + Target::new(TargetKind::LogDir { file_name: None }), + ]) + .build(), + ) .setup(|app| { use tauri::Manager; + log_environment(app); // The on-device store lives in the platform app-data dir (e.g. Linux // ~/.local/share/com.fabledsword.thoughtsync/thoughtsync.db), created on // first launch. This is what makes the app work with no server or login. let dir = app.path().app_data_dir()?; std::fs::create_dir_all(&dir)?; - app.manage(local::open(&dir.join("thoughtsync.db"))?); + let db_path = dir.join("thoughtsync.db"); + log::info!("opening local store: {}", db_path.display()); + let db = local::open(&db_path)?; + log::info!("local store ready — {}", local::summary(&db)); + app.manage(db); Ok(()) }) .invoke_handler(tauri::generate_handler![ + log_event, integration::integration_status, integration::integrate_desktop, integration::unintegrate_desktop, @@ -70,6 +90,59 @@ pub fn run() { .expect("error while running the ThoughtSync desktop app"); } +/// Frontend logging bridge: routes boot milestones and errors from the webview into +/// the same stdout + file log as the Rust side (see frontend/src/desktop/bridge.ts). +#[tauri::command] +fn log_event(level: String, message: String) { + match level.as_str() { + "error" => log::error!(target: "frontend", "{message}"), + "warn" => log::warn!(target: "frontend", "{message}"), + "debug" => log::debug!(target: "frontend", "{message}"), + _ => log::info!(target: "frontend", "{message}"), + } +} + +/// Log the app version and the environment that determines whether the window +/// renders — the first thing to check when a build works on one machine but not +/// another. +fn log_environment(app: &tauri::App) { + use tauri::Manager; + log::info!( + "ThoughtSync desktop v{} starting ({} {})", + env!("CARGO_PKG_VERSION"), + std::env::consts::OS, + std::env::consts::ARCH, + ); + match app.path().app_log_dir() { + Ok(d) => log::info!("log directory: {}", d.display()), + Err(e) => log::warn!("could not resolve log dir: {e}"), + } + #[cfg(target_os = "linux")] + log_linux_graphics_env(); +} + +/// The Linux display + graphics stack, and the WebKit render-hardening vars actually +/// in effect (set by harden_linux_webkit_rendering, which runs before the logger, so +/// we report the resulting environment rather than logging from inside it). +#[cfg(target_os = "linux")] +fn log_linux_graphics_env() { + let v = |k: &str| std::env::var(k).unwrap_or_else(|_| "(unset)".to_string()); + log::info!( + "display: session_type={} desktop={} wayland={} x11={} gdk_backend={}", + v("XDG_SESSION_TYPE"), + v("XDG_CURRENT_DESKTOP"), + v("WAYLAND_DISPLAY"), + v("DISPLAY"), + v("GDK_BACKEND"), + ); + log::info!( + "webkit hardening: dmabuf_disabled={} compositing_disabled={} nv_explicit_sync_disabled={}", + v("WEBKIT_DISABLE_DMABUF_RENDERER"), + v("WEBKIT_DISABLE_COMPOSITING_MODE"), + v("__NV_DISABLE_EXPLICIT_SYNC"), + ); +} + /// WebKitGTK's GPU-accelerated rendering (the DMA-BUF renderer + EGL compositing) fails /// to initialize on a wide range of Linux GPU/driver/Wayland setups — "Could not create /// default EGL display: EGL_BAD_PARAMETER" → a black/blank window. This is a well-known diff --git a/desktop/src-tauri/src/local/mod.rs b/desktop/src-tauri/src/local/mod.rs index b4232a6..1ece189 100644 --- a/desktop/src-tauri/src/local/mod.rs +++ b/desktop/src-tauri/src/local/mod.rs @@ -24,3 +24,17 @@ pub fn open(path: &Path) -> rusqlite::Result { schema::migrate(&conn)?; Ok(Db(Mutex::new(conn))) } + +/// A one-line count summary of the store, for the startup log. +pub fn summary(db: &Db) -> String { + let conn = match db.0.lock() { + Ok(c) => c, + Err(_) => return "counts unavailable (lock poisoned)".to_string(), + }; + let count = |sql: &str| conn.query_row(sql, [], |r| r.get::<_, i64>(0)).unwrap_or(-1); + format!( + "{} notes, {} labels", + count("SELECT COUNT(*) FROM notes"), + count("SELECT COUNT(*) FROM labels"), + ) +} diff --git a/frontend/src/desktop/bridge.ts b/frontend/src/desktop/bridge.ts index b3cff5c..3d8a33b 100644 --- a/frontend/src/desktop/bridge.ts +++ b/frontend/src/desktop/bridge.ts @@ -27,7 +27,18 @@ export function isDesktop(): boolean { export function invoke(cmd: string, args?: Record): Promise { const tauri = window.__TAURI__; if (!tauri) return Promise.reject(new Error("Not running in the desktop app.")); - return tauri.core.invoke(cmd, args); + return tauri.core.invoke(cmd, args).catch((err: unknown) => { + // Every failed command names itself in the log — so a broken "basic function" + // in some environment is diagnosable. Skip log_event to avoid recursion. + if (cmd !== "log_event") logEvent("error", `invoke '${cmd}' failed: ${String(err)}`); + throw err; + }); +} + +/** Route a frontend message into the desktop log (stdout + file). No-op on web. */ +export function logEvent(level: "info" | "warn" | "error" | "debug", message: string): void { + if (!isDesktop()) return; + void invoke("log_event", { level, message }).catch(() => {}); } // Desktop (Linux AppImage) self-integration: add/remove an applications-menu entry. diff --git a/frontend/src/main.ts b/frontend/src/main.ts index ce512d1..28f3479 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -2,6 +2,7 @@ import { createApp } from "vue"; import { createPinia } from "pinia"; import App from "./App.vue"; import router from "./router"; +import { isDesktop, logEvent } from "./desktop/bridge"; import "./style.css"; const app = createApp(App); @@ -9,6 +10,10 @@ app.use(createPinia()); // before router: the nav guard reads the session store app.use(router); app.mount("#app"); +// Boot line (desktop only): which data source, and the WebKit user-agent — the +// renderer identity that matters most when a build behaves differently per machine. +logEvent("info", `boot: source=${isDesktop() ? "local (offline core)" : "rest"} ua=${navigator.userAgent}`); + // Register the service worker so the app is installable (PWA). It's a // progressive enhancement — installability only, not offline-first (see // public/sw.js) — so failures are non-fatal and intentionally ignored. diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 6cb8f1a..aae7126 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,6 +1,11 @@ import { createRouter, createWebHistory } from "vue-router"; import { useSessionStore } from "../stores/session"; import { useConfigStore } from "../stores/config"; +import { logEvent } from "../desktop/bridge"; + +// One-time boot diagnostic: the first navigation is where config + session resolve, +// so it's the moment that tells us whether the app got past its startup gate. +let bootLogged = false; const router = createRouter({ history: createWebHistory(), @@ -56,6 +61,13 @@ router.beforeEach(async (to) => { if (!session.loaded) { await session.fetchMe(); } + if (!bootLogged) { + bootLogged = true; + logEvent( + "info", + `first route: config(site=${config.siteName}) session(user=${session.user?.email ?? "none"}) -> ${String(to.name ?? to.path)}`, + ); + } if (to.meta.requiresAuth && !session.user) { return { name: "login", query: to.fullPath !== "/" ? { redirect: to.fullPath } : undefined }; }