Merge branch 'dev' into add-more-openapi

This commit is contained in:
Meier Lukas committed 2025-12-10 15:58:04 +01:00
commit df5704019b
309 files changed
+23031 -8513

No files matched your search

+21 -13
View File
@@ -3,9 +3,9 @@ import type { ContainerInfo, ContainerStats } from "dockerode";
import { db, like, or } from "@homarr/db";
import { icons } from "@homarr/db/schema";
import type { ContainerState } from "@homarr/docker";
import { dockerLabels, DockerSingleton } from "@homarr/docker";
import type { ContainerState } from "../../docker/src";
import { DockerSingleton } from "../../docker/src";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
export const dockerContainersRequestHandler = createCachedWidgetRequestHandler({
@@ -27,13 +27,11 @@ async function getContainersWithStatsAsync() {
const containers = await Promise.all(
dockerInstances.map(async ({ instance, host }) => {
const instanceContainers = await instance.listContainers({ all: true });
return instanceContainers.map((container) => ({
...container,
instance: host,
}));
return instanceContainers
.filter((container) => dockerLabels.hide in container.Labels === false)
.map((container) => ({ ...container, instance: host }));
}),
).then((res) => res.flat());
const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`));
const dbIcons =
@@ -47,7 +45,7 @@ async function getContainersWithStatsAsync() {
const instance = dockerInstances.find(({ host }) => host === container.instance)?.instance;
if (!instance) return null;
const stats = await instance.getContainer(container.Id).stats({ stream: false });
const stats = await instance.getContainer(container.Id).stats({ stream: false, "one-shot": true });
return {
id: container.Id,
@@ -60,7 +58,18 @@ async function getContainersWithStatsAsync() {
return icon.name.toLowerCase().includes(extractedImage.toLowerCase());
})?.url ?? null,
cpuUsage: calculateCpuUsage(stats),
memoryUsage: stats.memory_stats.usage,
// memory usage by default includes cache, which should not be shown as it is also not shown with docker stats command
// The below type is probably wrong, sometimes stats can be undefined
// See https://docs.docker.com/reference/cli/docker/container/stats/ how it is / was calculated
memoryUsage:
stats.memory_stats.usage -
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
(stats.memory_stats.stats?.cache ??
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
stats.memory_stats.stats?.total_inactive_file ??
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
stats.memory_stats.stats?.inactive_file ??
0),
image: container.Image,
ports: container.Ports,
};
@@ -70,11 +79,10 @@ async function getContainersWithStatsAsync() {
}
function calculateCpuUsage(stats: ContainerStats): number {
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
const numberOfCpus = stats.cpu_stats.online_cpus || 1;
if (systemDelta === 0) return 0;
const usage = stats.cpu_stats.system_cpu_usage;
if (usage === 0) return 0;
return (cpuDelta / systemDelta) * numberOfCpus * 100;
return (stats.cpu_stats.cpu_usage.total_usage / usage) * numberOfCpus * 100;
}
@@ -10,6 +10,7 @@ import { createCachedRequestHandler } from "./cached-request-handler";
type IntegrationOfKind<TKind extends IntegrationKind> = Omit<Integration, "kind"> & {
kind: TKind;
decryptedSecrets: Modify<Pick<IntegrationSecret, "kind" | "value">, { value: string }>[];
externalUrl: string | null;
};
interface Options<TData, TKind extends IntegrationKind, TInput extends Record<string, unknown>> {
@@ -91,6 +91,7 @@ export const createRequestIntegrationJobHandler = <
...integration,
kind: integration.kind as TIntegrationKind,
decryptedSecrets,
externalUrl: integration.app?.href ?? null,
},
input,
);
@@ -7,7 +7,7 @@ import type { TdarrQueue, TdarrStatistics, TdarrWorker } from "@homarr/integrati
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const mediaTranscodingRequestHandler = createCachedIntegrationRequestHandler<
{ queue: TdarrQueue; workers: TdarrWorker[]; statistics: TdarrStatistics },
MediaTranscoding,
IntegrationKindByCategory<"mediaTranscoding">,
{ pageOffset: number; pageSize: number }
>({
@@ -22,3 +22,9 @@ export const mediaTranscodingRequestHandler = createCachedIntegrationRequestHand
};
},
});
export interface MediaTranscoding {
queue: TdarrQueue;
workers: TdarrWorker[];
statistics: TdarrStatistics;
}
+6 -23
View File
@@ -1,36 +1,19 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { getIconUrl } from "@homarr/definitions";
import type { ReleasesResponse } from "@homarr/integrations";
import type { ReleaseResponse, ReleasesRepository } from "@homarr/integrations";
import { createIntegrationAsync } from "@homarr/integrations";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const releasesRequestHandler = createCachedIntegrationRequestHandler<
ReleasesResponse,
ReleaseResponse,
IntegrationKindByCategory<"releasesProvider">,
{
id: string;
identifier: string;
versionRegex?: string;
}
ReleasesRepository
>({
async requestAsync(integration, input) {
const integrationInstance = await createIntegrationAsync(integration);
const response = await integrationInstance.getLatestMatchingReleaseAsync({
id: input.id,
identifier: input.identifier,
versionRegex: input.versionRegex,
});
return {
...response,
integration: {
name: integration.name,
iconUrl: getIconUrl(integration.kind),
},
};
requestAsync: async (integration, input) => {
const instance = await createIntegrationAsync(integration);
return instance.getLatestMatchingReleaseAsync(input.identifier, input.versionRegex);
},
cacheDuration: dayjs.duration(5, "minutes"),
queryKey: "repositoriesReleases",
+12 -4
View File
@@ -20,11 +20,19 @@ export const fetchStockPriceHandler = createCachedWidgetRequestHandler({
if (data.chart.result.length !== 1) {
throw new Error("Received multiple results");
}
if (!data.chart.result[0]) {
const firstResult = data.chart.result[0];
if (!firstResult) {
throw new Error("Received invalid data");
}
return data.chart.result[0];
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,
) ?? [],
symbol: firstResult.meta.symbol,
shortName: firstResult.meta.shortName,
};
},
cacheDuration: dayjs.duration(5, "minutes"),
});
@@ -43,7 +51,7 @@ const dataSchema = z
indicators: z.object({
quote: z.array(
z.object({
close: z.array(z.number()),
close: z.array(z.number().nullish()),
}),
),
}),
+70
View File
@@ -0,0 +1,70 @@
import dayjs from "dayjs";
import { z } from "zod";
import { fetchWithTimeout } from "@homarr/common";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
export const weatherRequestHandler = createCachedWidgetRequestHandler({
queryKey: "weatherAtLocation",
widgetKind: "weather",
async requestAsync(input: { latitude: number; longitude: number }) {
const res = await fetchWithTimeout(
`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();
const weather = await atLocationOutput.parseAsync(json);
return {
current: weather.current_weather,
daily: weather.daily.time.map((value, index) => {
return {
time: value,
weatherCode: weather.daily.weathercode[index] ?? 404,
maxTemp: weather.daily.temperature_2m_max[index],
minTemp: weather.daily.temperature_2m_min[index],
sunrise: weather.daily.sunrise[index],
sunset: weather.daily.sunset[index],
maxWindSpeed: weather.daily.wind_speed_10m_max[index],
maxWindGusts: weather.daily.wind_gusts_10m_max[index],
};
}),
} satisfies Weather;
},
cacheDuration: dayjs.duration(1, "minute"),
});
const atLocationOutput = z.object({
current_weather: z.object({
weathercode: z.number(),
temperature: z.number(),
windspeed: z.number(),
}),
daily: z.object({
time: z.array(z.string()),
weathercode: z.array(z.number()),
temperature_2m_max: z.array(z.number()),
temperature_2m_min: z.array(z.number()),
sunrise: z.array(z.string()),
sunset: z.array(z.string()),
wind_speed_10m_max: z.array(z.number()),
wind_gusts_10m_max: z.array(z.number()),
}),
});
export interface Weather {
current: {
weathercode: number;
temperature: number;
windspeed: number;
};
daily: {
time: string;
weatherCode: number;
maxTemp: number | undefined;
minTemp: number | undefined;
sunrise: string | undefined;
sunset: string | undefined;
maxWindSpeed: number | undefined;
maxWindGusts: number | undefined;
}[];
}