Fix event endpoint persistence (#831)

* Changes from fix/event-hook-endpoint

* fix: Allow empty eventHooks/ntfyEndpoints to reconcile from server

Remove the `length > 0` guards in fast-hydrate reconciliation that
prevented intentional empty-array clears from syncing across clients.
Server-side wipe protection (`__allowEmpty*` escape hatches) already
ensures empty arrays in the server are intentional.

Addresses PR #831 review feedback from CodeRabbit and Gemini.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-03-03 20:33:12 -08:00
committed by GitHub
parent dd7108a7a0
commit 20e7c74b17
3 changed files with 26 additions and 6 deletions

View File

@@ -2763,6 +2763,21 @@ export class HttpApiClient implements ElectronAPI {
headers?: Record<string, string>; headers?: Record<string, string>;
enabled?: boolean; enabled?: boolean;
}>; }>;
eventHooks?: Array<{
id: string;
trigger: string;
enabled: boolean;
action: Record<string, unknown>;
name?: string;
}>;
ntfyEndpoints?: Array<{
id: string;
name: string;
serverUrl: string;
topic: string;
authType: string;
enabled: boolean;
}>;
}; };
error?: string; error?: string;
}> => this.get('/api/settings/global'), }> => this.get('/api/settings/global'),

View File

@@ -600,10 +600,7 @@ function RootLayoutContent() {
// so updating them won't cause a visible re-render flash. // so updating them won't cause a visible re-render flash.
const serverHooks = (finalSettings as GlobalSettings).eventHooks ?? []; const serverHooks = (finalSettings as GlobalSettings).eventHooks ?? [];
const currentHooks = useAppStore.getState().eventHooks; const currentHooks = useAppStore.getState().eventHooks;
if ( if (JSON.stringify(serverHooks) !== JSON.stringify(currentHooks)) {
JSON.stringify(serverHooks) !== JSON.stringify(currentHooks) &&
serverHooks.length > 0
) {
logger.info( logger.info(
`[FAST_HYDRATE] Reconciling eventHooks from server (server=${serverHooks.length}, store=${currentHooks.length})` `[FAST_HYDRATE] Reconciling eventHooks from server (server=${serverHooks.length}, store=${currentHooks.length})`
); );

View File

@@ -1506,7 +1506,11 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
set({ eventHooks: hooks }); set({ eventHooks: hooks });
try { try {
const httpApi = getHttpApiClient(); const httpApi = getHttpApiClient();
await httpApi.settings.updateGlobal({ eventHooks: hooks }); await httpApi.settings.updateGlobal({
eventHooks: hooks,
// Signal the server that an empty array is intentional (not a wipe from stale state)
...(hooks.length === 0 ? { __allowEmptyEventHooks: true } : {}),
});
} catch (error) { } catch (error) {
logger.error('Failed to sync event hooks:', error); logger.error('Failed to sync event hooks:', error);
} }
@@ -1517,7 +1521,11 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
set({ ntfyEndpoints: endpoints }); set({ ntfyEndpoints: endpoints });
try { try {
const httpApi = getHttpApiClient(); const httpApi = getHttpApiClient();
await httpApi.settings.updateGlobal({ ntfyEndpoints: endpoints }); await httpApi.settings.updateGlobal({
ntfyEndpoints: endpoints,
// Signal the server that an empty array is intentional (not a wipe from stale state)
...(endpoints.length === 0 ? { __allowEmptyNtfyEndpoints: true } : {}),
});
} catch (error) { } catch (error) {
logger.error('Failed to sync ntfy endpoints:', error); logger.error('Failed to sync ntfy endpoints:', error);
} }