Merge branch 'dev' into add-more-openapi
This commit is contained in:
309 files changed
+23031
-8513
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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()),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
|
||||
@@ -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¤t_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;
|
||||
}[];
|
||||
}
|
||||
Reference in new issue
Block a user