From 4d1eaa70e2b82d6ccf61920b1139f5e0431fb538 Mon Sep 17 00:00:00 2001 From: Aditya Date: Sat, 27 Jun 2026 10:55:27 +0530 Subject: [PATCH 1/3] fix: address audit findings (ESC logic, IPC types, update restart, mutex panics, pause button, ESLint) --- package.json | 4 ++-- src-tauri/src/commands/shortcuts.rs | 4 ++-- src-tauri/src/commands/system.rs | 13 +++++++++---- src-tauri/src/macos.rs | 5 +++++ src/App.tsx | 11 +++++++++++ src/GraphView.tsx | 19 ++++++++++++++++--- src/api.ts | 13 ++++++++++--- src/components/TimersPage.tsx | 13 ++----------- src/hooks/useGlobalHotkey.ts | 14 ++++++++++---- src/types.d.ts | 9 +++++---- 10 files changed, 72 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index cf404ca..42f91e3 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,8 @@ "@types/d3-force": "^3.0.10", "d3-force": "^3.0.0", "expr-eval": "^2.0.2", + "react": "^19.2.6", + "react-dom": "^19.2.6", "react-force-graph-3d": "^1.29.1", "three": "^0.184.0" }, @@ -73,8 +75,6 @@ "jsdom": "^29.1.1", "lint-staged": "^17.0.7", "prettier": "^3.8.3", - "react": "^19.2.6", - "react-dom": "^19.2.6", "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", "vite": "^8.0.12", diff --git a/src-tauri/src/commands/shortcuts.rs b/src-tauri/src/commands/shortcuts.rs index 7887370..1e815b4 100644 --- a/src-tauri/src/commands/shortcuts.rs +++ b/src-tauri/src/commands/shortcuts.rs @@ -59,7 +59,7 @@ pub fn update_global_shortcut( // Update state let state = app.state::(); - let mut map = state.shortcuts.lock().unwrap(); + let mut map = state.shortcuts.lock().map_err(|e| e.to_string())?; map.insert(action, new_shortcut); Ok(()) @@ -75,7 +75,7 @@ pub fn pause_shortcuts(app: AppHandle) -> Result<(), String> { #[tauri::command] pub fn resume_shortcuts(app: AppHandle) -> Result<(), String> { let state = app.state::(); - let map = state.shortcuts.lock().unwrap(); + let map = state.shortcuts.lock().map_err(|e| e.to_string())?; for (action, shortcut_str) in map.iter() { if let Ok(shortcut) = shortcut_str.parse::() { diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs index 9c4cd50..dee602d 100644 --- a/src-tauri/src/commands/system.rs +++ b/src-tauri/src/commands/system.rs @@ -114,11 +114,16 @@ pub async fn check_for_updates(app: tauri::AppHandle) -> Result<(), String> { use tauri_plugin_updater::UpdaterExt; let updater = app.updater().map_err(|e| e.to_string())?; - // We handle the update automatically if one is available if let Some(update) = updater.check().await.map_err(|e| e.to_string())? { - // Here we could emit an event to the frontend or just download and install it - let _ = update.download_and_install(|_, _| {}, || {}).await; - app.restart(); + // Run the download + install + restart in the background so the command + // returns immediately. The "update-ready" event gives the frontend 3 seconds + // to show a toast before the process restarts. + tokio::spawn(async move { + let _ = update.download_and_install(|_, _| {}, || {}).await; + let _ = app.emit("update-ready", ()); + tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; + app.restart(); + }); } Ok(()) } diff --git a/src-tauri/src/macos.rs b/src-tauri/src/macos.rs index 44c5ee6..ffb56bd 100644 --- a/src-tauri/src/macos.rs +++ b/src-tauri/src/macos.rs @@ -65,6 +65,11 @@ pub fn setup_power_monitor(app_handle: AppHandle) { let delegate: id = msg_send![delegate_class, new]; + // SAFETY: We intentionally leak the AppHandle here. The NSNotificationCenter + // observer (delegate) is registered for the lifetime of the process and must + // always have a valid pointer to the handle. Freeing the box would invalidate + // the pointer stored in the Objective-C ivar, causing a use-after-free. + // This is a deliberate, bounded leak (one pointer per process lifetime). let app_box = Box::new(app_handle); let ptr = Box::into_raw(app_box) as *mut c_void; (*delegate).set_ivar("app_handle", ptr); diff --git a/src/App.tsx b/src/App.tsx index b00298f..37ab484 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -57,6 +57,17 @@ function App() { window.electronAPI.isHyprland().then((isHyp) => { useAppStore.getState().setIsHyprland(isHyp) }) + + // Show a toast before the app auto-restarts for an update + const disposeUpdateReady = window.electronAPI.onUpdateReady(() => { + useAppStore.getState().addToast({ + message: '✨ PaperCache updated — restarting in 3 seconds…', + type: 'info', + }) + }) + return () => { + disposeUpdateReady() + } }, []) // Auto-dismiss toasts after 5 seconds diff --git a/src/GraphView.tsx b/src/GraphView.tsx index de4b2e5..5dcf6cb 100644 --- a/src/GraphView.tsx +++ b/src/GraphView.tsx @@ -46,6 +46,17 @@ function buildFolderCentroids(folderNames: string[]): Map Record | null + cameraPosition: (pos: { x: number; y: number; z: number }) => void + zoomToFit: (duration: number, padding: number) => void + graphData: () => { nodes: GraphNode[]; links: GraphLink[] } | null + d3Force: (name: string, force?: unknown) => unknown + scene: () => THREE.Scene + nodeThreeObject: unknown +} + export default function GraphView({ notes, onClose, @@ -54,13 +65,13 @@ export default function GraphView({ bgColor, accentColor, }: GraphViewProps) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const fgRef = useRef(null) + const fgRef = useRef(null) const draggedNodesRef = useRef>(new Set()) useEffect(() => { let raf: number + // eslint-disable-next-line @typescript-eslint/no-explicit-any let ctrls: any = null const setup = () => { const fg = fgRef.current @@ -89,8 +100,10 @@ export default function GraphView({ }, []) useEffect(() => { + // Snapshot ref at effect-run time so the cleanup reads a stable value + // (avoids the react-hooks/exhaustive-deps stale-ref warning) + const fg = fgRef.current return () => { - const fg = fgRef.current if (!fg) return const data = fg.graphData() if (!data || !data.nodes) return diff --git a/src/api.ts b/src/api.ts index a50e7a0..3795ad8 100644 --- a/src/api.ts +++ b/src/api.ts @@ -39,9 +39,7 @@ export const tauriApi: ElectronAPI = { return () => {} }, getLaunchAtStartup: () => invoke('get_launch_at_startup'), - setLaunchAtStartup: (value) => { - invoke('set_launch_at_startup', { enabled: value }) - }, + setLaunchAtStartup: (value) => invoke('set_launch_at_startup', { enabled: value }), updateGlobalShortcut: (action, oldShortcut, newShortcut) => invoke('update_global_shortcut', { action, oldShortcut, newShortcut }), onTriggerNewNote: (callback) => { @@ -84,4 +82,13 @@ export const tauriApi: ElectronAPI = { }, pauseShortcuts: () => invoke('pause_shortcuts') as unknown as void, resumeShortcuts: () => invoke('resume_shortcuts') as unknown as void, + onUpdateReady: (callback) => { + let unlisten: (() => void) | undefined + listen('update-ready', () => callback()).then((fn) => { + unlisten = fn + }) + return () => { + if (unlisten) unlisten() + } + }, } diff --git a/src/components/TimersPage.tsx b/src/components/TimersPage.tsx index c80c9e7..4f2646c 100644 --- a/src/components/TimersPage.tsx +++ b/src/components/TimersPage.tsx @@ -18,7 +18,6 @@ import { listen } from '@tauri-apps/api/event' interface TimerItemProps { timer: Timer onRemove: (id: string) => void - onPause: (id: string) => void } function formatTime(ms: number): string { @@ -32,7 +31,7 @@ function formatTime(ms: number): string { return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` } -function TimerItem({ timer, onRemove, onPause }: TimerItemProps) { +function TimerItem({ timer, onRemove }: TimerItemProps) { const tickTimer = useTimerStore((s) => s.tickTimer) const timeoutRef = useRef | null>(null) @@ -75,11 +74,6 @@ function TimerItem({ timer, onRemove, onPause }: TimerItemProps) {
{timer.label || 'Timer'}
- {timer.status === 'running' && ( - - )}
) : ( - timers.map((t) => ( - - )) + timers.map((t) => ) )}
diff --git a/src/hooks/useGlobalHotkey.ts b/src/hooks/useGlobalHotkey.ts index a29d07d..1eac32c 100644 --- a/src/hooks/useGlobalHotkey.ts +++ b/src/hooks/useGlobalHotkey.ts @@ -25,10 +25,7 @@ export function useGlobalHotkey() { if (isRecordingShortcut) return // Do not close app while recording shortcut - // Close the app if nothing else was open - if (!state.showNoteSearch && !isRenaming && actionMenuIndex === 0) { - await getCurrentWindow().hide() - } + // Dismiss overlays in priority order — highest-level first if (state.showMainActionMenu) { e.preventDefault() e.stopPropagation() @@ -47,6 +44,15 @@ export function useGlobalHotkey() { setShowGraphView(false) return } + // Timers and Reminders pages have their own ESC handlers — let them fire + if (state.showTimersView || state.showRemindersView) { + return + } + + // Nothing was open: hide the window + if (!isRenaming && actionMenuIndex === 0) { + await getCurrentWindow().hide() + } } // Settings Shortcut diff --git a/src/types.d.ts b/src/types.d.ts index 6f26233..b993711 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -29,12 +29,12 @@ export interface ElectronAPI { cancelTimer: (id: string) => Promise removeOnboardingFiles: () => Promise - quitApp: () => void - openExternal: (url: string) => void - openFile: (path: string) => void + quitApp: () => Promise + openExternal: (url: string) => Promise + openFile: (path: string) => Promise onSwipeGesture: (callback: (direction: string) => void) => () => void getLaunchAtStartup: () => Promise - setLaunchAtStartup: (value: boolean) => void + setLaunchAtStartup: (value: boolean) => Promise updateGlobalShortcut: (action: string, oldShortcut: string, newShortcut: string) => void onTriggerNewNote: (callback: () => void) => () => void onTriggerTasks: (callback: () => void) => () => void @@ -44,6 +44,7 @@ export interface ElectronAPI { onPowerResume: (callback: () => void) => () => void pauseShortcuts: () => void resumeShortcuts: () => void + onUpdateReady: (callback: () => void) => () => void } declare global { From b13d5e02f352d059aaa291992de134ea4c8af91c Mon Sep 17 00:00:00 2001 From: Aditya Date: Sat, 27 Jun 2026 11:14:26 +0530 Subject: [PATCH 2/3] docs: add audit fix entries to AUDIT_LOG and CHANGELOG --- AUDIT_LOG.md | 19 +++++++++++++++++++ CHANGELOG.md | 4 ++++ 2 files changed, 23 insertions(+) diff --git a/AUDIT_LOG.md b/AUDIT_LOG.md index be7f2bf..0105e16 100644 --- a/AUDIT_LOG.md +++ b/AUDIT_LOG.md @@ -2,6 +2,25 @@ This log tracks all significant changes, updates, and versions in the PaperCache project. +## 2026-06-27 +**Change:** fix: address audit findings — ESC logic, IPC types, update UX, mutex safety, ESLint (PR #68) + +**Details/Why:** +Full-pass code-quality audit surfaced several correctness bugs and robustness issues that were fixed in a single PR: + +1. **ESC key logic** (`useGlobalHotkey.ts`) — The window was being hidden before checking whether any overlay (graph, timer panel, reminders) was open. Restructured to dismiss overlays in priority order; window hides only as final fallback. +2. **IPC `void` types** (`types.d.ts`, `api.ts`) — `openExternal`, `openFile`, `setLaunchAtStartup`, `quitApp` were declared as returning `void` but the underlying `invoke()` is async. Changed to `Promise`; `setLaunchAtStartup` now returns its promise instead of fire-and-forget. +3. **Silent auto-restart** (`system.rs`) — App was calling `app.restart()` immediately after an update download with no user notice. Now runs download in a background Tokio task, emits `update-ready` to the frontend (which shows a toast), waits 3 seconds, then restarts. +4. **Mutex `unwrap()` panics** (`shortcuts.rs`) — Two `.unwrap()` calls on `Mutex::lock()` replaced with `.map_err(|e| e.to_string())?` for graceful error propagation. +5. **Dead-end Pause button** (`TimersPage.tsx`) — Removed the ⏸ button since `resumeTimer` is an unimplemented stub; a button with no un-pause path was worse UX than no button. +6. **macOS `AppHandle` memory leak** (`macos.rs`) — Added a `SAFETY` comment documenting why `Box::into_raw` is intentionally not paired with `Box::from_raw`. +7. **ESLint warnings** (`GraphView.tsx`) — Introduced a `ForceGraphInstance` interface replacing the `any` ref type; snapshotted `fgRef.current` inside effect body to fix stale-ref cleanup warning. +8. **`react`/`react-dom` dependency category** (`package.json`) — Moved from `devDependencies` to `dependencies`. + +**Files changed:** `src/hooks/useGlobalHotkey.ts`, `src/types.d.ts`, `src/api.ts`, `src/App.tsx`, `src-tauri/src/commands/system.rs`, `src-tauri/src/commands/shortcuts.rs`, `src-tauri/src/macos.rs`, `src/components/TimersPage.tsx`, `src/GraphView.tsx`, `package.json`, `CHANGELOG.md`. + +--- + ## 2026-06-25 **Change:** fix: switch autostart from LaunchAgent to LoginItem (AppleScript) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fa2dfe..a6c8b31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Shortcut key pill "+" centered between key caps**: The `+` separator in the shortcut display was grouped inside the same `` as the key cap to its left with `justify-content: space-evenly`, making it appear visually attached to the left pill. Restructured to a flat flex layout with `gap` so the `+` is independently centered with equal spacing on both sides. - **Login-item toggle stays in sync with macOS System Settings**: The launch-at-startup toggle only read from `localStorage`, so removing PaperCache from System Settings left the toggle permanently stuck in the checked state. Fixed by adding a `get_launch_at_startup` Tauri command that queries the actual OS login-item state via `app.autolaunch().is_enabled()`, and syncing the toggle with the real OS state on every Settings mount. - **Scrollbars hidden on Windows/Linux in Settings and editor**: `.settings-content` and `.editor-container` had `overflow-y: auto`/`overflow: auto` but no scrollbar-hiding rules. macOS overlay scrollbars auto-hide, but Windows/Linux show persistent scrollbars. Added `scrollbar-width: none` (Firefox), `-ms-overflow-style: none` (IE/Edge legacy), and `::-webkit-scrollbar { display: none }` (Chrome/Safari/Edge Chromium) to both containers. +- **ESC no longer hides the window when an overlay is open**: The global hotkey handler was calling `window.hide()` before checking whether the graph view, timer panel, or reminders panel was open. Pressing Escape now correctly dismisses the top-most overlay first; the window only hides as a last resort. +- **IPC error handling for shell/file commands**: `openExternal`, `openFile`, `setLaunchAtStartup`, and `quitApp` now properly propagate backend errors to the caller instead of silently discarding the Promise. +- **Update notification before restart**: When an auto-update is ready, PaperCache now shows a toast ("PaperCache updated — restarting in 3 seconds…") for 3 seconds before restarting, so users are never caught off-guard. +- **Pause button removed from timer panel**: The ⏸ pause button had no corresponding resume path (backend not implemented). Removed to avoid a dead-end UX; the close/remove button remains. ## [v0.5.3] - 2026-06-24 From d72a3025adf84a47d1f4daf68af5fdc3fd6204d3 Mon Sep 17 00:00:00 2001 From: Aditya Date: Sat, 27 Jun 2026 11:26:07 +0530 Subject: [PATCH 3/3] fix: add Emitter import for app.emit() in check_for_updates --- src-tauri/src/commands/system.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs index dee602d..b0afa4e 100644 --- a/src-tauri/src/commands/system.rs +++ b/src-tauri/src/commands/system.rs @@ -1,4 +1,4 @@ -use tauri::{AppHandle, Manager, WebviewWindow}; +use tauri::{AppHandle, Emitter, Manager, WebviewWindow}; use tauri_plugin_opener::OpenerExt; use tauri_plugin_window_state::{AppHandleExt, StateFlags};