From 7135990340ad4ffd9595620c17463bbcccb0bc7c Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:20:53 +0200 Subject: [PATCH] =?UTF-8?q?fix(web):=20critical=20fixes=20=E2=80=94=20pres?= =?UTF-8?q?erve=20tokens=20on=20disconnect,=20clean=20up=20forceRemoveEntr?= =?UTF-8?q?y,=20update=20registry=20on=20reconnect=20failure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - disconnectInstance: save cached tokens BEFORE filtering the instance out, so reconnectInstance can restore disconnected entries from localStorage - reconnectInstance: if instance is not in active instances array, attempt to restore it from cached localStorage token before proceeding with reconnect - reconnectInstance: update registry to unreachable/auth_expired on failure - forceRemoveEntry: now tears down WS, removes from instances array, purges token from localStorage, and cleans up space store (was registry-only before) --- packages/web/src/stores/instanceStore.ts | 86 +++++++++++++++++++++--- 1 file changed, 77 insertions(+), 9 deletions(-) diff --git a/packages/web/src/stores/instanceStore.ts b/packages/web/src/stores/instanceStore.ts index 366c8d10..0b54645b 100644 --- a/packages/web/src/stores/instanceStore.ts +++ b/packages/web/src/stores/instanceStore.ts @@ -437,12 +437,17 @@ export const useInstanceStore = create((set, get) => ({ }); const registryUpdatedAt = Date.now(); - set((state) => { - const updated = state.instances.filter(i => i.origin !== origin); - const userId = useAuthStore.getState().user?.id; - if (userId) saveCachedTokens(updated, userId); - return { instances: updated, registry, registryUpdatedAt }; - }); + // Save the token BEFORE filtering it out of instances — this preserves + // the cached token in localStorage so reconnectInstance can restore it later. + const currentInstances = get().instances; + const userId = useAuthStore.getState().user?.id; + if (userId) saveCachedTokens(currentInstances, userId); + + set((state) => ({ + instances: state.instances.filter(i => i.origin !== origin), + registry, + registryUpdatedAt, + })); // Remove spaces from this instance from the space store useSpaceStore.getState().removeInstanceSpaces(origin); @@ -453,8 +458,39 @@ export const useInstanceStore = create((set, get) => ({ }, reconnectInstance: async (origin: string) => { - const inst = get().instances.find(i => i.origin === origin); - if (!inst || inst.status === 'connected' || inst.status === 'connecting') return; + let inst = get().instances.find(i => i.origin === origin); + + // If the instance was disconnected (removed from active instances array) but + // has a cached token in localStorage, restore it so reconnect can proceed. + if (!inst) { + const userId = useAuthStore.getState().user?.id; + if (!userId) return; + const cached = loadCachedTokens(userId); + const entry = cached[origin]; + if (!entry?.token) return; // No cached token — needs full re-authentication via connectToRemote + + const currentUser = useAuthStore.getState().user; + if (!currentUser) return; + + const client = createApiClient(origin, () => entry.token); + const restoredInstance: ConnectedInstance = { + origin, + label: entry.label || new URL(origin).host, + token: entry.token, + user: currentUser, + username: entry.username || '', + status: 'connecting' as const, + api: client, + }; + + set((state) => ({ + instances: [...state.instances, restoredInstance], + })); + + inst = restoredInstance; + } + + if (inst.status === 'connected' || inst.status === 'connecting') return; // Tokenless placeholders can't reconnect — they need full re-authentication if (!inst.token) return; @@ -497,6 +533,15 @@ export const useInstanceStore = create((set, get) => ({ : i ), })); + + // Update registry on network error + const errRegistry = upsertRegistryEntry(get().registry, origin, { + origin, + status: 'unreachable', + errorMessage: 'Instance unreachable', + }); + set({ registry: errRegistry, registryUpdatedAt: Date.now() }); + connectInstance(origin, inst.token); } else { set((state) => ({ @@ -506,6 +551,14 @@ export const useInstanceStore = create((set, get) => ({ : i ), })); + + // Update registry on auth error + const errRegistry = upsertRegistryEntry(get().registry, origin, { + origin, + status: 'auth_expired', + errorMessage: 'Token expired', + }); + set({ registry: errRegistry, registryUpdatedAt: Date.now() }); } } }, @@ -655,10 +708,25 @@ export const useInstanceStore = create((set, get) => ({ }, forceRemoveEntry: (origin: string) => { + // Tear down WebSocket if connected + disconnectWs(origin); + + // Remove from registry const registry = new Map(get().registry); registry.delete(origin); const registryUpdatedAt = Date.now(); - set({ registry, registryUpdatedAt }); + + // Remove from instances and purge token from localStorage + set((state) => { + const updated = state.instances.filter(i => i.origin !== origin); + const userId = useAuthStore.getState().user?.id; + if (userId) saveCachedTokens(updated, userId); + return { instances: updated, registry, registryUpdatedAt }; + }); + + // Clean up spaces belonging to this instance + useSpaceStore.getState().removeInstanceSpaces(origin); + get().syncRegistry().catch(() => {}); },