fix: address PR review — bugs, dead code, error handling

Critical fixes:
- Smart Home: invalidate entity state query after toggle mutation
- Beszel live mode: use 5s refetchInterval instead of global 30s
- Fix unit test assertions for 5-arg setQueryCacheAsync signature
- Fix serialize/deserialize mismatch: JSON.parse on read (matches persister's JSON.stringify)
- Remove render-time setActiveQueryCacheBoardId race (keep only useEffect)

Error handling:
- Log persistence errors instead of swallowing with catch(() => {})
- Log QueryCacheHydration failures with ErrorWithMetadata
- Beszel: log partial failures (system details, container stats)

Dead code removal:
- Remove dead queryKey from all 39 request handlers + lib files
- Delete unused Redis APIs: createCacheChannel, createChannelWithLatestAndEvents,
  createChannelEventHistory, history-channel.ts
- Remove stale createCacheChannel mock from docker test

Cleanup:
- Seed system-resources chart from initial query data (no empty start)
- Remove redundant refetchOnWindowFocus/refetchOnReconnect from BoardProvider
This commit is contained in:
Thomas Camlong committed 2026-06-25 11:25:09 +02:00
1 parent 457b1b0ce0
commit c4a664b7ac
50 files changed
+31 -254

No files matched your search

@@ -3,17 +3,22 @@ import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persi
import { fetchApi } from "@homarr/api/client";
import { getActiveQueryCacheBoardId, queryCacheStoragePrefix } from "@homarr/api/query-cache";
// ponytail: write-only persister — restore is SSR-only via QueryCacheHydration
const queryCacheStorage = {
getItem: (_key: string) => null as string | null,
async setItem(_key: string, value: string) {
const boardId = getActiveQueryCacheBoardId();
if (!boardId) return;
await fetchApi.queryCache.setItem.mutate({ boardId, value }).catch(() => {});
await fetchApi.queryCache.setItem.mutate({ boardId, value }).catch((error) => {
console.warn("[query-cache] persist failed", error);
});
},
async removeItem(_key: string) {
const boardId = getActiveQueryCacheBoardId();
if (!boardId) return;
await fetchApi.queryCache.removeItem.mutate({ boardId }).catch(() => {});
await fetchApi.queryCache.removeItem.mutate({ boardId }).catch((error) => {
console.warn("[query-cache] remove failed", error);
});
},
};
@@ -7,8 +7,6 @@ import "~/styles/gridstack.scss";
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import type { PersistedClient } from "@tanstack/react-query-persist-client";
import { parse } from "superjson";
import { getQueryClient } from "@homarr/api/server";
import { IntegrationProvider } from "@homarr/auth/client";
import { auth } from "@homarr/auth/next";
@@ -123,11 +121,14 @@ async function QueryCacheHydration({ userId, boardId }: { userId: string; boardI
const serialized = await getQueryCacheAsync(userId, boardId);
if (!serialized) return null;
const persisted = parse<PersistedClient>(serialized);
const persisted = JSON.parse(serialized) as PersistedClient;
if (!persisted?.clientState?.queries?.length) return null;
return <HydrationBoundary state={persisted.clientState} />;
} catch {
} catch (error) {
logger.warn(
new ErrorWithMetadata("Failed to hydrate query cache", { userId, boardId }, { cause: error }),
);
return null;
}
}
@@ -29,16 +29,6 @@ vi.mock("@homarr/request-handler/docker", () => ({
}),
},
}));
vi.mock("@homarr/redis", () => ({
createCacheChannel: () => ({
consumeAsync: async () => ({
timestamp: new Date().toISOString(),
data: { containers: [] },
}),
invalidateAsync: async () => {},
}),
}));
vi.mock("@homarr/docker/env", () => ({
env: {
ENABLE_DOCKER: true,
@@ -92,6 +92,7 @@ describe("queryCacheRouter", () => {
"board-1",
"cached-value",
expect.any(Number),
queryCacheMaxValueBytes,
);
await caller.removeItem({ boardId: "board-1" });
@@ -124,6 +125,7 @@ describe("queryCacheRouter", () => {
"board-1",
"cached-value",
expect.any(Number),
queryCacheMaxValueBytes,
);
});
+1 -7
View File
@@ -22,15 +22,9 @@ export const BoardProvider = ({
}>) => {
const { data } = clientApi.board.getBoardByName.useQuery(
{ name: initialBoard.name },
{
initialData: initialBoard,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
},
{ initialData: initialBoard },
);
setActiveQueryCacheBoardId(data.id);
// Update the board name so it can be used within updateBoard method
updateBoardName(initialBoard.name);
-5
View File
@@ -3,9 +3,6 @@ import type { LogLevel } from "@homarr/core/infrastructure/logs/constants";
import { createListChannel, createQueueChannel, createSubPubChannel } from "./lib/channel";
export {
createCacheChannel,
createChannelWithLatestAndEvents,
createChannelEventHistory,
handshakeAsync,
createSubPubChannel,
createGetSetChannel,
@@ -15,8 +12,6 @@ export {
invalidateIntegrationCacheAsync,
} from "./lib/channel";
export { createIntegrationHistoryChannel } from "./lib/channels/history-channel";
export const exampleChannel = createSubPubChannel<{ message: string }>("example");
export const pingChannel = createSubPubChannel<
{ url: string; statusCode: number; durationMs: number } | { url: string; error: string }
-146
View File
@@ -156,80 +156,6 @@ export const removeQueryCacheAsync = async (userId: string, boardId: string) =>
await getSetClient.del(queryCacheKey(userId, boardId));
};
/**
* Creates a new cache channel.
* @param name name of the channel
* @param cacheDurationMs duration in milliseconds to cache
* @returns cache channel object
*/
export const createCacheChannel = <TData>(name: string, cacheDurationMs: number = 5 * 60 * 1000) => {
const cacheChannelName = `cache:${name}`;
return {
/**
* Get the data from the cache channel.
* @returns data or null if not found or expired
*/
getAsync: async () => {
const data = await getSetClient.get(cacheChannelName);
if (!data) return null;
const parsedData = superjson.parse<{ data: TData; timestamp: Date }>(data);
const now = new Date();
const diff = now.getTime() - parsedData.timestamp.getTime();
if (diff > cacheDurationMs) return null;
return parsedData;
},
/**
* Consume the data from the cache channel, if not present or expired, it will call the callback to get new data.
* @param callback callback function to get new data if not present or expired
* @returns data or new data if not present or expired
*/
consumeAsync: async (callback: () => Promise<TData>) => {
const data = await getSetClient.get(cacheChannelName);
const getNewDataAsync = async () => {
logger.debug(`Cache miss for channel '${cacheChannelName}'`);
const newData = await callback();
const result = { data: newData, timestamp: new Date() };
await getSetClient.set(cacheChannelName, superjson.stringify(result));
logger.debug(`Cache updated for channel '${cacheChannelName}'`);
return result;
};
if (!data) {
return await getNewDataAsync();
}
const parsedData = superjson.parse<{ data: TData; timestamp: Date }>(data);
const now = new Date();
const diff = now.getTime() - parsedData.timestamp.getTime();
if (diff > cacheDurationMs) {
return await getNewDataAsync();
}
logger.debug(`Cache hit for channel '${cacheChannelName}'`);
return parsedData;
},
/**
* Invalidate the cache channels data.
*/
invalidateAsync: async () => {
await getSetClient.del(cacheChannelName);
},
/**
* Set the data in the cache channel.
* @param data data to be stored in the cache channel
*/
setAsync: async (data: TData) => {
await getSetClient.set(cacheChannelName, superjson.stringify({ data, timestamp: new Date() }));
},
};
};
/**
* Invalidates all cached data for a given integration by deleting every Redis key
* that starts with `integration:${integrationId}:` as well as the integration's
@@ -260,44 +186,6 @@ export const invalidateIntegrationCacheAsync = async (integrationId: string): Pr
}
};
export const createChannelEventHistory = <TData>(channelName: string, maxElements = 32) => {
return {
subscribe: (callback: (data: TData) => void) => {
return ChannelSubscriptionTracker.subscribe(channelName, (message) => {
callback(superjson.parse(message));
});
},
pushAsync: async (data: TData, options = { publish: false }) => {
if (options.publish) await publisher.publish(channelName, superjson.stringify(data));
await getSetClient.lpush(channelName, superjson.stringify({ data, timestamp: new Date() }));
await getSetClient.ltrim(channelName, 0, maxElements);
},
clearAsync: async () => {
await getSetClient.del(channelName);
},
/**
* Returns a slice of the available data in the channel.
* If any of the indexes are out of range (or -range), returned data will be clamped.
* @param startIndex Start index of the slice, negative values are counted from the end, defaults at beginning of range.
* @param endIndex End index of the slice, negative values are counted from the end, defaults at end of range.
*/
getSliceAsync: async (startIndex = 0, endIndex = -1) => {
const range = await getSetClient.lrange(channelName, startIndex, endIndex);
return range.map((item) => superjson.parse<{ data: TData; timestamp: Date }>(item));
},
getSliceUntilTimeAsync: async (time: Date) => {
const itemsInCollection = await getSetClient.lrange(channelName, 0, -1);
return itemsInCollection
.map((item) => superjson.parse<{ data: TData; timestamp: Date }>(item))
.filter((item) => item.timestamp < time);
},
getLengthAsync: async () => {
return await getSetClient.llen(channelName);
},
name: channelName,
};
};
/**
* @deprecated This function should no longer be used, see history-channel functions.
*/
@@ -362,40 +250,6 @@ export const createChannelEventHistoryOld = <TData>(channelName: string, maxElem
};
};
export const createChannelWithLatestAndEvents = <TData>(channelName: string) => {
return {
subscribe: (callback: (data: TData) => void) => {
return ChannelSubscriptionTracker.subscribe(channelName, (message) => {
callback(superjson.parse(message));
});
},
publishAndUpdateLastStateAsync: async (data: TData) => {
// Set first, then publish. If publish fails the storage is still consistent
// and subscribers will catch up on the next event.
const payload = superjson.stringify({ data, timestamp: new Date() });
await getSetClient.set(channelName, payload);
try {
await publisher.publish(channelName, superjson.stringify(data));
} catch (error) {
logger.warn("Failed to publish cache update (storage is correct, subscribers will catch up)", {
channel: channelName,
error: error instanceof Error ? error.message : String(error),
});
}
},
setAsync: async (data: TData) => {
await getSetClient.set(channelName, superjson.stringify({ data, timestamp: new Date() }));
},
getAsync: async () => {
const data = await getSetClient.get(channelName);
if (!data) return null;
return superjson.parse<{ data: TData; timestamp: Date }>(data);
},
name: channelName,
};
};
const queueClient = createRedisConnection();
type WithId<TItem> = TItem & { _id: string };
@@ -1,6 +0,0 @@
import { createChannelEventHistory } from "../channel";
export const createIntegrationHistoryChannel = <TData>(integrationId: string, queryKey: string, maxElements = 32) => {
const channelName = `integration:${integrationId}:history:${queryKey}`;
return createChannelEventHistory<TData>(channelName, maxElements);
};
@@ -8,7 +8,6 @@ export const anchorNotesListRequestHandler = createIntegrationRequestHandler<
"anchor",
AnchorNotesListInput
>({
queryKey: "anchorNotesList",
async requestAsync(integration, input) {
const instance = await createIntegrationAsync(integration);
return instance.listNotesAsync(input);
@@ -16,7 +15,6 @@ export const anchorNotesListRequestHandler = createIntegrationRequestHandler<
});
export const anchorNoteRequestHandler = createIntegrationRequestHandler<AnchorNote, "anchor", { noteId: string }>({
queryKey: "anchorNote",
async requestAsync(integration, input) {
const instance = await createIntegrationAsync(integration);
return instance.getNoteAsync(input.noteId);
@@ -13,5 +13,4 @@ export const archiveTeamWarriorRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getStatusAsync();
},
queryKey: "archiveTeamWarriorStatus",
});
@@ -12,5 +12,4 @@ export const audiobookshelfRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync();
},
queryKey: "audiobookshelfDashboard",
});
+8 -5
View File
@@ -62,7 +62,10 @@ export const beszelSystemsRequestHandler = createIntegrationRequestHandler<
const systems = await instance.getSystemsAsync();
const enriched = await Promise.all(
systems.map(async (system) => {
const details = await instance.getSystemDetailsAsync(system.id).catch(() => null);
const details = await instance.getSystemDetailsAsync(system.id).catch((error) => {
logger.warn("Failed to fetch Beszel system details", { systemId: system.id, error: String(error) });
return null;
});
return mapToSystemRow(system, details);
}),
);
@@ -73,7 +76,6 @@ export const beszelSystemsRequestHandler = createIntegrationRequestHandler<
});
return enriched;
},
queryKey: "beszelSystems",
});
export interface BeszelAlertsData {
@@ -99,7 +101,6 @@ export const beszelAlertsRequestHandler = createIntegrationRequestHandler<
});
return { alerts, history };
},
queryKey: "beszelAlerts",
});
const timePeriodConfig: Record<string, { type: string; perPage: number }> = {
@@ -127,7 +128,10 @@ export const beszelStatsRequestHandler = createIntegrationRequestHandler<
const instance = await createIntegrationAsync(integration);
const systemStats = await instance.getSystemStatsAsync(input.systemId, config.type, config.perPage);
const containerStats = input.includeDocker
? await instance.getContainerStatsAsync(input.systemId, config.type, config.perPage).catch(() => [])
? await instance.getContainerStatsAsync(input.systemId, config.type, config.perPage).catch((error) => {
logger.warn("Failed to fetch Beszel container stats", { systemId: input.systemId, error: String(error) });
return [];
})
: [];
logger.debug("beszelStats fetch completed", {
integrationId: integration.id,
@@ -139,5 +143,4 @@ export const beszelStatsRequestHandler = createIntegrationRequestHandler<
});
return { systemStats, containerStats };
},
queryKey: "beszelStats",
});
-1
View File
@@ -23,5 +23,4 @@ export const calendarMonthRequestHandler = createIntegrationRequestHandler<
input.showUnmonitored,
);
},
queryKey: "calendarMonth",
});
-1
View File
@@ -12,5 +12,4 @@ export const coolifyRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getInstanceInfoAsync();
},
queryKey: "coolifyInfo",
});
-1
View File
@@ -13,5 +13,4 @@ export const dnsHoleRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getSummaryAsync();
},
queryKey: "dnsHoleSummary",
});
-1
View File
@@ -171,7 +171,6 @@ const mockContainers: {
];
export const dockerContainersRequestHandler = createWidgetRequestHandler({
queryKey: "dockerContainersResult",
async requestAsync() {
if (isDemoMode) {
return mockContainers;
@@ -13,5 +13,4 @@ export const downloadClientRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getClientJobsAndStatusAsync(input);
},
queryKey: "downloadClientJobStatus",
});
@@ -37,7 +37,6 @@ const normalizeBangToken = (token: string) => token.toLowerCase().trim();
const compareBangTokenAsc = (left: DuckDuckGoBang, right: DuckDuckGoBang) => left.t.localeCompare(right.t);
export const duckDuckGoBangsRequestHandler = createRequestHandler({
queryKey: "duckduckgo-bangs",
async requestAsync(_) {
if (env.NO_EXTERNAL_CONNECTION) {
return [];
-4
View File
@@ -18,7 +18,6 @@ export const firewallCpuRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return integrationInstance.getFirewallCpuAsync();
},
queryKey: "firewallCpuSummary",
});
export const firewallMemoryRequestHandler = createIntegrationRequestHandler<
@@ -30,7 +29,6 @@ export const firewallMemoryRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getFirewallMemoryAsync();
},
queryKey: "firewallMemorySummary",
});
export const firewallInterfacesRequestHandler = createIntegrationRequestHandler<
@@ -42,7 +40,6 @@ export const firewallInterfacesRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getFirewallInterfacesAsync();
},
queryKey: "firewallInterfacesSummary",
});
export const firewallVersionRequestHandler = createIntegrationRequestHandler<
@@ -54,5 +51,4 @@ export const firewallVersionRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getFirewallVersionAsync();
},
queryKey: "firewallVersionSummary",
});
@@ -13,7 +13,6 @@ export const systemInfoRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getSystemInfoAsync();
},
queryKey: "systemInfo",
});
export const clusterInfoRequestHandler = createIntegrationRequestHandler<
@@ -25,5 +24,4 @@ export const clusterInfoRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getClusterInfoAsync();
},
queryKey: "clusterInfo",
});
-3
View File
@@ -13,7 +13,6 @@ export const immichStatsRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getServerStatsAsync();
},
queryKey: "immich-server-stats",
});
export const immichAlbumsRequestHandler = createIntegrationRequestHandler<
@@ -29,7 +28,6 @@ export const immichAlbumsRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getAlbumsAsync();
},
queryKey: "immich-albums",
});
export const immichAlbumRequestHandler = createIntegrationRequestHandler<
@@ -41,5 +39,4 @@ export const immichAlbumRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getAlbumAsync(input.albumId);
},
queryKey: "immich-album",
});
@@ -13,5 +13,4 @@ export const indexerManagerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getIndexersAsync();
},
queryKey: "indexerManager",
});
@@ -11,7 +11,6 @@ type IntegrationOfKind<TKind extends IntegrationKind> = Omit<Integration, "kind"
};
interface Options<TData, TKind extends IntegrationKind, TInput extends Record<string, unknown>> {
queryKey: string;
requestAsync: (integration: IntegrationOfKind<TKind>, input: TInput) => Promise<TData>;
}
@@ -24,7 +23,6 @@ export const createIntegrationRequestHandler = <
) => ({
handler: (integration: IntegrationOfKind<TKind>, itemOptions: TInput) =>
createRequestHandler<TData, { options: TInput; integration: IntegrationOfKind<TKind> }>({
queryKey: options.queryKey,
requestAsync: async (input) => options.requestAsync(input.integration, input.options),
}).handler({ options: itemOptions, integration }),
});
@@ -1,5 +1,4 @@
interface Options<TData, TInput extends Record<string, unknown>> {
queryKey: string;
requestAsync: (input: TInput) => Promise<TData>;
}
@@ -1,7 +1,6 @@
import { createRequestHandler } from "./request-handler";
interface Options<TData, TInput extends Record<string, unknown>> {
queryKey: string;
requestAsync: (input: TInput) => Promise<TData>;
}
@@ -10,7 +9,6 @@ export const createWidgetRequestHandler = <TData, TInput extends Record<string,
) => ({
handler: (widgetOptions: TInput) =>
createRequestHandler<TData, TInput>({
queryKey: options.queryKey,
requestAsync: options.requestAsync,
}).handler(widgetOptions),
});
@@ -13,5 +13,4 @@ export const mediaReleaseRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getMediaReleasesAsync();
},
queryKey: "mediaReleases",
});
@@ -13,5 +13,4 @@ export const mediaRequestListRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getRequestsAsync();
},
queryKey: "mediaRequestList",
});
@@ -16,5 +16,4 @@ export const mediaRequestStatsRequestHandler = createIntegrationRequestHandler<
users: await integrationInstance.getUsersAsync(),
};
},
queryKey: "mediaRequestStats",
});
@@ -15,5 +15,4 @@ export const mediaServerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getCurrentSessionsAsync({ showOnlyPlaying: input.showOnlyPlaying });
},
queryKey: "mediaServerSessions",
});
@@ -9,7 +9,6 @@ export const mediaTranscodingRequestHandler = createIntegrationRequestHandler<
IntegrationKindByCategory<"mediaTranscoding">,
{ pageOffset: number; pageSize: number }
>({
queryKey: "mediaTranscoding",
async requestAsync(integration, input) {
const integrationInstance = await createIntegrationAsync(integration);
return {
@@ -6,7 +6,6 @@ import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createWidgetRequestHandler } from "./lib/widget-request-handler";
export const minecraftServerStatusRequestHandler = createWidgetRequestHandler({
queryKey: "minecraftServerStatusApiResult",
async requestAsync(input: { domain: string; isBedrockServer: boolean }) {
const path = `${input.isBedrockServer ? "/bedrock" : ""}/3/${input.domain}`;
@@ -12,5 +12,4 @@ export const navidromeRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync();
},
queryKey: "navidromeDashboard",
});
@@ -13,5 +13,4 @@ export const networkControllerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getNetworkSummaryAsync();
},
queryKey: "networkControllerSummary",
});
@@ -13,5 +13,4 @@ export const notificationsRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getNotificationsAsync();
},
queryKey: "notificationsJobStatus",
});
@@ -12,5 +12,4 @@ export const paperlessNgxStatsRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getStatsAsync();
},
queryKey: "paperless-ngx-stats",
});
-1
View File
@@ -13,5 +13,4 @@ export const releasesRequestHandler = createIntegrationRequestHandler<
const instance = await createIntegrationAsync(integration);
return instance.getLatestMatchingReleaseAsync(input.identifier, input.versionRegex);
},
queryKey: "repositoriesReleases",
});
@@ -10,7 +10,6 @@ import { createWidgetRequestHandler } from "./lib/widget-request-handler";
const logger = createLogger({ module: "rssFeedsRequestHandler" });
export const rssFeedsRequestHandler = createWidgetRequestHandler({
queryKey: "rssFeedList",
async requestAsync(input: { url: string; count: number }) {
const result = (await extract(input.url, {
getExtraEntryFields: (feedEntry) => {
@@ -18,5 +18,4 @@ export const smartHomeEntityStateRequestHandler = createIntegrationRequestHandle
return result.data.state;
},
queryKey: "smartHome-entityState",
});
@@ -12,5 +12,4 @@ export const speedtestTrackerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync();
},
queryKey: "speedtestTrackerDashboard",
});
@@ -6,7 +6,6 @@ import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createWidgetRequestHandler } from "./lib/widget-request-handler";
export const fetchStockPriceHandler = createWidgetRequestHandler({
queryKey: "fetchStockPriceResult",
async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) {
const response = await withTimeoutAsync(async (signal) => {
return await fetchWithTrustedCertificatesAsync(
@@ -13,7 +13,6 @@ export const timetableSearchStationsRequestHandler = createIntegrationRequestHan
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.searchStationsAsync(input.query);
},
queryKey: "timetableSearchStations",
});
export const timetableGetTimetableRequestHandler = createIntegrationRequestHandler<
@@ -28,5 +27,4 @@ export const timetableGetTimetableRequestHandler = createIntegrationRequestHandl
limit: input.limit,
});
},
queryKey: "timetableGetTimetable",
});
-1
View File
@@ -12,5 +12,4 @@ export const tracearrRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync();
},
queryKey: "tracearrDashboard",
});
-6
View File
@@ -12,7 +12,6 @@ export const umamiRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getVisitorStatsAsync(input.websiteId, input.timeFrame, input.eventName);
},
queryKey: "umamiVisitorStats",
});
export const umamiEventNamesRequestHandler = createIntegrationRequestHandler<string[], "umami", { websiteId: string }>({
@@ -20,7 +19,6 @@ export const umamiEventNamesRequestHandler = createIntegrationRequestHandler<str
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getEventNamesAsync(input.websiteId);
},
queryKey: "umamiEventNames",
});
export const umamiTopPagesRequestHandler = createIntegrationRequestHandler<
@@ -32,7 +30,6 @@ export const umamiTopPagesRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getTopPagesAsync(input.websiteId, input.timeFrame, input.limit);
},
queryKey: "umamiTopPages",
});
export const umamiTopReferrersRequestHandler = createIntegrationRequestHandler<
@@ -44,7 +41,6 @@ export const umamiTopReferrersRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getTopReferrersAsync(input.websiteId, input.timeFrame, input.limit);
},
queryKey: "umamiTopReferrers",
});
export const umamiMultiEventRequestHandler = createIntegrationRequestHandler<
@@ -56,7 +52,6 @@ export const umamiMultiEventRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getMultiEventTimeSeriesAsync(input.websiteId, input.timeFrame, input.eventNames);
},
queryKey: "umamiMultiEvent",
});
export const umamiActiveVisitorsRequestHandler = createIntegrationRequestHandler<
@@ -68,5 +63,4 @@ export const umamiActiveVisitorsRequestHandler = createIntegrationRequestHandler
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getActiveVisitorsAsync(input.websiteId);
},
queryKey: "umamiActiveVisitors",
});
@@ -12,7 +12,6 @@ import packageJson from "../../../package.json";
const logger = createLogger({ module: "updateCheckerRequestHandler" });
export const updateCheckerRequestHandler = createRequestHandler({
queryKey: "homarr-update-checker",
async requestAsync(_) {
return {
availableUpdates: await getAvailableUpdatesAsync(packageJson.version),
-1
View File
@@ -13,5 +13,4 @@ export const upsSummariesRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getUpsSummariesAsync();
},
queryKey: "upsSummaries",
});
@@ -12,5 +12,4 @@ export const uptimeKumaRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync();
},
queryKey: "uptimeKumaDashboard",
});
-1
View File
@@ -6,7 +6,6 @@ import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createWidgetRequestHandler } from "./lib/widget-request-handler";
export const weatherRequestHandler = createWidgetRequestHandler({
queryKey: "weatherAtLocation",
async requestAsync(input: { latitude: number; longitude: number }) {
const res = await withTimeoutAsync(async (signal) => {
return await fetchWithTrustedCertificatesAsync(
@@ -1,24 +1,13 @@
"use client";
import { useMemo } from "react";
import { clientApi } from "@homarr/api/client";
export const useLiveStats = (integrationIds: string[], systemId: string, enabled: boolean) => {
const queryInput = useMemo(
() => ({
integrationIds,
systemId,
timePeriod: "1m" as const,
includeDocker: true,
}),
[integrationIds, systemId],
const { data, error } = clientApi.widget.beszel.getSystemStats.useQuery(
{ integrationIds, systemId, timePeriod: "1m" as const, includeDocker: true },
{ enabled: enabled && systemId !== "", refetchInterval: 5_000 },
);
const { data, error } = clientApi.widget.beszel.getSystemStats.useQuery(queryInput, {
enabled: enabled && systemId !== "",
});
return {
data: data ? { systemStats: data.systemStats, containerStats: data.containerStats } : null,
error: error ?? (data?.error ? new Error(data.error) : null),
@@ -25,7 +25,10 @@ export default function SmartHomeEntityStateWidget({
};
const { data: entityState } = clientApi.widget.smartHome.entityState.useQuery(input);
const { mutate } = clientApi.widget.smartHome.switchEntity.useMutation();
const utils = clientApi.useUtils();
const { mutate } = clientApi.widget.smartHome.switchEntity.useMutation({
onSettled: () => void utils.widget.smartHome.entityState.invalidate(input),
});
const attribute = options.entityUnit.length > 0 ? " " + options.entityUnit : "";
@@ -41,7 +41,7 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo
const [items, setItems] = useState<
{ cpu: number; memory: number; gpu: number; network: { up: number; down: number } | null }[]
>([]);
>(() => (data[0] ? [toChartItem(data[0].healthInfo)] : []));
useEffect(() => {
const firstItem = data[0];