desktop: robust startup + operation logging (portability troubleshooting)
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 9s
CI & Build / Python tests (push) Successful in 13s
CI & Build / Build & push image (push) Successful in 32s

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
2026-07-25 10:18:06 -04:00
co-authored by Claude Opus 4.8
parent 3958db0a8b
commit f325402902
6 changed files with 121 additions and 2 deletions
+12 -1
View File
@@ -27,7 +27,18 @@ export function isDesktop(): boolean {
export function invoke<T>(cmd: string, args?: Record<string, unknown>): Promise<T> {
const tauri = window.__TAURI__;
if (!tauri) return Promise.reject(new Error("Not running in the desktop app."));
return tauri.core.invoke<T>(cmd, args);
return tauri.core.invoke<T>(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.
+5
View File
@@ -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.
+12
View File
@@ -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 };
}