diff --git a/apps/docs/docs/management/api/index.mdx b/apps/docs/docs/management/api/index.mdx index 8a6e407c0..2259f2303 100644 --- a/apps/docs/docs/management/api/index.mdx +++ b/apps/docs/docs/management/api/index.mdx @@ -144,6 +144,9 @@ Docker or Podman endpoints from targeting the wrong service. Docker environment, or pass one or more IDs returned by `docker.getEndpoints` to query only those Dockerode clients. The Docker widget uses the same behavior for its environment selector. +`docker.refreshInventory` always refreshes the current replica and returns `scope: "all"` after cross-replica publication or +`scope: "local"` when Redis is unavailable. A local result is partial success: retry after restoring Redis to refresh every replica. + ## Partial integration failures Multi-integration widget queries keep data from integrations that remain available. Calendar and media-server results include diff --git a/apps/nextjs/src/app/[locale]/_client-providers/query-persistence.ts b/apps/nextjs/src/app/[locale]/_client-providers/query-persistence.ts index 8084e761a..0ba1f7186 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/query-persistence.ts +++ b/apps/nextjs/src/app/[locale]/_client-providers/query-persistence.ts @@ -239,10 +239,8 @@ export const createSessionQueryPersister = ( const persistNow = (client: PersistedClient) => { if (!storage || persistenceDisabled) return; let candidate: PersistedClient | undefined; - let sanitizedQueryCount = 0; try { const sanitizedClient = sanitizePersistedClient(client); - sanitizedQueryCount = sanitizedClient.clientState.queries.length; candidate = trimToQueryLimit(sanitizedClient); } catch { persistenceDisabled = true; @@ -254,10 +252,11 @@ export const createSessionQueryPersister = ( return; } + let quotaLimited = false; while (candidate) { try { storage.setItem(key, stringify(candidate)); - if (candidate.clientState.queries.length < sanitizedQueryCount) { + if (quotaLimited) { persistedQueryLimit = Math.min(persistedQueryLimit, candidate.clientState.queries.length); } return; @@ -267,6 +266,7 @@ export const createSessionQueryPersister = ( persistenceDisabled = true; return; } + quotaLimited = true; candidate = keepNewestQueries(candidate, Math.floor(candidate.clientState.queries.length / 2)); } } diff --git a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx index 716ca1c1c..04ac5a5cc 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx @@ -199,12 +199,13 @@ export function DockerTable({ initialData }: DockerTableProps) { refetchOnMount: false, }); const refreshInventory = clientApi.docker.refreshInventory.useMutation({ - async onSuccess() { + async onSuccess(result) { await Promise.all([ utils.docker.getContainers.invalidate(), utils.docker.reconcileServices.invalidate(), utils.docker.getServiceHealth.invalidate(), ]); + if (result.scope === "local") return; showSuccessNotification({ title: tDocker("action.refresh.notification.success.title"), message: tDocker("action.refresh.notification.success.message"), diff --git a/packages/api/src/router/docker/docker-router.ts b/packages/api/src/router/docker/docker-router.ts index 2e84ce42e..bfde1e49a 100644 --- a/packages/api/src/router/docker/docker-router.ts +++ b/packages/api/src/router/docker/docker-router.ts @@ -44,10 +44,12 @@ try { const publishDockerInventoryRefreshAsync = async () => { try { await dockerInventoryRefreshChannel.publishAsync({ requestedAt: Date.now() }); + return true; } catch (error) { if (!isRedisDisabled()) { logger.warn(new Error("Failed to publish Docker inventory refresh", { cause: error })); } + return false; } }; @@ -91,7 +93,9 @@ export const dockerRouter = createTRPCRouter({ .concat(dockerMiddleware()) .mutation(async () => { resetDockerInventory(); - await publishDockerInventoryRefreshAsync(); + const propagated = await publishDockerInventoryRefreshAsync(); + if (propagated) return { scope: "all" as const }; + return { scope: "local" as const }; }), getContainers: permissionRequiredProcedure .requiresPermission("admin")