feat: add user-facing debug logs

This commit is contained in:
t8y2
2026-06-08 12:16:49 +08:00
parent c38994d02b
commit b777cddde5
16 changed files with 506 additions and 13 deletions
@@ -53,6 +53,7 @@ import ThemeCustomizerDialog from "./ThemeCustomizerDialog.vue";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { useTheme } from "@/composables/useTheme";
import { copyToClipboard } from "@/lib/clipboard";
import { clearDebugLogs as clearStoredDebugLogs, downloadDebugLogs, getDebugLogBundleText } from "@/lib/debugLog";
import {
aiListModels,
aiTestConnection,
@@ -115,6 +116,9 @@ const editConfirmDangerousSqlExecution = ref(settingsStore.editorSettings.confir
const editAppLayout = ref(settingsStore.editorSettings.appLayout);
const editShowTrayIcon = ref(settingsStore.desktopSettings.show_tray_icon);
const editIconTheme = ref<DesktopIconTheme>(settingsStore.desktopSettings.icon_theme);
const editDebugLoggingEnabled = ref(settingsStore.desktopSettings.debug_logging_enabled);
const debugLogCopied = ref(false);
const debugLogDownloaded = ref(false);
const editShowColumnCommentsInHeader = ref(settingsStore.editorSettings.showColumnCommentsInHeader);
const editShowColumnTypesInHeader = ref(settingsStore.editorSettings.showColumnTypesInHeader);
const editCompactColumnHeaderActions = ref(settingsStore.editorSettings.compactColumnHeaderActions);
@@ -273,6 +277,7 @@ watch(
editAppLayout.value = settingsStore.editorSettings.appLayout;
editShowTrayIcon.value = settingsStore.desktopSettings.show_tray_icon;
editIconTheme.value = settingsStore.desktopSettings.icon_theme;
editDebugLoggingEnabled.value = settingsStore.desktopSettings.debug_logging_enabled;
editShowColumnCommentsInHeader.value = settingsStore.editorSettings.showColumnCommentsInHeader;
editShowColumnTypesInHeader.value = settingsStore.editorSettings.showColumnTypesInHeader;
editCompactColumnHeaderActions.value = settingsStore.editorSettings.compactColumnHeaderActions;
@@ -321,6 +326,7 @@ function hasChanges(): boolean {
editAppLayout.value !== settingsStore.editorSettings.appLayout ||
editShowTrayIcon.value !== settingsStore.desktopSettings.show_tray_icon ||
editIconTheme.value !== settingsStore.desktopSettings.icon_theme ||
editDebugLoggingEnabled.value !== settingsStore.desktopSettings.debug_logging_enabled ||
editShowColumnCommentsInHeader.value !== settingsStore.editorSettings.showColumnCommentsInHeader ||
editShowColumnTypesInHeader.value !== settingsStore.editorSettings.showColumnTypesInHeader ||
editCompactColumnHeaderActions.value !== settingsStore.editorSettings.compactColumnHeaderActions ||
@@ -376,6 +382,7 @@ async function persistSettings() {
await settingsStore.updateDesktopSettings({
show_tray_icon: editShowTrayIcon.value,
icon_theme: editIconTheme.value,
debug_logging_enabled: editDebugLoggingEnabled.value,
});
if (sidebarObjectDisplayChanged) {
await connectionStore.refreshAllTree();
@@ -404,6 +411,7 @@ function resetDefaults() {
editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout;
editShowTrayIcon.value = DEFAULT_DESKTOP_SETTINGS.show_tray_icon;
editIconTheme.value = DEFAULT_DESKTOP_SETTINGS.icon_theme;
editDebugLoggingEnabled.value = DEFAULT_DESKTOP_SETTINGS.debug_logging_enabled;
editShowColumnCommentsInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader;
editShowColumnTypesInHeader.value = DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader;
editCompactColumnHeaderActions.value = DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions;
@@ -629,6 +637,29 @@ function openExternalUrl(url: string) {
}
}
async function copyDebugLogs() {
await copyToClipboard(await getDebugLogBundleText());
debugLogCopied.value = true;
window.setTimeout(() => {
debugLogCopied.value = false;
}, 1500);
}
function clearDebugLogs() {
clearStoredDebugLogs();
debugLogCopied.value = false;
debugLogDownloaded.value = false;
}
async function exportDebugLogs() {
const saved = await downloadDebugLogs();
if (!saved) return;
debugLogDownloaded.value = true;
window.setTimeout(() => {
debugLogDownloaded.value = false;
}, 1500);
}
// ---------- MCP Server ----------
const mcpStatus = ref<McpServerStatus | null>(null);
const mcpStatusLoading = ref(false);
@@ -871,6 +902,7 @@ watch(
await settingsStore.initDesktopSettings();
editShowTrayIcon.value = settingsStore.desktopSettings.show_tray_icon;
editIconTheme.value = settingsStore.desktopSettings.icon_theme;
editDebugLoggingEnabled.value = settingsStore.desktopSettings.debug_logging_enabled;
webdavPassword.value = "";
await refreshWebDavPasswordStatus();
syncAiEditState();
@@ -1559,6 +1591,29 @@ watch(
<Switch id="update-notifications-enabled" v-model="editUpdateNotificationsEnabled" />
</div>
<div v-if="!isWeb" class="flex flex-col gap-3 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center justify-between gap-4">
<div class="space-y-1">
<Label for="debug-logging-enabled">{{ t("settings.debugLoggingEnabled") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.debugLoggingEnabledDescription") }}
</p>
</div>
<Switch id="debug-logging-enabled" v-model="editDebugLoggingEnabled" />
</div>
<div class="flex justify-end gap-2">
<Button type="button" variant="outline" size="sm" @click="clearDebugLogs">
{{ t("settings.debugLogsClear") }}
</Button>
<Button type="button" variant="outline" size="sm" @click="copyDebugLogs">
{{ debugLogCopied ? t("settings.debugLogsCopied") : t("settings.debugLogsCopy") }}
</Button>
<Button type="button" variant="outline" size="sm" @click="exportDebugLogs">
{{ debugLogDownloaded ? t("settings.debugLogsDownloaded") : t("settings.debugLogsDownload") }}
</Button>
</div>
</div>
<Separator />
<div class="space-y-3">
+8
View File
@@ -1673,6 +1673,14 @@ export default {
updateNotificationsEnabled: "Enable update reminders",
updateNotificationsEnabledDescription:
"When disabled, DBX will not automatically check app or driver updates or show update badges. Manual checks are still available.",
debugLoggingEnabled: "Enable debug logs",
debugLoggingEnabledDescription:
"Record more detailed local user-side diagnostic logs while troubleshooting reports. Disabled by default.",
debugLogsCopy: "Copy logs",
debugLogsCopied: "Copied",
debugLogsDownload: "Download logs",
debugLogsDownloaded: "Downloaded",
debugLogsClear: "Clear logs",
dataGridDisplay: "Data grid display",
showColumnCommentsInHeader: "Show column comments under names",
showColumnCommentsInHeaderDescription: "Display table column comments directly below grid column names.",
+8
View File
@@ -1526,6 +1526,14 @@ export default {
updateNotificationsEnabled: "Activar recordatorios de actualización",
updateNotificationsEnabledDescription:
"Al desactivarlo, DBX no comprobará automáticamente actualizaciones de la app ni de los controladores, ni mostrará indicadores. Las comprobaciones manuales siguen disponibles.",
debugLoggingEnabled: "Activar logs de depuración",
debugLoggingEnabledDescription:
"Registra logs de diagnóstico locales del lado del usuario al investigar reportes. Desactivado por defecto.",
debugLogsCopy: "Copiar logs",
debugLogsCopied: "Copiado",
debugLogsDownload: "Descargar logs",
debugLogsDownloaded: "Descargado",
debugLogsClear: "Borrar logs",
dataGridDisplay: "Visualización de la tabla",
showColumnCommentsInHeader: "Mostrar comentarios bajo los nombres",
showColumnCommentsInHeaderDescription:
+8
View File
@@ -1669,6 +1669,14 @@ export default {
updateNotificationsEnabled: "Abilita promemoria aggiornamenti",
updateNotificationsEnabledDescription:
"Se disattivato, DBX non controllerà automaticamente gli aggiornamenti dell'app o dei driver e non mostrerà badge. I controlli manuali restano disponibili.",
debugLoggingEnabled: "Abilita log di debug",
debugLoggingEnabledDescription:
"Registra log diagnostici locali lato utente durante l'analisi delle segnalazioni. Disattivato per impostazione predefinita.",
debugLogsCopy: "Copia log",
debugLogsCopied: "Copiati",
debugLogsDownload: "Scarica log",
debugLogsDownloaded: "Scaricati",
debugLogsClear: "Cancella log",
dataGridDisplay: "Visualizzazione griglia dati",
showColumnCommentsInHeader: "Mostra i commenti delle colonne sotto i nomi",
showColumnCommentsInHeaderDescription:
+8
View File
@@ -1659,6 +1659,14 @@ export default {
updateNotificationsEnabled: "Ativar lembretes de atualização",
updateNotificationsEnabledDescription:
"Quando desativado, o DBX não verificará automaticamente atualizações do app ou dos drivers nem mostrará indicadores. Verificações manuais continuam disponíveis.",
debugLoggingEnabled: "Ativar logs de depuração",
debugLoggingEnabledDescription:
"Registra logs locais de diagnóstico do lado do usuário ao investigar relatos. Desativado por padrão.",
debugLogsCopy: "Copiar logs",
debugLogsCopied: "Copiado",
debugLogsDownload: "Baixar logs",
debugLogsDownloaded: "Baixado",
debugLogsClear: "Limpar logs",
dataGridDisplay: "Exibição da grade de dados",
showColumnCommentsInHeader: "Mostrar comentários de coluna sob os nomes",
showColumnCommentsInHeaderDescription:
+7
View File
@@ -1642,6 +1642,13 @@ export default {
updateNotificationsEnabled: "启用更新提醒",
updateNotificationsEnabledDescription:
"关闭后,DBX 不会自动检查应用和驱动更新,也不会显示更新红点;仍可手动检查更新。",
debugLoggingEnabled: "启用调试日志",
debugLoggingEnabledDescription: "开启后在本机记录更详细的用户侧诊断日志,反馈问题时可临时开启;默认关闭。",
debugLogsCopy: "复制日志",
debugLogsCopied: "已复制",
debugLogsDownload: "下载日志",
debugLogsDownloaded: "已下载",
debugLogsClear: "清空日志",
dataGridDisplay: "数据表格显示",
showColumnCommentsInHeader: "在字段名下方显示注释",
showColumnCommentsInHeaderDescription: "把表字段注释直接显示在结果表头字段名下方。",
+7
View File
@@ -1583,6 +1583,13 @@ export default {
updateNotificationsEnabled: "啟用更新提醒",
updateNotificationsEnabledDescription:
"關閉後,DBX 不會自動檢查應用程式和驅動程式更新,也不會顯示更新紅點;仍可手動檢查更新。",
debugLoggingEnabled: "啟用偵錯日誌",
debugLoggingEnabledDescription: "開啟後在本機記錄更詳細的使用者側診斷日誌,回報問題時可暫時開啟;預設關閉。",
debugLogsCopy: "複製日誌",
debugLogsCopied: "已複製",
debugLogsDownload: "下載日誌",
debugLogsDownloaded: "已下載",
debugLogsClear: "清空日誌",
dataGridDisplay: "資料表格顯示",
showColumnCommentsInHeader: "在欄位名稱下方顯示註解",
showColumnCommentsInHeaderDescription: "直接在資料表格欄位名稱下方顯示資料表欄位註解。",
+19 -1
View File
@@ -1,5 +1,6 @@
import { isTauriRuntime } from "./tauriRuntime";
import type * as TauriModule from "./tauri";
import { appendDebugLog } from "./debugLog";
// ---------------------------------------------------------------------------
// Lazy backend resolution (avoids top-level await)
@@ -21,8 +22,25 @@ async function getBackend(): Promise<Backend> {
function forward<K extends keyof Backend>(name: K): Backend[K] {
return (async (...args: unknown[]) => {
const startedAt = performance.now();
const operation = String(name);
appendDebugLog("debug", "[DBX][api:start]", operation);
const b = await getBackend();
return (b[name] as (...a: unknown[]) => unknown)(...args);
try {
const result = await (b[name] as (...a: unknown[]) => unknown)(...args);
appendDebugLog("debug", "[DBX][api:success]", {
operation,
elapsedMs: Math.round(performance.now() - startedAt),
});
return result;
} catch (error) {
appendDebugLog("error", "[DBX][api:error]", {
operation,
elapsedMs: Math.round(performance.now() - startedAt),
error,
});
throw error;
}
}) as unknown as Backend[K];
}
+296
View File
@@ -0,0 +1,296 @@
const DEBUG_LOG_ENABLED_KEY = "dbx-debug-logging-enabled";
const DEBUG_LOG_ENTRIES_KEY = "dbx-debug-log-entries";
const MAX_DEBUG_LOG_ENTRIES = 1500;
const MAX_TEXT_LENGTH = 4000;
const MAX_LABEL_LENGTH = 120;
type DebugLogLevel = "debug" | "info" | "log" | "warn" | "error";
interface DebugLogEntry {
timestamp: string;
level: DebugLogLevel;
message: string;
}
let installed = false;
let originalConsole: Partial<Record<DebugLogLevel, (...args: unknown[]) => void>> = {};
function safeLocalStorageGet(key: string): string | null {
try {
return localStorage.getItem(key);
} catch {
return null;
}
}
function safeLocalStorageSet(key: string, value: string) {
try {
localStorage.setItem(key, value);
} catch {
// Ignore quota or unavailable storage errors. The app should keep running.
}
}
function safeLocalStorageRemove(key: string) {
try {
localStorage.removeItem(key);
} catch {}
}
export function isDebugLoggingEnabled(): boolean {
return safeLocalStorageGet(DEBUG_LOG_ENABLED_KEY) === "1";
}
function readEntries(): DebugLogEntry[] {
const raw = safeLocalStorageGet(DEBUG_LOG_ENTRIES_KEY);
if (!raw) return [];
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed.slice(-MAX_DEBUG_LOG_ENTRIES) : [];
} catch {
return [];
}
}
function redactSensitiveText(value: string): string {
return value
.replace(
/("(?:password|passphrase|apiKey|api_key|token|secret|connectionString|connection_string)"\s*:\s*")([^"]*)(")/gi,
"$1[redacted]$3",
)
.replace(/\b(password|passphrase|apiKey|api_key|token|secret)=([^&\s]+)/gi, "$1=[redacted]");
}
function formatValue(value: unknown, seen = new WeakSet<object>()): string {
if (value instanceof Error) {
return redactSensitiveText([value.name, value.message, value.stack].filter(Boolean).join(": "));
}
if (typeof value === "string") return redactSensitiveText(value);
if (typeof value !== "object" || value === null) return String(value);
if (seen.has(value)) return "[Circular]";
seen.add(value);
try {
return redactSensitiveText(
JSON.stringify(value, (_key, item) => {
if (typeof item === "function") return `[Function ${item.name || "anonymous"}]`;
return item;
}),
);
} catch {
return redactSensitiveText(String(value));
}
}
function formatArgs(args: unknown[]): string {
const message = args.map((arg) => formatValue(arg)).join(" ");
return message.length > MAX_TEXT_LENGTH ? `${message.slice(0, MAX_TEXT_LENGTH)}...` : message;
}
export function appendDebugLog(level: DebugLogLevel, ...args: unknown[]) {
if (!isDebugLoggingEnabled()) return;
const entries = readEntries();
entries.push({
timestamp: new Date().toISOString(),
level,
message: formatArgs(args),
});
safeLocalStorageSet(DEBUG_LOG_ENTRIES_KEY, JSON.stringify(entries.slice(-MAX_DEBUG_LOG_ENTRIES)));
}
export function setDebugLoggingEnabled(enabled: boolean) {
safeLocalStorageSet(DEBUG_LOG_ENABLED_KEY, enabled ? "1" : "0");
if (enabled) {
appendDebugLog("info", "[DBX][debug-log] enabled", {
url: location.href,
viewport: `${window.innerWidth}x${window.innerHeight}`,
devicePixelRatio: window.devicePixelRatio,
language: navigator.language,
online: navigator.onLine,
});
}
}
export function clearDebugLogs() {
safeLocalStorageRemove(DEBUG_LOG_ENTRIES_KEY);
}
export function getDebugLogText(): string {
const entries = readEntries();
const header = [
`DBX debug log`,
`Exported: ${new Date().toISOString()}`,
`User agent: ${navigator.userAgent}`,
`Platform: ${navigator.platform}`,
`Timezone: ${Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown"}`,
"",
];
const body = entries.map((entry) => `[${entry.timestamp}] [${entry.level.toUpperCase()}] ${entry.message}`);
return [...header, ...body].join("\n");
}
export async function downloadDebugLogs() {
const text = await getDebugLogBundleText();
const filename = `dbx-debug-log-${new Date().toISOString().replace(/[:.]/g, "-")}.txt`;
if (typeof window !== "undefined" && isTauriRuntimeLike()) {
const [{ save }, { writeTextFile }] = await Promise.all([
import("@tauri-apps/plugin-dialog"),
import("@tauri-apps/plugin-fs"),
]);
const path = await save({
defaultPath: filename,
filters: [{ name: "Text", extensions: ["txt", "log"] }],
});
if (!path) return false;
await writeTextFile(path, text);
return true;
}
const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
return true;
}
export async function getDebugLogBundleText(): Promise<string> {
const sections = [getDebugLogText()];
const nativeLogs = await loadNativeDebugLogs().catch((error) => `Failed to load native logs: ${formatValue(error)}`);
if (nativeLogs) {
sections.push(["", "===== Native / Tauri logs =====", nativeLogs].join("\n"));
}
return sections.join("\n");
}
async function loadNativeDebugLogs(): Promise<string> {
if (!isTauriRuntimeLike()) return "";
const { invoke } = await import("@tauri-apps/api/core");
return invoke<string>("load_native_debug_logs");
}
function isTauriRuntimeLike(): boolean {
return "__TAURI_INTERNALS__" in globalThis || "__TAURI__" in globalThis;
}
export function installDebugLogCapture() {
if (installed) return;
installed = true;
originalConsole = {
debug: console.debug.bind(console),
info: console.info.bind(console),
log: console.log.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
};
(Object.keys(originalConsole) as DebugLogLevel[]).forEach((level) => {
console[level] = (...args: unknown[]) => {
appendDebugLog(level, ...args);
originalConsole[level]?.(...args);
};
});
window.addEventListener("error", (event) => {
const target = event.target;
if (target instanceof HTMLElement) {
appendDebugLog("error", "[window:resource-error]", describeElement(target));
return;
}
appendDebugLog("error", "[window:error]", event.error ?? event.message);
});
window.addEventListener("unhandledrejection", (event) => {
appendDebugLog("error", "[window:unhandledrejection]", event.reason);
});
window.addEventListener("online", () => appendDebugLog("info", "[window:online]"));
window.addEventListener("offline", () => appendDebugLog("warn", "[window:offline]"));
window.addEventListener("hashchange", () => appendDebugLog("info", "[window:hashchange]", location.hash));
document.addEventListener("visibilitychange", () => {
appendDebugLog("info", "[document:visibilitychange]", document.visibilityState);
});
document.addEventListener(
"click",
(event) => {
const target = event.target instanceof HTMLElement ? interactiveElement(event.target) : null;
if (!target) return;
appendDebugLog("debug", "[ui:click]", describeElement(target));
},
true,
);
document.addEventListener(
"change",
(event) => {
const target = event.target instanceof HTMLElement ? event.target : null;
if (!target || !isFormControl(target)) return;
appendDebugLog("debug", "[ui:change]", describeElement(target));
},
true,
);
installPerformanceCapture();
}
function installPerformanceCapture() {
if (typeof PerformanceObserver === "undefined") return;
try {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration >= 250) {
appendDebugLog("warn", "[performance:long-task]", {
name: entry.name,
durationMs: Math.round(entry.duration),
startTimeMs: Math.round(entry.startTime),
});
}
}
});
observer.observe({ entryTypes: ["longtask"] });
} catch {
// Long task entries are not available in every WebView.
}
}
function interactiveElement(target: HTMLElement): HTMLElement | null {
return target.closest(
[
"button",
"a[href]",
"input",
"select",
"textarea",
"[role='button']",
"[role='menuitem']",
"[role='tab']",
"[data-debug-id]",
].join(","),
);
}
function isFormControl(target: HTMLElement): boolean {
return (
target instanceof HTMLInputElement ||
target instanceof HTMLSelectElement ||
target instanceof HTMLTextAreaElement ||
target.getAttribute("role") === "switch"
);
}
function describeElement(element: HTMLElement): Record<string, string> {
const label =
element.getAttribute("data-debug-id") ||
element.getAttribute("aria-label") ||
element.getAttribute("title") ||
element.textContent ||
"";
const attrs: Record<string, string> = {
tag: element.tagName.toLowerCase(),
};
if (element.id) attrs.id = element.id;
const role = element.getAttribute("role");
if (role) attrs.role = role;
const type = element.getAttribute("type");
if (type) attrs.type = type;
const safeLabel = redactSensitiveText(label.trim().replace(/\s+/g, " "));
if (safeLabel) attrs.label = safeLabel.slice(0, MAX_LABEL_LENGTH);
const testId = element.getAttribute("data-testid");
if (testId) attrs.testId = testId;
return attrs;
}
+1 -1
View File
@@ -874,7 +874,7 @@ export async function loadAiConfig(): Promise<AiConfig | null> {
}
export async function loadDesktopSettings(): Promise<DesktopSettings> {
return { show_tray_icon: true, icon_theme: "default" };
return { show_tray_icon: true, icon_theme: "default", debug_logging_enabled: false };
}
export async function saveDesktopSettings(_settings: DesktopSettings): Promise<void> {
+1
View File
@@ -145,6 +145,7 @@ export interface DriverRuntimeSummary {
export interface DesktopSettings {
show_tray_icon: boolean;
icon_theme: "default" | "black";
debug_logging_enabled: boolean;
}
export interface WebDavConfig {
+2
View File
@@ -3,6 +3,7 @@ import { createPinia } from "pinia";
import VueVirtualScroller from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import "./styles/globals.css";
import { installDebugLogCapture } from "@/lib/debugLog";
function startupErrorMessage(error: unknown): string {
if (error instanceof Error) {
@@ -89,5 +90,6 @@ async function bootstrap() {
console.log("[STARTUP] vue mounted");
}
installDebugLogCapture();
installStartupErrorHandlers();
void bootstrap().catch(renderStartupError);
+7
View File
@@ -13,6 +13,7 @@ import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebarTableNameDispl
import type { SidebarActivation } from "@/lib/treeNodeClick";
import type { SqlSnippet } from "@/types/database";
import { DEFAULT_SQL_SNIPPETS } from "@/lib/sqlCompletion";
import { setDebugLoggingEnabled } from "@/lib/debugLog";
export type AiProvider =
| "claude"
@@ -39,6 +40,7 @@ export interface AiConfig {
export interface DesktopSettings {
show_tray_icon: boolean;
icon_theme: DesktopIconTheme;
debug_logging_enabled: boolean;
}
export type DesktopIconTheme = "default" | "black";
@@ -46,6 +48,7 @@ export type DesktopIconTheme = "default" | "black";
export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
show_tray_icon: true,
icon_theme: "default",
debug_logging_enabled: false,
};
function normalizeDesktopSettings(settings: Partial<DesktopSettings> | null | undefined): DesktopSettings {
@@ -53,6 +56,7 @@ function normalizeDesktopSettings(settings: Partial<DesktopSettings> | null | un
return {
show_tray_icon: settings?.show_tray_icon ?? DEFAULT_DESKTOP_SETTINGS.show_tray_icon,
icon_theme: iconTheme,
debug_logging_enabled: settings?.debug_logging_enabled ?? DEFAULT_DESKTOP_SETTINGS.debug_logging_enabled,
};
}
@@ -567,6 +571,7 @@ export const useSettingsStore = defineStore("settings", () => {
async function initDesktopSettings() {
if (isDesktopSettingsLoaded.value) return;
desktopSettings.value = normalizeDesktopSettings(await api.loadDesktopSettings().catch(() => null));
setDebugLoggingEnabled(desktopSettings.value.debug_logging_enabled);
isDesktopSettingsLoaded.value = true;
}
@@ -577,10 +582,12 @@ export const useSettingsStore = defineStore("settings", () => {
...partial,
};
desktopSettings.value = normalizeDesktopSettings(next);
setDebugLoggingEnabled(desktopSettings.value.debug_logging_enabled);
try {
await api.saveDesktopSettings(desktopSettings.value);
} catch (error) {
desktopSettings.value = previous;
setDebugLoggingEnabled(previous.debug_logging_enabled);
throw error;
}
}
+28 -5
View File
@@ -32,11 +32,13 @@ pub struct TabRuntimeCacheEntry {
pub struct DesktopSettings {
pub show_tray_icon: bool,
pub icon_theme: DesktopIconTheme,
#[serde(default)]
pub debug_logging_enabled: bool,
}
impl Default for DesktopSettings {
fn default() -> Self {
Self { show_tray_icon: true, icon_theme: DesktopIconTheme::Default }
Self { show_tray_icon: true, icon_theme: DesktopIconTheme::Default, debug_logging_enabled: false }
}
}
@@ -430,6 +432,10 @@ impl Storage {
"icon_theme".to_string(),
serde_json::to_value(desktop_settings.icon_theme).map_err(|e| e.to_string())?,
);
settings.insert(
"debug_logging_enabled".to_string(),
serde_json::Value::Bool(desktop_settings.debug_logging_enabled),
);
self.save_app_settings_json(&settings).await
}
@@ -442,6 +448,10 @@ impl Storage {
.or_else(|| settings.get("run_in_background").and_then(|value| value.as_bool()))
.unwrap_or_else(|| DesktopSettings::default().show_tray_icon),
icon_theme: DesktopIconTheme::from_settings_value(settings.get("icon_theme")),
debug_logging_enabled: settings
.get("debug_logging_enabled")
.and_then(|value| value.as_bool())
.unwrap_or_else(|| DesktopSettings::default().debug_logging_enabled),
})
}
@@ -1334,14 +1344,18 @@ mod tests {
storage.save_password_hash("hash-1").await.unwrap();
storage
.save_desktop_settings(&DesktopSettings { show_tray_icon: false, icon_theme: DesktopIconTheme::Black })
.save_desktop_settings(&DesktopSettings {
show_tray_icon: false,
icon_theme: DesktopIconTheme::Black,
debug_logging_enabled: true,
})
.await
.unwrap();
assert_eq!(storage.load_password_hash().await.unwrap(), Some("hash-1".to_string()));
assert_eq!(
storage.load_desktop_settings().await.unwrap(),
DesktopSettings { show_tray_icon: false, icon_theme: DesktopIconTheme::Black }
DesktopSettings { show_tray_icon: false, icon_theme: DesktopIconTheme::Black, debug_logging_enabled: true }
);
}
@@ -1365,6 +1379,7 @@ mod tests {
assert_eq!(settings.get("run_in_background"), None);
assert_eq!(settings.get("show_tray_icon").and_then(|value| value.as_bool()), Some(true));
assert_eq!(settings.get("icon_theme").and_then(|value| value.as_str()), Some("black"));
assert_eq!(settings.get("debug_logging_enabled").and_then(|value| value.as_bool()), Some(false));
}
#[tokio::test]
@@ -1373,7 +1388,11 @@ mod tests {
let storage = Storage::open(&path).await.unwrap();
storage
.save_desktop_settings(&DesktopSettings { show_tray_icon: false, icon_theme: DesktopIconTheme::Black })
.save_desktop_settings(&DesktopSettings {
show_tray_icon: false,
icon_theme: DesktopIconTheme::Black,
..DesktopSettings::default()
})
.await
.unwrap();
storage.save_password_hash("hash-2").await.unwrap();
@@ -1381,7 +1400,11 @@ mod tests {
assert_eq!(storage.load_password_hash().await.unwrap(), Some("hash-2".to_string()));
assert_eq!(
storage.load_desktop_settings().await.unwrap(),
DesktopSettings { show_tray_icon: false, icon_theme: DesktopIconTheme::Black }
DesktopSettings {
show_tray_icon: false,
icon_theme: DesktopIconTheme::Black,
..DesktopSettings::default()
}
);
}
+43 -2
View File
@@ -1,10 +1,10 @@
use std::sync::Arc;
use dbx_core::storage::DesktopSettings;
use tauri::{AppHandle, State};
use tauri::{AppHandle, Manager, State};
use super::connection::AppState;
use crate::apply_desktop_settings;
use crate::{apply_debug_log_level, apply_desktop_settings};
#[tauri::command]
pub async fn load_desktop_settings(state: State<'_, Arc<AppState>>) -> Result<DesktopSettings, String> {
@@ -18,6 +18,7 @@ pub async fn save_desktop_settings(
settings: DesktopSettings,
) -> Result<(), String> {
state.storage.save_desktop_settings(&settings).await?;
apply_debug_log_level(settings.debug_logging_enabled);
if let Err(err) = apply_desktop_settings(&app, &settings) {
eprintln!("Failed to apply desktop settings: {err}");
}
@@ -33,3 +34,43 @@ pub async fn load_pinned_tree_node_ids(state: State<'_, Arc<AppState>>) -> Resul
pub async fn save_pinned_tree_node_ids(state: State<'_, Arc<AppState>>, ids: Vec<String>) -> Result<(), String> {
state.storage.save_pinned_tree_node_ids(&ids).await
}
#[tauri::command]
pub async fn load_native_debug_logs(app: AppHandle) -> Result<String, String> {
const MAX_FILES: usize = 6;
const MAX_FILE_BYTES: u64 = 512 * 1024;
let log_dir = app.path().app_log_dir().map_err(|e| e.to_string())?;
if !log_dir.exists() {
return Ok(format!("Native log dir does not exist yet: {}", log_dir.display()));
}
let mut files = std::fs::read_dir(&log_dir)
.map_err(|e| e.to_string())?
.filter_map(|entry| entry.ok())
.filter_map(|entry| {
let path = entry.path();
let metadata = entry.metadata().ok()?;
if !metadata.is_file() {
return None;
}
let modified = metadata.modified().ok()?;
Some((path, modified, metadata.len()))
})
.collect::<Vec<_>>();
files.sort_by_key(|(_, modified, _)| *modified);
files.reverse();
let mut output = String::new();
output.push_str(&format!("Native log dir: {}\n", log_dir.display()));
for (path, _, len) in files.into_iter().take(MAX_FILES) {
let name = path.file_name().and_then(|name| name.to_str()).unwrap_or("unknown");
output.push_str(&format!("\n===== {name} =====\n"));
let bytes = std::fs::read(&path).map_err(|e| e.to_string())?;
let start = if len > MAX_FILE_BYTES { bytes.len().saturating_sub(MAX_FILE_BYTES as usize) } else { 0 };
if start > 0 {
output.push_str("[truncated to last 512 KiB]\n");
}
output.push_str(&String::from_utf8_lossy(&bytes[start..]));
output.push('\n');
}
Ok(output)
}
+8 -4
View File
@@ -23,6 +23,10 @@ const DESKTOP_TRAY_ID: &str = "main-tray";
const MACOS_TRAY_ICON: tauri::image::Image<'_> = tauri::include_image!("icons/tray-macos-template.png");
const BLACK_APP_ICON: tauri::image::Image<'_> = tauri::include_image!("icons/icon-black.png");
pub(crate) fn apply_debug_log_level(debug_logging_enabled: bool) {
log::set_max_level(if debug_logging_enabled { log::LevelFilter::Debug } else { log::LevelFilter::Off });
}
fn should_hide_window_on_close(target_os: &str) -> bool {
matches!(target_os, "macos" | "windows")
}
@@ -153,6 +157,7 @@ fn apply_desktop_tray_icon_theme(app: &tauri::AppHandle, icon_theme: DesktopIcon
}
pub(crate) fn apply_desktop_settings(app: &tauri::AppHandle, desktop_settings: &DesktopSettings) -> tauri::Result<()> {
apply_debug_log_level(desktop_settings.debug_logging_enabled);
apply_desktop_icon_theme(app, desktop_settings.icon_theme)?;
if matches!(std::env::consts::OS, "macos" | "windows") {
if let Some(tray) = app.tray_by_id(DESKTOP_TRAY_ID) {
@@ -266,10 +271,6 @@ pub fn run() {
let setup_start = Instant::now();
eprintln!("[STARTUP] plugins registered in {:?}", startup_begin.elapsed());
if cfg!(debug_assertions) {
app.handle().plugin(tauri_plugin_log::Builder::default().level(log::LevelFilter::Info).build())?;
}
let default_data_dir =
app.path().app_data_dir().map_err(|e| e.to_string()).expect("Failed to resolve app data dir");
let data_dir = data_dir::resolve_data_dir(default_data_dir);
@@ -286,6 +287,8 @@ pub fn run() {
s
});
let desktop_settings = tauri::async_runtime::block_on(storage.load_desktop_settings()).unwrap_or_default();
app.handle().plugin(tauri_plugin_log::Builder::default().level(log::LevelFilter::Debug).build())?;
apply_debug_log_level(desktop_settings.debug_logging_enabled);
eprintln!("[STARTUP] storage ready in {:?}", t.elapsed());
let state = if data_dir::uses_custom_data_dir() {
@@ -355,6 +358,7 @@ pub fn run() {
commands::app_settings::save_desktop_settings,
commands::app_settings::load_pinned_tree_node_ids,
commands::app_settings::save_pinned_tree_node_ids,
commands::app_settings::load_native_debug_logs,
commands::cloud_sync::webdav_sync_test,
commands::cloud_sync::webdav_password_status,
commands::cloud_sync::save_webdav_saved_password,