diff --git a/web/dashboard/src/lib/hooks/use-notifications-feed.ts b/web/dashboard/src/lib/hooks/use-notifications-feed.ts index 970b000..6cafd3a 100644 --- a/web/dashboard/src/lib/hooks/use-notifications-feed.ts +++ b/web/dashboard/src/lib/hooks/use-notifications-feed.ts @@ -40,6 +40,9 @@ export function useNotificationsFeed(options: UseNotificationsFeedOptions = {}) queryFn: () => fetchNotifications(cafeId!, unreadOnly, limit), enabled: !!cafeId, refetchInterval: 60_000, + // Keep polling even when the tab is in the background, so the unread count + // stays current if the live socket missed something — no refresh needed. + refetchIntervalInBackground: true, }); const refresh = useCallback(() => { diff --git a/web/dashboard/src/lib/notifications/use-tab-badge.ts b/web/dashboard/src/lib/notifications/use-tab-badge.ts index 0bdaae6..b22a073 100644 --- a/web/dashboard/src/lib/notifications/use-tab-badge.ts +++ b/web/dashboard/src/lib/notifications/use-tab-badge.ts @@ -94,6 +94,7 @@ export function useTabBadge() { queryFn: () => fetchNotifications(cafeId!, false, 50), enabled: !!cafeId, refetchInterval: 60_000, + refetchIntervalInBackground: true, // update the tab badge even when unfocused }); const unread = data?.unreadCount ?? 0; diff --git a/web/dashboard/src/lib/realtime/use-order-alerts.ts b/web/dashboard/src/lib/realtime/use-order-alerts.ts index 8630052..b781294 100644 --- a/web/dashboard/src/lib/realtime/use-order-alerts.ts +++ b/web/dashboard/src/lib/realtime/use-order-alerts.ts @@ -54,10 +54,23 @@ export function useOrderAlerts() { accessTokenFactory: () => (typeof window !== "undefined" ? localStorage.getItem("meezi_access_token") : null) ?? "", }) - .withAutomaticReconnect() + // Retry FOREVER (capped backoff). The default policy gives up after ~30s, + // leaving the connection dead until a page refresh → missed notifications. + .withAutomaticReconnect({ + nextRetryDelayInMilliseconds: (ctx) => + Math.min(30000, 1000 * 2 ** Math.min(ctx.previousRetryCount, 5)), + }) .build(); let stopped = false; + const joinCafe = () => connection.invoke("JoinCafe", cafeId).catch(() => {}); + + // On reconnect the server group membership is gone — re-join or we silently + // stop receiving notifications. Also catch up anything missed while down. + connection.onreconnected(() => { + void joinCafe(); + void qc.invalidateQueries({ queryKey: ["notifications", cafeId] }); + }); const severityFor = (type: string) => { if (type === "table_call_waiter") return notify.warning; @@ -104,14 +117,36 @@ export function useOrderAlerts() { void connection .start() .then(() => { - if (!stopped) return connection.invoke("JoinCafe", cafeId); + if (!stopped) return joinCafe(); }) .catch(() => { // connection/auth failed — alerts simply won't fire; no UI breakage }); + // If the connection fully dropped (gave up, or the device slept), bring it + // back when the network returns or the tab is focused again. + const ensureConnected = () => { + if (stopped) return; + if (connection.state === signalR.HubConnectionState.Disconnected) { + void connection + .start() + .then(() => { + if (!stopped) return joinCafe(); + }) + .then(() => qc.invalidateQueries({ queryKey: ["notifications", cafeId] })) + .catch(() => {}); + } + }; + const onVisible = () => { + if (document.visibilityState === "visible") ensureConnected(); + }; + window.addEventListener("online", ensureConnected); + document.addEventListener("visibilitychange", onVisible); + return () => { stopped = true; + window.removeEventListener("online", ensureConnected); + document.removeEventListener("visibilitychange", onVisible); void connection.stop(); }; }, [cafeId, locale, qc]);