Merge branch 'dev' into add-more-openapi

This commit is contained in:
Meier Lukas committed 2025-12-19 20:02:20 +01:00
commit f4cdf7aca6
323 files changed
+4555 -2926

No files matched your search

@@ -1,9 +1,11 @@
import dayjs from "dayjs";
import type { Duration } from "dayjs/plugin/duration";
import { logger } from "@homarr/log";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { createChannelWithLatestAndEvents } from "@homarr/redis";
const logger = createLogger({ module: "cachedRequestHandler" });
interface Options<TData, TInput extends Record<string, unknown>> {
// Unique key for this request handler
queryKey: string;
@@ -34,9 +36,10 @@ export const createCachedRequestHandler = <TData, TInput extends Record<string,
};
if (forceUpdate) {
logger.debug(
`Cached request handler forced update for channel='${channel.name}' queryKey='${options.queryKey}'`,
);
logger.debug("Cached request handler forced update", {
channel: channel.name,
queryKey: options.queryKey,
});
return await requestNewDataAsync();
}
@@ -47,22 +50,27 @@ export const createCachedRequestHandler = <TData, TInput extends Record<string,
dayjs().diff(channelData.timestamp, "milliseconds") > options.cacheDuration.asMilliseconds();
if (shouldRequestNewData) {
logger.debug(
`Cached request handler cache miss for channel='${channel.name}' queryKey='${options.queryKey}' reason='${!channelData ? "no data" : "cache expired"}'`,
);
logger.debug("Cached request handler cache miss", {
channel: channel.name,
queryKey: options.queryKey,
reason: !channelData ? "no data" : "cache expired",
});
return await requestNewDataAsync();
}
logger.debug(
`Cached request handler cache hit for channel='${channel.name}' queryKey='${options.queryKey}' expiresAt='${dayjs(channelData.timestamp).add(options.cacheDuration).toISOString()}'`,
);
logger.debug("Cached request handler cache hit", {
channel: channel.name,
queryKey: options.queryKey,
expiresAt: dayjs(channelData.timestamp).add(options.cacheDuration).toISOString(),
});
return channelData;
},
async invalidateAsync() {
logger.debug(
`Cached request handler invalidating cache channel='${channel.name}' queryKey='${options.queryKey}'`,
);
logger.debug("Cached request handler invalidating cache", {
channel: channel.name,
queryKey: options.queryKey,
});
await this.getCachedOrUpdatedDataAsync({ forceUpdate: true });
},
subscribe(callback: (data: TData) => void) {
@@ -3,10 +3,11 @@ import SuperJSON from "superjson";
import { hashObjectBase64, Stopwatch } from "@homarr/common";
import { decryptSecret } from "@homarr/common/server";
import type { MaybeArray } from "@homarr/common/types";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import { db } from "@homarr/db";
import { getItemsWithIntegrationsAsync, getServerSettingsAsync } from "@homarr/db/queries";
import type { WidgetKind } from "@homarr/definitions";
import { logger } from "@homarr/log";
// This imports are done that way to avoid circular dependencies.
import type { inferSupportedIntegrationsStrict } from "../../../widgets/src";
@@ -14,6 +15,8 @@ import { reduceWidgetOptionsWithDefaultValues } from "../../../widgets/src";
import type { WidgetComponentProps } from "../../../widgets/src/definition";
import type { createCachedIntegrationRequestHandler } from "./cached-integration-request-handler";
const logger = createLogger({ module: "cachedRequestIntegrationJobHandler" });
export const createRequestIntegrationJobHandler = <
TWidgetKind extends WidgetKind,
TIntegrationKind extends inferSupportedIntegrationsStrict<TWidgetKind>,
@@ -37,9 +40,10 @@ export const createRequestIntegrationJobHandler = <
kinds: widgetKinds,
});
logger.debug(
`Found items for integration widgetKinds='${widgetKinds.join(",")}' count=${itemsForIntegration.length}`,
);
logger.debug("Found items for integration", {
widgetKinds: widgetKinds.join(","),
count: itemsForIntegration.length,
});
const distinctIntegrations: {
integrationId: string;
@@ -97,14 +101,14 @@ export const createRequestIntegrationJobHandler = <
);
const stopWatch = new Stopwatch();
await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
logger.debug(
`Ran integration job integration=${integrationId} inputHash='${inputHash}' elapsed=${stopWatch.getElapsedInHumanWords()}`,
);
logger.debug("Ran integration job", {
integration: integrationId,
inputHash,
elapsed: stopWatch.getElapsedInHumanWords(),
});
} catch (error) {
logger.error(
new Error(`Failed to run integration job integration=${integrationId} inputHash='${inputHash}'`, {
cause: error,
}),
new ErrorWithMetadata("Failed to run integration job", { integrationId, inputHash }, { cause: error }),
);
}
}
@@ -1,7 +1,7 @@
import dayjs from "dayjs";
import { z } from "zod/v4";
import { fetchWithTimeout } from "@homarr/common";
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
@@ -11,7 +11,7 @@ export const minecraftServerStatusRequestHandler = createCachedWidgetRequestHand
async requestAsync(input: { domain: string; isBedrockServer: boolean }) {
const path = `${input.isBedrockServer ? "/bedrock" : ""}/3/${input.domain}`;
const response = await fetchWithTimeout(`https://api.mcsrvstat.us${path}`);
const response = await fetchWithTimeoutAsync(`https://api.mcsrvstat.us${path}`);
return responseSchema.parse(await response.json());
},
cacheDuration: dayjs.duration(5, "minutes"),
+3 -1
View File
@@ -4,10 +4,12 @@ import dayjs from "dayjs";
import { z } from "zod/v4";
import type { Modify } from "@homarr/common/types";
import { logger } from "@homarr/log";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
const logger = createLogger({ module: "rssFeedsRequestHandler" });
export const rssFeedsRequestHandler = createCachedWidgetRequestHandler({
queryKey: "rssFeedList",
widgetKind: "rssFeed",
+12 -7
View File
@@ -1,7 +1,7 @@
import dayjs from "dayjs";
import { z } from "zod/v4";
import { fetchWithTimeout } from "@homarr/common";
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
@@ -9,7 +9,7 @@ export const fetchStockPriceHandler = createCachedWidgetRequestHandler({
queryKey: "fetchStockPriceResult",
widgetKind: "stockPrice",
async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) {
const response = await fetchWithTimeout(
const response = await fetchWithTimeoutAsync(
`https://query1.finance.yahoo.com/v8/finance/chart/${input.stock}?range=${input.timeRange}&interval=${input.timeInterval}`,
);
const data = dataSchema.parse(await response.json());
@@ -24,12 +24,16 @@ export const fetchStockPriceHandler = createCachedWidgetRequestHandler({
if (!firstResult) {
throw new Error("Received invalid data");
}
const priceHistory =
firstResult.indicators.quote[0]?.close.filter(
// Filter out null values from price arrays (Yahoo Finance returns null for missing data points)
(value) => value !== null && value !== undefined,
) ?? [];
return {
priceHistory:
firstResult.indicators.quote[0]?.close.filter(
// Filter out null values from price arrays (Yahoo Finance returns null for missing data points)
(value) => value !== null && value !== undefined,
) ?? [],
priceHistory,
previousClose: firstResult.meta.previousClose ?? priceHistory[0] ?? 1,
symbol: firstResult.meta.symbol,
shortName: firstResult.meta.shortName,
};
@@ -58,6 +62,7 @@ const dataSchema = z
meta: z.object({
symbol: z.string(),
shortName: z.string(),
previousClose: z.number().optional(),
}),
}),
),
@@ -2,14 +2,16 @@ import dayjs from "dayjs";
import { Octokit } from "octokit";
import { compareSemVer, isValidSemVer } from "semver-parser";
import { fetchWithTimeout } from "@homarr/common";
import { env } from "@homarr/common/env";
import { logger } from "@homarr/log";
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { createChannelWithLatestAndEvents } from "@homarr/redis";
import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler";
import packageJson from "../../../package.json";
const logger = createLogger({ module: "updateCheckerRequestHandler" });
export const updateCheckerRequestHandler = createCachedRequestHandler({
queryKey: "homarr-update-checker",
cacheDuration: dayjs.duration(1, "hour"),
@@ -21,7 +23,7 @@ export const updateCheckerRequestHandler = createCachedRequestHandler({
const octokit = new Octokit({
request: {
fetch: fetchWithTimeout,
fetch: fetchWithTimeoutAsync,
},
});
const releases = await octokit.rest.repos.listReleases({
@@ -34,7 +36,7 @@ export const updateCheckerRequestHandler = createCachedRequestHandler({
for (const release of releases.data) {
if (!isValidSemVer(release.tag_name)) {
logger.warn(`Unable to parse semantic tag '${release.tag_name}'. Update check might not work.`);
logger.warn("Unable to parse semantic tag. Update check might not work.", { tagName: release.tag_name });
continue;
}
@@ -46,11 +48,12 @@ export const updateCheckerRequestHandler = createCachedRequestHandler({
.sort((releaseA, releaseB) => compareSemVer(releaseB.tag_name, releaseA.tag_name));
if (availableNewerReleases.length > 0) {
logger.info(
"Update checker found a new available version",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
`Update checker found a new available version: ${availableReleases[0]!.tag_name}. Current version is ${currentVersion}`,
{ version: availableReleases[0]!.tag_name, currentVersion },
);
} else {
logger.debug(`Update checker did not find any available updates. Current version is ${currentVersion}`);
logger.debug("Update checker did not find any available updates", { currentVersion });
}
return {
+2 -2
View File
@@ -1,7 +1,7 @@
import dayjs from "dayjs";
import { z } from "zod";
import { fetchWithTimeout } from "@homarr/common";
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
@@ -9,7 +9,7 @@ export const weatherRequestHandler = createCachedWidgetRequestHandler({
queryKey: "weatherAtLocation",
widgetKind: "weather",
async requestAsync(input: { latitude: number; longitude: number }) {
const res = await fetchWithTimeout(
const res = await fetchWithTimeoutAsync(
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max&current_weather=true&timezone=auto`,
);
const json: unknown = await res.json();