From e376f179ce93c1cdbc6987e604cda0b7e7d03480 Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 4.6" Date: Tue, 14 Apr 2026 09:57:03 +0000 Subject: [PATCH 1/2] fix/client: correctly parse SyncStatus::Error payload from Tauri event SyncStatus::Error(String) serializes as {"Error": "..."} in JSON via serde's default enum representation. The frontend was assigning this object directly to the store, causing SYNC_LABEL lookup to return undefined and the status to render as blank. - Normalize the payload in the sync-status listener, extracting the variant name and error message separately - Store syncErrorMessage in the general store (optional arg on setSyncStatus) - Update tests to cover the error message round-trip --- .../src/components/account/AccountMenu.tsx | 15 ++++++++++++--- notto-client/src/store/__tests__/general.test.ts | 7 ++++++- notto-client/src/store/general.tsx | 9 +++++++-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/notto-client/src/components/account/AccountMenu.tsx b/notto-client/src/components/account/AccountMenu.tsx index 8d23d14..b9c21a2 100644 --- a/notto-client/src/components/account/AccountMenu.tsx +++ b/notto-client/src/components/account/AccountMenu.tsx @@ -37,9 +37,18 @@ export default function AccountMenu() { const authMenuRef = useRef(null); useEffect(() => { - listen("sync-status", (event) => { - trace("sync status: " + event.payload); - setSyncStatus(event.payload); + /** `SyncStatus::Error(String)` serializes as `{ Error: string }`, others as plain strings. */ + type SyncStatusPayload = syncStatusEnum | { Error: string }; + + listen("sync-status", (event) => { + const payload = event.payload; + if (typeof payload === "object" && "Error" in payload) { + trace("sync status: Error - " + payload.Error); + setSyncStatus(syncStatusEnum.Error, payload.Error); + } else { + trace("sync status: " + payload); + setSyncStatus(payload as syncStatusEnum); + } }); }, []); diff --git a/notto-client/src/store/__tests__/general.test.ts b/notto-client/src/store/__tests__/general.test.ts index 5ce6ad1..2c12e0d 100644 --- a/notto-client/src/store/__tests__/general.test.ts +++ b/notto-client/src/store/__tests__/general.test.ts @@ -20,6 +20,7 @@ beforeEach(() => { allWorkspaces: [], notes: [], syncStatus: syncStatusEnum.Offline, + syncErrorMessage: null, }); }); @@ -67,7 +68,11 @@ describe("useGeneral", () => { useGeneral.getState().setSyncStatus(syncStatusEnum.Synched); expect(useGeneral.getState().syncStatus).toBe(syncStatusEnum.Synched); - useGeneral.getState().setSyncStatus(syncStatusEnum.Error); + useGeneral.getState().setSyncStatus(syncStatusEnum.Error, "403 Forbidden"); expect(useGeneral.getState().syncStatus).toBe(syncStatusEnum.Error); + expect(useGeneral.getState().syncErrorMessage).toBe("403 Forbidden"); + + useGeneral.getState().setSyncStatus(syncStatusEnum.Synched); + expect(useGeneral.getState().syncErrorMessage).toBeNull(); }); }); diff --git a/notto-client/src/store/general.tsx b/notto-client/src/store/general.tsx index affae1d..398abb6 100644 --- a/notto-client/src/store/general.tsx +++ b/notto-client/src/store/general.tsx @@ -15,11 +15,12 @@ type Store = { allWorkspaces: Workspace[]; notes: Note[]; syncStatus: syncStatusEnum; + syncErrorMessage: string | null; setWorkspace: (newWorkspace: Workspace | null) => void; setAllWorkspaces: (newWorkspaces: Workspace[]) => void; setNotes: (notes: Note[]) => void; - setSyncStatus: (status: syncStatusEnum) => void; + setSyncStatus: (status: syncStatusEnum, errorMessage?: string) => void; }; /** Global store for workspace, note list, and sync status. */ @@ -28,9 +29,13 @@ export const useGeneral = create((set) => ({ allWorkspaces: [], notes: [], syncStatus: syncStatusEnum.Offline, + syncErrorMessage: null, setWorkspace: (newWorkspace) => set(() => ({ workspace: newWorkspace })), setAllWorkspaces: (newWorkspaces) => set(() => ({ allWorkspaces: newWorkspaces })), setNotes: (notes) => set(() => ({ notes })), - setSyncStatus: (status) => set(() => ({ syncStatus: status })), + setSyncStatus: (status, errorMessage) => set(() => ({ + syncStatus: status, + syncErrorMessage: errorMessage ?? null, + })), })); From 9bce00d61304335833fb2dc080ce4e30be63d9a1 Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 4.6" Date: Tue, 14 Apr 2026 12:32:29 +0000 Subject: [PATCH 2/2] fix/client: drop error message from SyncStatus::Error variant The message was already logged by the backend. Sending it to the frontend had no good use: displaying it would spam on every sync cycle, and logging it on the frontend is redundant. Error is now a unit variant, serializing as the plain string "Error" like the other variants. This also removes the payload normalization workaround added in the previous commit. --- notto-client/src-tauri/src/sync/service.rs | 10 +++++----- .../src/components/account/AccountMenu.tsx | 15 +++------------ notto-client/src/store/__tests__/general.test.ts | 7 +------ notto-client/src/store/general.tsx | 9 ++------- 4 files changed, 11 insertions(+), 30 deletions(-) diff --git a/notto-client/src-tauri/src/sync/service.rs b/notto-client/src-tauri/src/sync/service.rs index f7457fd..7e65aaf 100644 --- a/notto-client/src-tauri/src/sync/service.rs +++ b/notto-client/src-tauri/src/sync/service.rs @@ -18,7 +18,7 @@ use crate::{ pub enum SyncStatus { Synched, Syncing, - Error(String), + Error, Offline, NotConnected, } @@ -44,7 +44,7 @@ pub async fn run(handle: AppHandle) { if let Some(ts) = max_ts { if let Err(e) = update_last_sync(&state, workspace.clone(), ts).await { error!("{e:#}"); - emit(&handle, "sync-status", SyncStatus::Error(format!("{e:#}").to_string())); + emit(&handle, "sync-status", SyncStatus::Error); break 'sync; } } @@ -54,7 +54,7 @@ pub async fn run(handle: AppHandle) { emit(&handle, "sync-status", SyncStatus::Offline); info!("Couldn't connect to server"); } else { - emit(&handle, "sync-status", SyncStatus::Error(format!("{e:#}").to_string())); + emit(&handle, "sync-status", SyncStatus::Error); error!("{e:#}"); } break 'sync; @@ -66,7 +66,7 @@ pub async fn run(handle: AppHandle) { if let Some(ts) = max_ts { if let Err(e) = update_last_sync(&state, workspace.clone(), ts).await { error!("{e:#}"); - emit(&handle, "sync-status", SyncStatus::Error(format!("{e:#}").to_string())); + emit(&handle, "sync-status", SyncStatus::Error); break 'sync; } } @@ -76,7 +76,7 @@ pub async fn run(handle: AppHandle) { emit(&handle, "sync-status", SyncStatus::Offline); info!("Couldn't connect to server"); } else { - emit(&handle, "sync-status", SyncStatus::Error(format!("{e:#}").to_string())); + emit(&handle, "sync-status", SyncStatus::Error); error!("{e:#}"); } break 'sync; diff --git a/notto-client/src/components/account/AccountMenu.tsx b/notto-client/src/components/account/AccountMenu.tsx index b9c21a2..8d23d14 100644 --- a/notto-client/src/components/account/AccountMenu.tsx +++ b/notto-client/src/components/account/AccountMenu.tsx @@ -37,18 +37,9 @@ export default function AccountMenu() { const authMenuRef = useRef(null); useEffect(() => { - /** `SyncStatus::Error(String)` serializes as `{ Error: string }`, others as plain strings. */ - type SyncStatusPayload = syncStatusEnum | { Error: string }; - - listen("sync-status", (event) => { - const payload = event.payload; - if (typeof payload === "object" && "Error" in payload) { - trace("sync status: Error - " + payload.Error); - setSyncStatus(syncStatusEnum.Error, payload.Error); - } else { - trace("sync status: " + payload); - setSyncStatus(payload as syncStatusEnum); - } + listen("sync-status", (event) => { + trace("sync status: " + event.payload); + setSyncStatus(event.payload); }); }, []); diff --git a/notto-client/src/store/__tests__/general.test.ts b/notto-client/src/store/__tests__/general.test.ts index 2c12e0d..5ce6ad1 100644 --- a/notto-client/src/store/__tests__/general.test.ts +++ b/notto-client/src/store/__tests__/general.test.ts @@ -20,7 +20,6 @@ beforeEach(() => { allWorkspaces: [], notes: [], syncStatus: syncStatusEnum.Offline, - syncErrorMessage: null, }); }); @@ -68,11 +67,7 @@ describe("useGeneral", () => { useGeneral.getState().setSyncStatus(syncStatusEnum.Synched); expect(useGeneral.getState().syncStatus).toBe(syncStatusEnum.Synched); - useGeneral.getState().setSyncStatus(syncStatusEnum.Error, "403 Forbidden"); + useGeneral.getState().setSyncStatus(syncStatusEnum.Error); expect(useGeneral.getState().syncStatus).toBe(syncStatusEnum.Error); - expect(useGeneral.getState().syncErrorMessage).toBe("403 Forbidden"); - - useGeneral.getState().setSyncStatus(syncStatusEnum.Synched); - expect(useGeneral.getState().syncErrorMessage).toBeNull(); }); }); diff --git a/notto-client/src/store/general.tsx b/notto-client/src/store/general.tsx index 398abb6..affae1d 100644 --- a/notto-client/src/store/general.tsx +++ b/notto-client/src/store/general.tsx @@ -15,12 +15,11 @@ type Store = { allWorkspaces: Workspace[]; notes: Note[]; syncStatus: syncStatusEnum; - syncErrorMessage: string | null; setWorkspace: (newWorkspace: Workspace | null) => void; setAllWorkspaces: (newWorkspaces: Workspace[]) => void; setNotes: (notes: Note[]) => void; - setSyncStatus: (status: syncStatusEnum, errorMessage?: string) => void; + setSyncStatus: (status: syncStatusEnum) => void; }; /** Global store for workspace, note list, and sync status. */ @@ -29,13 +28,9 @@ export const useGeneral = create((set) => ({ allWorkspaces: [], notes: [], syncStatus: syncStatusEnum.Offline, - syncErrorMessage: null, setWorkspace: (newWorkspace) => set(() => ({ workspace: newWorkspace })), setAllWorkspaces: (newWorkspaces) => set(() => ({ allWorkspaces: newWorkspaces })), setNotes: (notes) => set(() => ({ notes })), - setSyncStatus: (status, errorMessage) => set(() => ({ - syncStatus: status, - syncErrorMessage: errorMessage ?? null, - })), + setSyncStatus: (status) => set(() => ({ syncStatus: status })), }));