diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 89aa72bba..39d2c4904 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -320,8 +320,12 @@ jobs:
publish-preview:
name: Publish multi-platform preview image
- needs: [preview-amd64, preview-arm64]
+ needs: [container, preview-amd64, preview-arm64]
if: |
+ !cancelled() &&
+ needs.container.result == 'success' &&
+ needs.preview-amd64.result == 'success' &&
+ needs.preview-arm64.result == 'success' &&
(
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'push' && github.ref == 'refs/heads/release/v2') ||
@@ -375,9 +379,6 @@ jobs:
ARM64_DIGEST: ${{ needs.preview-arm64.outputs.digest }}
run: |
tags=(-t "${PREVIEW_IMAGE}:${PREVIEW_TAG}")
- if [[ "${GITHUB_EVENT_NAME}" == "push" && "${GITHUB_REF}" == "refs/heads/release/v2" ]]; then
- tags+=(-t "${PREVIEW_IMAGE}:pr-6545")
- fi
docker buildx imagetools create "${tags[@]}" \
"${PREVIEW_IMAGE}@${AMD64_DIGEST}" \
"${PREVIEW_IMAGE}@${ARM64_DIGEST}"
@@ -386,9 +387,6 @@ jobs:
if: "!env.ACT"
run: |
tags=("${PREVIEW_TAG}")
- if [[ "${GITHUB_EVENT_NAME}" == "push" && "${GITHUB_REF}" == "refs/heads/release/v2" ]]; then
- tags+=("pr-6545")
- fi
for tag in "${tags[@]}"; do
image="${PREVIEW_IMAGE}:${tag}"
manifest="$(docker buildx imagetools inspect "$image" --raw)"
@@ -435,6 +433,14 @@ jobs:
core.info(`No package version found with tag "${tag}"`);
return;
}
+ const targetTags = target.metadata?.container?.tags ?? [];
+ const extraTags = targetTags.filter((targetTag) => targetTag !== tag);
+ if (extraTags.length > 0) {
+ core.setFailed(
+ `Package version ${target.id} also carries tag(s) ${extraTags.map((targetTag) => `"${targetTag}"`).join(", ")}; refusing cleanup.`,
+ );
+ return;
+ }
await github.rest.packages.deletePackageVersionForOrg({
package_type: "container",
package_name: packageName,
diff --git a/apps/docs/docs/management/api/index.mdx b/apps/docs/docs/management/api/index.mdx
index fab706520..67fa8119f 100644
--- a/apps/docs/docs/management/api/index.mdx
+++ b/apps/docs/docs/management/api/index.mdx
@@ -115,6 +115,8 @@ The OpenAPI and MCP APIs include board management endpoints for common automatio
Board layout responses identify each layout with a `mobile`, `base`, or `custom` role. Mobile and Base are protected. Layout-saving clients must preserve existing roles, keep Mobile at breakpoint `0`, keep Base as the highest breakpoint, and use unique custom breakpoints between them. The layout save operation returns the canonical saved layouts, including server-generated IDs for new custom layouts. A non-Base layout can be regenerated from Base with the protected reset operation.
+The internal `board.getManageOverview` tRPC query returns compact board previews limited to 12 layout rows by default. Callers may provide `previewRowLimit` from 1 through 48; `fullPreview: true` requests the server's 48-row maximum.
+
Use the interactive API reference to inspect the exact request body for each endpoint under the `boards` and `settings` tags.
## Docker action payload migration
diff --git a/apps/docs/docs/management/settings/index.mdx b/apps/docs/docs/management/settings/index.mdx
index 215ea1bd5..f45fabdc5 100644
--- a/apps/docs/docs/management/settings/index.mdx
+++ b/apps/docs/docs/management/settings/index.mdx
@@ -128,6 +128,26 @@ You can decide if app status is disabled on all boards.
You can set the default color scheme for users without their own color scheme.
+## Branding
+
+Branding settings let administrators adapt Homarr to the identity and visual style of their installation. Changes apply across the application and authentication pages after you save the settings.
+
+### Identity
+
+- **Application name** replaces the default Homarr name in page titles and branded surfaces.
+- **Logo** is used in the header and other application branding.
+- **Favicon** controls the browser tab icon. When it is empty, Homarr falls back to the configured logo.
+
+### Colors and corners
+
+Choose the primary and secondary colors used by the theme. **Lock primary color** prevents users from overriding the configured primary color. **Default corner style** controls the global radius used by cards, controls, and other interface elements.
+
+### Sign-in page
+
+You can set a greeting and a background image for authentication pages. The overlay slider adjusts the contrast between the background and the sign-in content. The visibility controls independently show or hide the application name, logo, and greeting.
+
+Use an image URL that every user can load before signing in. A URL that requires an authenticated Homarr session will not work on the sign-in page.
+
## Culture
### Default language
diff --git a/apps/docs/docs/widgets/iframe/index.mdx b/apps/docs/docs/widgets/iframe/index.mdx
index 5f410a7f3..040e08c31 100644
--- a/apps/docs/docs/widgets/iframe/index.mdx
+++ b/apps/docs/docs/widgets/iframe/index.mdx
@@ -67,6 +67,8 @@ add_header Content-Security-Policy "frame-ancestors 'self' https://trusted-domai
- **Wikipedia homepage:** https://en.wikipedia.org/wiki/Main_Page
- **Any YouTube video or stream:** https://www.youtube.com/embed/dQw4w9WgXcQ?si=MWUbbG66UNTla_Ni
+Homarr passes the configured URL directly to the browser. The iframe sandbox and configured permissions still apply.
+
### Configuration
diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json
index 16392ff8d..f5c4794b7 100644
--- a/apps/nextjs/package.json
+++ b/apps/nextjs/package.json
@@ -72,6 +72,7 @@
"@rexa-developer/tiks": "catalog:",
"@scalar/api-reference-react": "catalog:",
"@tabler/icons-react": "catalog:",
+ "@tanstack/query-sync-storage-persister": "catalog:",
"@tanstack/react-devtools": "catalog:",
"@tanstack/react-query": "catalog:",
"@tanstack/react-query-devtools": "catalog:",
diff --git a/apps/nextjs/src/app/[locale]/_client-providers/query-persistence.spec.ts b/apps/nextjs/src/app/[locale]/_client-providers/query-persistence.spec.ts
index 5c18c31f6..d7324e679 100644
--- a/apps/nextjs/src/app/[locale]/_client-providers/query-persistence.spec.ts
+++ b/apps/nextjs/src/app/[locale]/_client-providers/query-persistence.spec.ts
@@ -1,20 +1,17 @@
import { dehydrate, QueryClient } from "@tanstack/react-query";
import type { PersistedClient } from "@tanstack/react-query-persist-client";
-import { persistQueryClientRestore } from "@tanstack/react-query-persist-client";
-import { parse, stringify } from "superjson";
import { afterEach, describe, expect, test, vi } from "vitest";
import {
- createSessionQueryPersister,
+ createSessionQueryPersistence,
getQueryPersistenceStorageKey,
queryPersistenceBuster,
- queryPersistenceMaxDataBytes,
shouldPersistDashboardQuery,
} from "./query-persistence";
-const createSuccessfulQuery = (queryKey: readonly unknown[], data: unknown, updatedAt = Date.now()) => {
+const createSuccessfulQuery = (queryKey: readonly unknown[], data: unknown) => {
const queryClient = new QueryClient();
- queryClient.setQueryData(queryKey, data, { updatedAt });
+ queryClient.setQueryData(queryKey, data);
const query = queryClient.getQueryCache().find({ queryKey });
if (!query) throw new Error("Expected query to be present");
return { query, queryClient };
@@ -37,314 +34,98 @@ const createMemoryStorage = (): Storage => {
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
- window.sessionStorage.clear();
});
-describe("widget query persistence", () => {
- test("persists successful dashboard data but not unrelated, ping, pending, or oversized queries", () => {
- const widget = createSuccessfulQuery([["widget", "weather", "getWeather"], { type: "query" }], { temp: 21 });
+describe("dashboard query persistence", () => {
+ test("keeps usable dashboard data and skips volatile or unrelated queries", () => {
+ const widget = createSuccessfulQuery([["widget", "weather", "atLocation"], { type: "query" }], { temp: 21 });
const supporting = createSuccessfulQuery([["app", "byIds"], { type: "query" }], [{ id: "app-1" }]);
- const unrelated = createSuccessfulQuery([["board", "getBoardByName"], { type: "query" }], { name: "Home" });
const ping = createSuccessfulQuery([["widget", "app", "ping"], { type: "query" }], { status: "online" });
- const oversized = createSuccessfulQuery([["widget", "example"], { type: "query" }], {
- value: "x".repeat(queryPersistenceMaxDataBytes + 1),
+ const beszel = createSuccessfulQuery([["widget", "beszel", "getSystemStats"], { type: "query" }], []);
+ const customApi = createSuccessfulQuery([["widget", "customApi", "getData"], { type: "query" }], {
+ apiKey: "secret",
});
+ const unrelated = createSuccessfulQuery([["board", "getBoardByName"], { type: "query" }], { name: "Home" });
const pendingClient = new QueryClient();
const pending = pendingClient.getQueryCache().build(pendingClient, {
queryKey: [["widget", "calendar", "findAllEvents"], { type: "query" }],
queryFn: () => Promise.resolve([]),
});
+ const failed = createSuccessfulQuery([["widget", "calendar", "findAllEvents"], { type: "query" }], []);
+ failed.query.setState({ ...failed.query.state, error: new Error("refresh failed"), status: "error" });
expect(shouldPersistDashboardQuery(widget.query)).toBe(true);
expect(shouldPersistDashboardQuery(supporting.query)).toBe(true);
- expect(shouldPersistDashboardQuery(unrelated.query)).toBe(false);
expect(shouldPersistDashboardQuery(ping.query)).toBe(false);
- expect(shouldPersistDashboardQuery(oversized.query)).toBe(false);
+ expect(shouldPersistDashboardQuery(beszel.query)).toBe(false);
+ expect(shouldPersistDashboardQuery(customApi.query)).toBe(false);
+ expect(shouldPersistDashboardQuery(unrelated.query)).toBe(false);
expect(shouldPersistDashboardQuery(pending)).toBe(false);
+ expect(shouldPersistDashboardQuery(failed.query)).toBe(true);
});
- test("uses stable, isolated storage keys for anonymous and authenticated scopes", () => {
- expect(getQueryPersistenceStorageKey("user-a")).toBe(getQueryPersistenceStorageKey("user-a"));
- expect(getQueryPersistenceStorageKey("user-a")).not.toBe(getQueryPersistenceStorageKey("user-b"));
- expect(getQueryPersistenceStorageKey(null)).not.toBe(getQueryPersistenceStorageKey("user-a"));
-
- const userAKey = getQueryPersistenceStorageKey("user-a");
- window.sessionStorage.setItem(userAKey, "a");
- createSessionQueryPersister("user-a").removeClient();
- expect(window.sessionStorage.getItem(userAKey)).toBeNull();
- });
-
- test("evicts every other scope so signing out cannot leave data behind", () => {
- const userAKey = getQueryPersistenceStorageKey("user-a");
- const anonymousKey = getQueryPersistenceStorageKey(null);
- window.sessionStorage.setItem(userAKey, "a");
- window.sessionStorage.setItem(anonymousKey, "anonymous");
- window.sessionStorage.setItem("unrelated", "kept");
-
- createSessionQueryPersister("user-b");
-
- expect(window.sessionStorage.getItem(userAKey)).toBeNull();
- expect(window.sessionStorage.getItem(anonymousKey)).toBeNull();
- expect(window.sessionStorage.getItem("unrelated")).toBe("kept");
- });
-
- test("never restores another session scope", async () => {
- const queryKey = [["widget", "weather", "getWeather"], { type: "query" }] as const;
- const { queryClient } = createSuccessfulQuery(queryKey, { temp: 21 });
- const userAPersister = createSessionQueryPersister("user-a");
- userAPersister.persistClient({
- timestamp: Date.now(),
- buster: queryPersistenceBuster,
- clientState: dehydrate(queryClient),
- });
- userAPersister.flush();
- expect((await userAPersister.restoreClient())?.clientState.queries).toHaveLength(1);
-
- expect(createSessionQueryPersister("user-b").restoreClient()).toBeUndefined();
- });
-
- test("stops persisting once the provider tears the cache down", async () => {
- const storage = createMemoryStorage();
- const persister = createSessionQueryPersister("user-a", storage);
- const { queryClient } = createSuccessfulQuery([["widget", "weather", "getWeather"], { type: "query" }], {
- temp: 21,
- });
-
- persister.persistClient({
- timestamp: Date.now(),
- buster: queryPersistenceBuster,
- clientState: dehydrate(queryClient),
- });
- persister.flush();
- persister.stop();
-
- persister.persistClient({
- timestamp: Date.now(),
- buster: queryPersistenceBuster,
- clientState: { mutations: [], queries: [] },
- });
- persister.flush();
-
- expect((await persister.restoreClient())?.clientState.queries).toHaveLength(1);
- });
-
- test("removes the cache instead of storing an empty one", () => {
- const scope = "user-a";
- const key = getQueryPersistenceStorageKey(scope);
- const persister = createSessionQueryPersister(scope);
- window.sessionStorage.setItem(key, "stale");
-
- persister.persistClient({
- timestamp: Date.now(),
- buster: queryPersistenceBuster,
- clientState: { mutations: [], queries: [] },
- });
- persister.flush();
-
- expect(window.sessionStorage.getItem(key)).toBeNull();
- });
-
- test("round-trips SuperJSON data, strips refresh errors, and removes corrupted storage", async () => {
+ test("round-trips dashboard data with SuperJSON in an isolated session scope", async () => {
vi.useFakeTimers();
- const scope = "user-a";
- const key = getQueryPersistenceStorageKey(scope);
- const persister = createSessionQueryPersister(scope);
- const { query, queryClient } = createSuccessfulQuery([["widget", "calendar", "findAllEvents"], { type: "query" }], {
+ const storage = createMemoryStorage();
+ const userA = createSessionQueryPersistence("user-a", storage);
+ const userB = createSessionQueryPersistence("user-b", storage);
+ const { queryClient } = createSuccessfulQuery([["widget", "calendar", "findAllEvents"], { type: "query" }], {
startsAt: new Date("2026-08-22T12:00:00.000Z"),
});
- query.setState({ ...query.state, error: new Error("private refresh error"), status: "error" });
- expect(shouldPersistDashboardQuery(query)).toBe(true);
+ const cachedQuery = queryClient.getQueryCache().find({
+ queryKey: [["widget", "calendar", "findAllEvents"], { type: "query" }],
+ });
+ if (!cachedQuery) throw new Error("Expected cached query to be present");
+ cachedQuery.setState({ ...cachedQuery.state, error: new Error("refresh failed"), status: "error" });
+ queryClient.getMutationCache().build(
+ queryClient,
+ { mutationKey: ["integration", "create"], mutationFn: async () => undefined },
+ {
+ context: undefined,
+ data: undefined,
+ error: null,
+ failureCount: 0,
+ failureReason: null,
+ isPaused: true,
+ status: "pending",
+ submittedAt: Date.now(),
+ variables: { apiKey: "secret" },
+ },
+ );
const persistedClient: PersistedClient = {
timestamp: Date.now(),
buster: queryPersistenceBuster,
- clientState: dehydrate(queryClient, { shouldDehydrateQuery: shouldPersistDashboardQuery }),
+ clientState: dehydrate(queryClient, userA.dehydrateOptions),
};
- persister.persistClient(persistedClient);
- persister.flush();
- const restored = await persister.restoreClient();
- const restoredData = restored?.clientState.queries[0]?.state.data as { startsAt?: Date } | undefined;
- expect(restoredData?.startsAt).toEqual(new Date("2026-08-22T12:00:00.000Z"));
+ expect(persistedClient.clientState.mutations).toHaveLength(0);
+
+ await userA.persister.persistClient(persistedClient);
+ await vi.advanceTimersByTimeAsync(1_000);
+
+ const restored = await userA.persister.restoreClient();
+ expect(restored?.clientState.queries[0]?.state.data).toEqual({
+ startsAt: new Date("2026-08-22T12:00:00.000Z"),
+ });
expect(restored?.clientState.queries[0]?.state.status).toBe("success");
expect(restored?.clientState.queries[0]?.state.error).toBeNull();
- expect(restored?.clientState.queries[0]?.state.isInvalidated).toBe(true);
- expect(window.sessionStorage.getItem(key)).not.toContain("private refresh error");
+ expect(await userB.persister.restoreClient()).toBeUndefined();
- window.sessionStorage.setItem(key, "not-valid-superjson");
- expect(persister.restoreClient()).toBeUndefined();
- expect(window.sessionStorage.getItem(key)).toBeNull();
+ await userA.persister.removeClient();
+ expect(storage.getItem(getQueryPersistenceStorageKey("user-a"))).toBeNull();
});
- test("sanitizes structurally valid but unsafe stored state before hydration", async () => {
- const scope = "user-a";
- const key = getQueryPersistenceStorageKey(scope);
- const { queryClient } = createSuccessfulQuery(
- [["widget", "calendar", "findAllEvents"], { type: "query" }],
- [{ title: "safe" }],
- );
- const validQuery = dehydrate(queryClient).queries[0];
- if (!validQuery) throw new Error("Expected a dehydrated query");
-
- window.sessionStorage.setItem(
- key,
- stringify({
- timestamp: Date.now(),
- buster: queryPersistenceBuster,
- clientState: {
- mutations: [{ state: { status: "pending" } }],
- queries: [
- {
- ...validQuery,
- state: {
- ...validQuery.state,
- error: new Error("private refresh error"),
- fetchStatus: "fetching",
- status: "error",
- },
- },
- {
- ...validQuery,
- queryHash: "unrelated",
- queryKey: [["board", "getBoardByName"], { type: "query" }],
- },
- { queryHash: "malformed", queryKey: [["widget", "weather"]], state: null },
- {
- ...validQuery,
- queryHash: "missing-timestamp",
- state: { ...validQuery.state, dataUpdatedAt: undefined },
- },
- ],
- },
- }),
- );
-
- const restored = await createSessionQueryPersister(scope).restoreClient();
- expect(restored?.clientState.mutations).toEqual([]);
- expect(restored?.clientState.queries).toHaveLength(1);
- expect(restored?.clientState.queries[0]?.state.status).toBe("success");
- expect(restored?.clientState.queries[0]?.state.fetchStatus).toBe("idle");
- expect(restored?.clientState.queries[0]?.state.error).toBeNull();
- });
-
- test.each([
- ["expired", 1, queryPersistenceBuster],
- ["different buster", new Date("2026-08-22T12:00:00.000Z").getTime(), "old-version"],
- ["future timestamp", new Date("2026-08-22T12:00:00.000Z").getTime() + 1, queryPersistenceBuster],
- ])("discards %s cache entries", async (_case, timestamp, storedBuster) => {
- vi.useFakeTimers();
- vi.setSystemTime(new Date("2026-08-22T12:00:00.000Z"));
- const scope = "user-a";
- const persister = createSessionQueryPersister(scope);
- const { queryClient } = createSuccessfulQuery([["widget", "weather", "getWeather"], { type: "query" }], {
- temp: 21,
- });
- persister.persistClient({ timestamp, buster: storedBuster, clientState: dehydrate(queryClient) });
- persister.flush();
-
- const restoredClient = new QueryClient();
- await persistQueryClientRestore({
- queryClient: restoredClient,
- persister,
- maxAge: 5 * 60 * 1_000,
- buster: queryPersistenceBuster,
- });
-
- expect(restoredClient.getQueryData([["widget", "weather", "getWeather"], { type: "query" }])).toBeUndefined();
- expect(window.sessionStorage.getItem(getQueryPersistenceStorageKey(scope))).toBeNull();
- });
-
- test("quota retry writes the cache after evicting the oldest query", async () => {
- const old = createSuccessfulQuery([["widget", "old"], { type: "query" }], "old", 1);
- const recent = createSuccessfulQuery([["widget", "recent"], { type: "query" }], "recent", 2);
- const storage = createMemoryStorage();
- const setItem = storage.setItem.bind(storage);
- const setItemSpy = vi.spyOn(storage, "setItem").mockImplementation((key, value) => {
- const client = parse(value);
- if (client.clientState.queries.length > 1) throw new DOMException("quota", "QuotaExceededError");
- setItem(key, value);
- });
- const persistedClient: PersistedClient = {
- timestamp: Date.now(),
- buster: queryPersistenceBuster,
- clientState: {
- mutations: [],
- queries: [...dehydrate(old.queryClient).queries, ...dehydrate(recent.queryClient).queries],
- },
- };
- const persister = createSessionQueryPersister("user-a", storage);
-
- persister.persistClient(persistedClient);
- persister.flush();
-
- expect(setItemSpy).toHaveBeenCalledTimes(2);
- const restored = await persister.restoreClient();
- expect(restored?.clientState.queries).toHaveLength(1);
- expect(restored?.clientState.queries[0]?.queryKey).toEqual([["widget", "recent"], { type: "query" }]);
- });
-
- test("coalesces pending writes and can synchronously flush the latest cache before reload", async () => {
- vi.useFakeTimers();
- const storage = createMemoryStorage();
- const setItem = vi.spyOn(storage, "setItem");
- const queryKey = [["widget", "weather", "getWeather"], { type: "query" }] as const;
- const first = createSuccessfulQuery(queryKey, { temp: 20 }, 1);
- const latest = createSuccessfulQuery(queryKey, { temp: 21 }, 2);
- const persister = createSessionQueryPersister("user-a", storage);
-
- persister.persistClient({
- timestamp: Date.now(),
- buster: queryPersistenceBuster,
- clientState: dehydrate(first.queryClient),
- });
- persister.persistClient({
- timestamp: Date.now(),
- buster: queryPersistenceBuster,
- clientState: dehydrate(latest.queryClient),
- });
-
- expect(setItem).not.toHaveBeenCalled();
- persister.flush();
- expect(setItem).toHaveBeenCalledOnce();
- await vi.advanceTimersByTimeAsync(0);
- expect(setItem).toHaveBeenCalledOnce();
-
- const restored = await persister.restoreClient();
- expect(restored?.clientState.queries[0]?.state.data).toEqual({ temp: 21 });
- });
-
- test("treats blocked storage operations as an empty cache", async () => {
- const blockedStorage = {
- getItem: () => {
- throw new DOMException("blocked", "SecurityError");
- },
- removeItem: () => {
- throw new DOMException("blocked", "SecurityError");
- },
- setItem: () => {
- throw new DOMException("blocked", "SecurityError");
- },
- } as unknown as Storage;
- const persister = createSessionQueryPersister("blocked", blockedStorage);
- const { queryClient } = createSuccessfulQuery([["widget", "weather"], { type: "query" }], { temp: 21 });
-
- expect(persister.restoreClient()).toBeUndefined();
- expect(() => {
- persister.persistClient({
- timestamp: Date.now(),
- buster: queryPersistenceBuster,
- clientState: dehydrate(queryClient),
- });
- persister.flush();
- }).not.toThrow();
- expect(() => persister.removeClient()).not.toThrow();
- });
-
- test("treats an unavailable session storage getter as an empty cache", async () => {
- vi.spyOn(window, "sessionStorage", "get").mockImplementation(() => {
+ test("treats unavailable session storage as an empty cache", async () => {
+ const blockedStorage = createMemoryStorage();
+ blockedStorage.getItem = () => {
throw new DOMException("blocked", "SecurityError");
- });
+ };
+ blockedStorage.removeItem = () => {
+ throw new DOMException("blocked", "SecurityError");
+ };
- expect(() => createSessionQueryPersister("blocked-getter")).not.toThrow();
- expect(createSessionQueryPersister("blocked-getter").restoreClient()).toBeUndefined();
+ const persistence = createSessionQueryPersistence("blocked", blockedStorage);
+ expect(await persistence.persister.restoreClient()).toBeUndefined();
+ expect(() => persistence.persister.removeClient()).not.toThrow();
});
});
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 e5145ff5c..dbfd90c2f 100644
--- a/apps/nextjs/src/app/[locale]/_client-providers/query-persistence.ts
+++ b/apps/nextjs/src/app/[locale]/_client-providers/query-persistence.ts
@@ -1,13 +1,12 @@
import type { QueryKey } from "@tanstack/react-query";
-import type { PersistedClient, Persister } from "@tanstack/react-query-persist-client";
+import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
import { removeOldestQuery } from "@tanstack/react-query-persist-client";
+import type { PersistedClient, PersistQueryClientProviderProps } from "@tanstack/react-query-persist-client";
import { parse, stringify } from "superjson";
-import { isWidgetDataQueryKey, queryCacheDefaultGcTimeMs } from "@homarr/api/query-cache";
+import { isPersistableDashboardQueryKey, queryCacheDefaultGcTimeMs } from "@homarr/api/query-cache";
-export const queryPersistenceMaxAgeMs = queryCacheDefaultGcTimeMs;
-export const queryPersistenceMaxDataBytes = 256 * 1024;
-export const queryPersistenceBuster = "v2-session-superjson";
+export const queryPersistenceBuster = "v6-dashboard-data";
const queryPersistenceStoragePrefix = "homarr:widget-query-cache";
@@ -16,95 +15,33 @@ export const getQueryPersistenceStorageKey = (scope: string | null) =>
interface PersistableQuery {
queryKey: QueryKey;
- state: {
- data: unknown;
- dataUpdatedAt: number;
- };
+ state: { data: unknown; status: string };
}
-export type SessionQueryPersister = Persister & { flush: () => void; stop: () => void };
-
-const textEncoder = new TextEncoder();
-const budgetVerdictCache = new WeakMap