mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-27 00:40:58 +01:00
chore(release): automatic release v1.39.0
This commit is contained in:
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -33,6 +33,7 @@ body:
|
||||
options:
|
||||
# The below comment is used to insert a new version with on-release.yml
|
||||
#NEXT_VERSION#
|
||||
- 1.38.0
|
||||
- 1.37.0
|
||||
- 1.36.1
|
||||
- 1.36.0
|
||||
|
||||
@@ -57,10 +57,10 @@
|
||||
"@mantine/modals": "^8.3.1",
|
||||
"@mantine/tiptap": "^8.3.1",
|
||||
"@million/lint": "1.0.14",
|
||||
"@tabler/icons-react": "^3.34.1",
|
||||
"@tanstack/react-query": "^5.87.4",
|
||||
"@tanstack/react-query-devtools": "^5.87.4",
|
||||
"@tanstack/react-query-next-experimental": "^5.87.4",
|
||||
"@tabler/icons-react": "^3.35.0",
|
||||
"@tanstack/react-query": "^5.89.0",
|
||||
"@tanstack/react-query-devtools": "^5.89.0",
|
||||
"@tanstack/react-query-next-experimental": "^5.89.0",
|
||||
"@trpc/client": "^11.5.1",
|
||||
"@trpc/next": "^11.5.1",
|
||||
"@trpc/react-query": "^11.5.1",
|
||||
@@ -83,24 +83,24 @@
|
||||
"react-dom": "19.1.1",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"sass": "^1.92.1",
|
||||
"sass": "^1.93.0",
|
||||
"superjson": "2.2.2",
|
||||
"swagger-ui-react": "^5.29.0",
|
||||
"use-deep-compare-effect": "^1.8.1",
|
||||
"zod": "^4.1.8"
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/chroma-js": "3.1.1",
|
||||
"@types/node": "^22.18.3",
|
||||
"@types/node": "^22.18.6",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "19.1.13",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"node-loader": "^2.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
|
||||
@@ -47,10 +47,10 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node": "^22.18.3",
|
||||
"@types/node": "^22.18.6",
|
||||
"dotenv-cli": "^10.0.0",
|
||||
"esbuild": "^0.25.9",
|
||||
"eslint": "^9.35.0",
|
||||
"esbuild": "^0.25.10",
|
||||
"eslint": "^9.36.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tsx": "4.20.4",
|
||||
"typescript": "^5.9.2"
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"esbuild": "^0.25.9",
|
||||
"eslint": "^9.35.0",
|
||||
"esbuild": "^0.25.10",
|
||||
"eslint": "^9.36.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
|
||||
22
package.json
22
package.json
@@ -42,23 +42,23 @@
|
||||
"@semantic-release/github": "^11.0.6",
|
||||
"@semantic-release/npm": "^12.0.2",
|
||||
"@semantic-release/release-notes-generator": "^14.1.0",
|
||||
"@testcontainers/redis": "^11.5.1",
|
||||
"@testcontainers/redis": "^11.6.0",
|
||||
"@turbo/gen": "^2.5.6",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@vitejs/plugin-react": "^5.0.3",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
||||
"cross-env": "^10.0.0",
|
||||
"jsdom": "^27.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"semantic-release": "^24.2.8",
|
||||
"testcontainers": "^11.5.1",
|
||||
"semantic-release": "^24.2.9",
|
||||
"testcontainers": "^11.6.0",
|
||||
"turbo": "^2.5.6",
|
||||
"typescript": "^5.9.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"packageManager": "pnpm@10.16.1",
|
||||
"packageManager": "pnpm@10.17.0",
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
},
|
||||
@@ -80,20 +80,20 @@
|
||||
"axios@>=1.0.0 <1.8.2": ">=1.12.2",
|
||||
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
|
||||
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
|
||||
"esbuild@<=0.24.2": ">=0.25.9",
|
||||
"esbuild@<=0.24.2": ">=0.25.10",
|
||||
"form-data@>=4.0.0 <4.0.4": ">=4.0.4",
|
||||
"hono@<4.6.5": ">=4.9.7",
|
||||
"hono@<4.6.5": ">=4.9.8",
|
||||
"linkifyjs@<4.3.2": ">=4.3.2",
|
||||
"nanoid@>=4.0.0 <5.0.9": ">=5.1.5",
|
||||
"prismjs@<1.30.0": ">=1.30.0",
|
||||
"proxmox-api>undici": "7.16.0",
|
||||
"react-is": "^19.1.1",
|
||||
"rollup@>=4.0.0 <4.22.4": ">=4.50.1",
|
||||
"rollup@>=4.0.0 <4.22.4": ">=4.52.2",
|
||||
"sha.js@<=2.4.11": ">=2.4.12",
|
||||
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.0",
|
||||
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.0",
|
||||
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
|
||||
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
|
||||
"tmp@<=0.2.3": ">=0.2.5",
|
||||
"vite@>=5.0.0 <=5.4.18": ">=7.1.5"
|
||||
"vite@>=5.0.0 <=5.4.18": ">=7.1.7"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@types/node-unifi": "patches/@types__node-unifi.patch",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@kubernetes/client-node": "^1.3.0",
|
||||
"@tanstack/react-query": "^5.87.4",
|
||||
"@tanstack/react-query": "^5.89.0",
|
||||
"@trpc/client": "^11.5.1",
|
||||
"@trpc/react-query": "^11.5.1",
|
||||
"@trpc/server": "^11.5.1",
|
||||
@@ -53,13 +53,13 @@
|
||||
"react-dom": "19.1.1",
|
||||
"superjson": "2.2.2",
|
||||
"trpc-to-openapi": "^3.0.1",
|
||||
"zod": "^4.1.8"
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import type { Modify } from "@homarr/common/types";
|
||||
import type { Integration } from "@homarr/db/schema";
|
||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import type { CalendarEvent } from "@homarr/integrations/types";
|
||||
import { radarrReleaseTypes } from "@homarr/integrations/types";
|
||||
import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar";
|
||||
|
||||
@@ -19,14 +24,56 @@ export const calendarRouter = createTRPCRouter({
|
||||
)
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar")))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const results = await Promise.all(
|
||||
return await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
const innerHandler = calendarMonthRequestHandler.handler(integration, input);
|
||||
const { integrationIds: _integrationIds, ...handlerInput } = input;
|
||||
const innerHandler = calendarMonthRequestHandler.handler(integration, handlerInput);
|
||||
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
|
||||
return data;
|
||||
return {
|
||||
events: data,
|
||||
integration: {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
return results.flat();
|
||||
}),
|
||||
subscribeToEvents: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
year: z.number(),
|
||||
month: z.number(),
|
||||
releaseType: z.array(z.enum(radarrReleaseTypes)),
|
||||
showUnmonitored: z.boolean(),
|
||||
}),
|
||||
)
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar")))
|
||||
.subscription(({ ctx, input }) => {
|
||||
return observable<{
|
||||
integration: Modify<Integration, { kind: IntegrationKindByCategory<"calendar"> }>;
|
||||
events: CalendarEvent[];
|
||||
}>((emit) => {
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
for (const integrationWithSecrets of ctx.integrations) {
|
||||
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
|
||||
const { integrationIds: _integrationIds, ...handlerInput } = input;
|
||||
const innerHandler = calendarMonthRequestHandler.handler(integrationWithSecrets, handlerInput);
|
||||
const unsubscribe = innerHandler.subscribe((events) => {
|
||||
emit.next({
|
||||
integration,
|
||||
events,
|
||||
});
|
||||
});
|
||||
unsubscribes.push(unsubscribe);
|
||||
}
|
||||
return () => {
|
||||
unsubscribes.forEach((unsubscribe) => {
|
||||
unsubscribe();
|
||||
});
|
||||
};
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { fetchWithTimeout } from "@homarr/common";
|
||||
import type { Weather } from "@homarr/request-handler/weather";
|
||||
import { weatherRequestHandler } from "@homarr/request-handler/weather";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
@@ -9,45 +11,19 @@ const atLocationInput = z.object({
|
||||
latitude: z.number(),
|
||||
});
|
||||
|
||||
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 const weatherRouter = createTRPCRouter({
|
||||
atLocation: publicProcedure.input(atLocationInput).query(async ({ input }) => {
|
||||
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],
|
||||
};
|
||||
}),
|
||||
};
|
||||
const handler = weatherRequestHandler.handler(input);
|
||||
return await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false }).then((result) => result.data);
|
||||
}),
|
||||
subscribeAtLocation: publicProcedure.input(atLocationInput).subscription(({ input }) => {
|
||||
return observable<Weather>((emit) => {
|
||||
const handler = weatherRequestHandler.handler(input);
|
||||
const unsubscribe = handler.subscribe((data) => {
|
||||
emit.next(data);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"next-auth": "5.0.0-beta.29",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"zod": "^4.1.8"
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
@@ -47,7 +47,7 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/bcrypt": "6.0.0",
|
||||
"@types/cookies": "0.9.1",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"esbuild": "^0.25.9",
|
||||
"eslint": "^9.35.0",
|
||||
"esbuild": "^0.25.10",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,20 +31,20 @@
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"dayjs": "^1.11.18",
|
||||
"dns-caching": "^0.2.5",
|
||||
"dns-caching": "^0.2.7",
|
||||
"next": "15.5.3",
|
||||
"octokit": "^5.0.3",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"undici": "7.16.0",
|
||||
"zod": "^4.1.8",
|
||||
"zod-validation-error": "^4.0.1"
|
||||
"zod": "^4.1.11",
|
||||
"zod-validation-error": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,17 @@ const calculateTimeAgo = (timestamp: Date) => {
|
||||
};
|
||||
|
||||
export const useTimeAgo = (timestamp: Date, updateFrequency = 1000) => {
|
||||
const [timeAgo, setTimeAgo] = useState(calculateTimeAgo(timestamp));
|
||||
const [timeAgo, setTimeAgo] = useState(() => calculateTimeAgo(timestamp));
|
||||
|
||||
useEffect(() => {
|
||||
setTimeAgo(calculateTimeAgo(timestamp));
|
||||
}, [timestamp]);
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => setTimeAgo(calculateTimeAgo(timestamp)), updateFrequency);
|
||||
|
||||
return () => clearInterval(intervalId); // clear interval on hook unmount
|
||||
}, [timestamp]);
|
||||
}, [timestamp, updateFrequency]);
|
||||
|
||||
return timeAgo;
|
||||
};
|
||||
|
||||
@@ -26,13 +26,13 @@
|
||||
"dependencies": {
|
||||
"@t3-oss/env-nextjs": "^0.13.8",
|
||||
"ioredis": "5.7.0",
|
||||
"zod": "^4.1.8"
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
"@homarr/core": "workspace:^0.1.0",
|
||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@tanstack/react-query": "^5.87.4",
|
||||
"@tanstack/react-query": "^5.89.0",
|
||||
"@trpc/client": "^11.5.1",
|
||||
"@trpc/server": "^11.5.1",
|
||||
"@trpc/tanstack-react-query": "^11.5.1",
|
||||
"node-cron": "^4.2.1",
|
||||
"react": "19.1.1",
|
||||
"zod": "^4.1.8"
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
@@ -43,7 +43,7 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/react": "19.1.13",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
|
||||
import { pingJob } from "./jobs/ping";
|
||||
import { rssFeedsJob } from "./jobs/rss-feeds";
|
||||
import { updateCheckerJob } from "./jobs/update-checker";
|
||||
import { weatherJob } from "./jobs/weather";
|
||||
import { createCronJobGroup } from "./lib";
|
||||
|
||||
export const jobGroup = createCronJobGroup({
|
||||
@@ -48,6 +49,7 @@ export const jobGroup = createCronJobGroup({
|
||||
firewallVersion: firewallVersionJob,
|
||||
firewallInterfaces: firewallInterfacesJob,
|
||||
refreshNotifications: refreshNotificationsJob,
|
||||
weather: weatherJob,
|
||||
});
|
||||
|
||||
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
|
||||
|
||||
33
packages/cron-jobs/src/jobs/weather.ts
Normal file
33
packages/cron-jobs/src/jobs/weather.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions";
|
||||
import { db, eq } from "@homarr/db";
|
||||
import { items } from "@homarr/db/schema";
|
||||
import { logger } from "@homarr/log";
|
||||
import { weatherRequestHandler } from "@homarr/request-handler/weather";
|
||||
|
||||
import type { WidgetComponentProps } from "../../../widgets";
|
||||
import { createCronJob } from "../lib";
|
||||
|
||||
export const weatherJob = createCronJob("weather", EVERY_10_MINUTES).withCallback(async () => {
|
||||
const weatherItems = await db.query.items.findMany({
|
||||
where: eq(items.kind, "weather"),
|
||||
});
|
||||
|
||||
const parsedItems = weatherItems.map((item) => ({
|
||||
id: item.id,
|
||||
options: SuperJSON.parse<WidgetComponentProps<"weather">["options"]>(item.options),
|
||||
}));
|
||||
|
||||
for (const item of parsedItems) {
|
||||
try {
|
||||
const innerHandler = weatherRequestHandler.handler({
|
||||
longitude: item.options.location.longitude,
|
||||
latitude: item.options.location.latitude,
|
||||
});
|
||||
await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
|
||||
} catch (error) {
|
||||
logger.error("Failed to update weather", { id: item.id, error });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -51,14 +51,14 @@
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.3.1",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@testcontainers/mysql": "^11.5.1",
|
||||
"@testcontainers/postgresql": "^11.5.1",
|
||||
"@testcontainers/mysql": "^11.6.0",
|
||||
"@testcontainers/postgresql": "^11.6.0",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"dotenv": "^17.2.2",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"drizzle-zod": "^0.8.3",
|
||||
"mysql2": "3.14.5",
|
||||
"mysql2": "3.15.0",
|
||||
"pg": "^8.16.3",
|
||||
"superjson": "2.2.2"
|
||||
},
|
||||
@@ -69,8 +69,8 @@
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"@types/pg": "^8.15.5",
|
||||
"dotenv-cli": "^10.0.0",
|
||||
"esbuild": "^0.25.9",
|
||||
"eslint": "^9.35.0",
|
||||
"esbuild": "^0.25.10",
|
||||
"eslint": "^9.36.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tsx": "4.20.4",
|
||||
"typescript": "^5.9.2"
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"fast-xml-parser": "^5.2.5",
|
||||
"zod": "^4.1.8"
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"tsx": "4.20.4",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/tags/programming"
|
||||
| "/docs/tags/proxy"
|
||||
| "/docs/tags/puid"
|
||||
| "/docs/tags/redis"
|
||||
| "/docs/tags/responsive"
|
||||
| "/docs/tags/roles"
|
||||
| "/docs/tags/search"
|
||||
@@ -159,6 +160,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/integrations/github"
|
||||
| "/docs/integrations/gitlab"
|
||||
| "/docs/integrations/home-assistant"
|
||||
| "/docs/integrations/ical"
|
||||
| "/docs/integrations/jellyfin"
|
||||
| "/docs/integrations/jellyseerr"
|
||||
| "/docs/integrations/kubernetes"
|
||||
|
||||
@@ -161,7 +161,7 @@ export const integrationDefs = {
|
||||
name: "Home Assistant",
|
||||
secretKinds: [["apiKey"]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/home-assistant.svg",
|
||||
category: ["smartHomeServer"],
|
||||
category: ["smartHomeServer", "calendar"],
|
||||
documentationUrl: createDocumentationLink("/docs/integrations/home-assistant"),
|
||||
},
|
||||
openmediavault: {
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/dockerode": "^3.3.43",
|
||||
"eslint": "^9.35.0",
|
||||
"@types/dockerode": "^3.3.44",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,13 +28,13 @@
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/form": "^8.3.1",
|
||||
"mantine-form-zod-resolver": "^1.3.0",
|
||||
"zod": "^4.1.8"
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,13 +31,13 @@
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.3.1",
|
||||
"react": "19.1.1",
|
||||
"zod": "^4.1.8"
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/bcrypt": "6.0.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@ctrl/deluge": "^7.2.0",
|
||||
"@ctrl/qbittorrent": "^9.7.0",
|
||||
"@ctrl/transmission": "^7.3.0",
|
||||
"@ctrl/deluge": "^7.3.0",
|
||||
"@ctrl/qbittorrent": "^9.8.0",
|
||||
"@ctrl/transmission": "^7.4.0",
|
||||
"@gitbeaker/rest": "^43.5.0",
|
||||
"@homarr/certificates": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
@@ -43,13 +43,13 @@
|
||||
"@octokit/auth-app": "^8.1.0",
|
||||
"ical.js": "^2.2.1",
|
||||
"maria2": "^0.4.1",
|
||||
"node-ical": "^0.20.1",
|
||||
"node-ical": "^0.21.0",
|
||||
"octokit": "^5.0.3",
|
||||
"proxmox-api": "1.1.1",
|
||||
"tsdav": "^2.1.5",
|
||||
"undici": "7.16.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"zod": "^4.1.8"
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
@@ -57,7 +57,7 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-unifi": "^2.5.1",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import z from "zod";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { ResponseError } from "@homarr/common/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { ICalendarIntegration } from "../interfaces/calendar/calendar-integration";
|
||||
import type { ISmartHomeIntegration } from "../interfaces/smart-home/smart-home-integration";
|
||||
import { entityStateSchema } from "./homeassistant-types";
|
||||
import type { CalendarEvent } from "../types";
|
||||
import { calendarEventSchema, calendarsSchema, entityStateSchema } from "./homeassistant-types";
|
||||
|
||||
export class HomeAssistantIntegration extends Integration implements ISmartHomeIntegration {
|
||||
export class HomeAssistantIntegration extends Integration implements ISmartHomeIntegration, ICalendarIntegration {
|
||||
public async getEntityStateAsync(entityId: string) {
|
||||
try {
|
||||
const response = await this.getAsync(`/api/states/${entityId}`);
|
||||
@@ -62,6 +67,35 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI
|
||||
}
|
||||
}
|
||||
|
||||
public async getCalendarEventsAsync(start: Date, end: Date): Promise<CalendarEvent[]> {
|
||||
const calendarsResponse = await this.getAsync("/api/calendars");
|
||||
if (!calendarsResponse.ok) throw new ResponseError(calendarsResponse);
|
||||
const calendars = await calendarsSchema.parseAsync(await calendarsResponse.json());
|
||||
|
||||
return await Promise.all(
|
||||
calendars.map(async (calendar) => {
|
||||
const response = await this.getAsync(`/api/calendars/${calendar.entity_id}`, { start, end });
|
||||
if (!response.ok) throw new ResponseError(response);
|
||||
return await z.array(calendarEventSchema).parseAsync(await response.json());
|
||||
}),
|
||||
).then((events) =>
|
||||
events.flat().map(
|
||||
(event): CalendarEvent => ({
|
||||
title: event.summary,
|
||||
subTitle: null,
|
||||
description: event.description,
|
||||
// If not reseting it to 0 o'clock it uses utc time and therefore shows as 2 o'clock
|
||||
startDate: "date" in event.start ? new Date(`${event.start.date}T00:00:00`) : new Date(event.start.dateTime),
|
||||
endDate: "date" in event.end ? new Date(`${event.end.date}T00:00:00`) : new Date(event.end.dateTime),
|
||||
image: null,
|
||||
indicatorColor: "#18bcf2",
|
||||
links: [],
|
||||
location: event.location,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const response = await input.fetchAsync(this.url("/api/config"), {
|
||||
headers: this.getAuthHeaders(),
|
||||
@@ -82,8 +116,8 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI
|
||||
* @param path full path to the API endpoint
|
||||
* @returns the response from the API
|
||||
*/
|
||||
private async getAsync(path: `/api/${string}`) {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url(path), {
|
||||
private async getAsync(path: `/api/${string}`, queryParams?: Record<string, string | Date | number | boolean>) {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url(path, queryParams), {
|
||||
headers: this.getAuthHeaders(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,3 +12,27 @@ export const entityStateSchema = z.object({
|
||||
});
|
||||
|
||||
export type EntityState = z.infer<typeof entityStateSchema>;
|
||||
|
||||
export const calendarsSchema = z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
entity_id: z.string(),
|
||||
}),
|
||||
);
|
||||
|
||||
const calendarMomentSchema = z
|
||||
.object({
|
||||
date: z.string(),
|
||||
})
|
||||
.or(
|
||||
z.object({
|
||||
dateTime: z.string(),
|
||||
}),
|
||||
);
|
||||
export const calendarEventSchema = z.object({
|
||||
start: calendarMomentSchema,
|
||||
end: calendarMomentSchema,
|
||||
summary: z.string(),
|
||||
description: z.string().nullable(),
|
||||
location: z.string().nullable(),
|
||||
});
|
||||
|
||||
@@ -47,6 +47,10 @@ export class SonarrIntegration extends Integration implements ICalendarIntegrati
|
||||
? {
|
||||
src: imageSrc,
|
||||
aspectRatio: { width: 7, height: 12 },
|
||||
badge: {
|
||||
color: "red",
|
||||
content: `S${event.seasonNumber}/E${event.episodeNumber}`,
|
||||
},
|
||||
}
|
||||
: null,
|
||||
location: null,
|
||||
|
||||
@@ -69,7 +69,7 @@ const seriesRelease = (start: Date, end: Date): CalendarEvent => ({
|
||||
src: "https://image.tmdb.org/t/p/original/sWgBv7LV2PRoQgkxwlibdGXKz1S.jpg",
|
||||
aspectRatio: { width: 7, height: 12 },
|
||||
badge: {
|
||||
content: "S1:E1",
|
||||
content: "S1/E1",
|
||||
color: "red",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
"@homarr/core": "workspace:^0.1.0",
|
||||
"superjson": "2.2.2",
|
||||
"winston": "3.17.0",
|
||||
"zod": "^4.1.8"
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,18 +34,18 @@
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.3.1",
|
||||
"@tabler/icons-react": "^3.34.1",
|
||||
"@tabler/icons-react": "^3.35.0",
|
||||
"dayjs": "^1.11.18",
|
||||
"next": "15.5.3",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"zod": "^4.1.8"
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
"dependencies": {
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/notifications": "^8.3.1",
|
||||
"@tabler/icons-react": "^3.34.1"
|
||||
"@tabler/icons-react": "^3.35.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"superjson": "2.2.2",
|
||||
"zod": "^4.1.8",
|
||||
"zod": "^4.1.11",
|
||||
"zod-form-data": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -52,7 +52,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/adm-zip": "0.5.7",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"zod": "^4.1.8"
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,13 +32,14 @@
|
||||
"dayjs": "^1.11.18",
|
||||
"octokit": "^5.0.3",
|
||||
"superjson": "2.2.2",
|
||||
"undici": "7.16.0"
|
||||
"undici": "7.16.0",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
70
packages/request-handler/src/weather.ts
Normal file
70
packages/request-handler/src/weather.ts
Normal 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¤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;
|
||||
}[];
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@mantine/core": "^8.3.1",
|
||||
"@mantine/hooks": "^8.3.1",
|
||||
"@mantine/spotlight": "^8.3.1",
|
||||
"@tabler/icons-react": "^3.34.1",
|
||||
"@tabler/icons-react": "^3.35.0",
|
||||
"jotai": "^2.14.0",
|
||||
"next": "15.5.3",
|
||||
"react": "19.1.1",
|
||||
@@ -47,7 +47,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"deepmerge": "4.3.1",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "15.5.3",
|
||||
"next-intl": "4.3.8",
|
||||
"next-intl": "4.3.9",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1"
|
||||
},
|
||||
@@ -41,7 +41,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1707,6 +1707,9 @@
|
||||
"calendar": {
|
||||
"name": "Calendar",
|
||||
"description": "Display events from your integrations in a calendar view within a certain relative time period",
|
||||
"duration": {
|
||||
"allDay": "All day"
|
||||
},
|
||||
"option": {
|
||||
"releaseType": {
|
||||
"label": "Radarr release type",
|
||||
@@ -3315,6 +3318,9 @@
|
||||
},
|
||||
"firewallInterfaces": {
|
||||
"label": "Firewall Interfaces"
|
||||
},
|
||||
"weather": {
|
||||
"label": "Weather"
|
||||
}
|
||||
},
|
||||
"interval": {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@mantine/core": "^8.3.1",
|
||||
"@mantine/dates": "^8.3.1",
|
||||
"@mantine/hooks": "^8.3.1",
|
||||
"@tabler/icons-react": "^3.34.1",
|
||||
"@tabler/icons-react": "^3.35.0",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "15.5.3",
|
||||
"react": "19.1.1",
|
||||
@@ -45,7 +45,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/css-modules": "^1.0.5",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
"dependencies": {
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"zod": "^4.1.8",
|
||||
"zod": "^4.1.11",
|
||||
"zod-form-data": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"@mantine/charts": "^8.3.1",
|
||||
"@mantine/core": "^8.3.1",
|
||||
"@mantine/hooks": "^8.3.1",
|
||||
"@tabler/icons-react": "^3.34.1",
|
||||
"@tabler/icons-react": "^3.35.0",
|
||||
"@tiptap/extension-color": "2.26.1",
|
||||
"@tiptap/extension-highlight": "2.26.1",
|
||||
"@tiptap/extension-image": "2.26.1",
|
||||
@@ -78,14 +78,14 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"recharts": "^2.15.4",
|
||||
"video.js": "^8.23.4",
|
||||
"zod": "^4.1.8"
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/video.js": "^7.3.58",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,16 +84,24 @@ export const CalendarEventList = ({ events }: CalendarEventListProps) => {
|
||||
|
||||
<Group gap={3} wrap="nowrap" align={"center"}>
|
||||
<IconClock opacity={0.7} size={"1rem"} />
|
||||
<Text c={"dimmed"} size={"sm"}>
|
||||
{dayjs(event.startDate).format("HH:mm")}
|
||||
</Text>
|
||||
|
||||
{event.endDate !== null && (
|
||||
{isAllDay(event) ? (
|
||||
<Text c={"dimmed"} size={"sm"}>
|
||||
{t("widget.calendar.duration.allDay")}
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
-{" "}
|
||||
<Text c={"dimmed"} size={"sm"}>
|
||||
{dayjs(event.endDate).format("HH:mm")}
|
||||
{dayjs(event.startDate).format("HH:mm")}
|
||||
</Text>
|
||||
|
||||
{event.endDate !== null && (
|
||||
<>
|
||||
-{" "}
|
||||
<Text c={"dimmed"} size={"sm"}>
|
||||
{dayjs(event.endDate).format("HH:mm")}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
@@ -152,3 +160,12 @@ export const CalendarEventList = ({ events }: CalendarEventListProps) => {
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
const isAllDay = (event: Pick<CalendarEvent, "startDate" | "endDate">) => {
|
||||
if (!event.endDate) return false;
|
||||
|
||||
const start = dayjs(event.startDate);
|
||||
const end = dayjs(event.endDate);
|
||||
|
||||
return start.startOf("day").isSame(start) && end.endOf("day").isSame(end);
|
||||
};
|
||||
|
||||
66
packages/widgets/src/calendar/calendar.spec.ts
Normal file
66
packages/widgets/src/calendar/calendar.spec.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import type { CalendarEvent } from "@homarr/integrations/types";
|
||||
|
||||
import { splitEvents } from "./component";
|
||||
|
||||
describe("splitEvents should split multi-day events into multiple single-day events", () => {
|
||||
test("2 day all-day event should be split up into two all-day events", () => {
|
||||
const event = createEvent(new Date(2025, 0, 1), new Date(2025, 0, 3));
|
||||
|
||||
const result = splitEvents([event]);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]?.startDate).toEqual(event.startDate);
|
||||
expect(result[0]?.endDate).toEqual(new Date(new Date(2025, 0, 2).getTime() - 1));
|
||||
expect(result[1]?.startDate).toEqual(new Date(2025, 0, 2));
|
||||
// Because we want to end the event on the previous day, we have not the same endDate.
|
||||
// Otherwise there would be three single-day events, with the last being from 0:00 - 0:00
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
expect(result[1]?.endDate).toEqual(new Date(event.endDate!.getTime() - 1));
|
||||
});
|
||||
test("2 day partial event should be split up into two events", () => {
|
||||
const event = createEvent(new Date(2025, 0, 1, 15), new Date(2025, 0, 2, 9));
|
||||
|
||||
const result = splitEvents([event]);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]?.startDate).toEqual(event.startDate);
|
||||
expect(result[0]?.endDate).toEqual(new Date(new Date(2025, 0, 2).getTime() - 1));
|
||||
expect(result[1]?.startDate).toEqual(new Date(2025, 0, 2));
|
||||
expect(result[1]?.endDate).toEqual(event.endDate);
|
||||
});
|
||||
test("one day partial event should only have one event after split", () => {
|
||||
const event = createEvent(new Date(2025, 0, 1), new Date(2025, 0, 2));
|
||||
|
||||
const result = splitEvents([event]);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
test("without endDate should not be split", () => {
|
||||
const event = createEvent(new Date(2025, 0, 1));
|
||||
|
||||
const result = splitEvents([event]);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
test("startDate after endDate should not cause infinite loop", () => {
|
||||
const event = createEvent(new Date(2025, 0, 2), new Date(2025, 0, 1));
|
||||
|
||||
const result = splitEvents([event]);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
const createEvent = (startDate: Date, endDate: Date | null = null): CalendarEvent => ({
|
||||
title: "Test",
|
||||
subTitle: null,
|
||||
description: null,
|
||||
startDate,
|
||||
endDate,
|
||||
image: null,
|
||||
indicatorColor: "red",
|
||||
links: [],
|
||||
location: null,
|
||||
});
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useMantineTheme } from "@mantine/core";
|
||||
import { Calendar } from "@mantine/dates";
|
||||
import { useElementSize } from "@mantine/hooks";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import type { CalendarEvent } from "@homarr/integrations/types";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
@@ -32,28 +32,43 @@ interface FetchCalendarProps extends WidgetComponentProps<"calendar"> {
|
||||
}
|
||||
|
||||
const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }: FetchCalendarProps) => {
|
||||
const [events] = clientApi.widget.calendar.findAllEvents.useSuspenseQuery(
|
||||
{
|
||||
integrationIds,
|
||||
month: month.getMonth(),
|
||||
year: month.getFullYear(),
|
||||
releaseType: options.releaseType,
|
||||
showUnmonitored: options.showUnmonitored,
|
||||
const input = {
|
||||
integrationIds,
|
||||
month: month.getMonth(),
|
||||
year: month.getFullYear(),
|
||||
releaseType: options.releaseType,
|
||||
showUnmonitored: options.showUnmonitored,
|
||||
};
|
||||
const [data] = clientApi.widget.calendar.findAllEvents.useSuspenseQuery(input, {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const utils = clientApi.useUtils();
|
||||
clientApi.widget.calendar.subscribeToEvents.useSubscription(input, {
|
||||
onData(data) {
|
||||
utils.widget.calendar.findAllEvents.setData(input, (old) => {
|
||||
return old?.map((item) => {
|
||||
if (item.integration.id !== data.integration.id) return item;
|
||||
return {
|
||||
...item,
|
||||
events: data.events,
|
||||
};
|
||||
});
|
||||
});
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const events = useMemo(() => data.flatMap((item) => item.events), [data]);
|
||||
|
||||
return <CalendarBase isEditMode={isEditMode} events={events} month={month} setMonth={setMonth} options={options} />;
|
||||
};
|
||||
|
||||
interface CalendarBaseProps {
|
||||
isEditMode: boolean;
|
||||
events: RouterOutputs["widget"]["calendar"]["findAllEvents"];
|
||||
events: CalendarEvent[];
|
||||
month: Date;
|
||||
setMonth: (date: Date) => void;
|
||||
options: WidgetComponentProps<"calendar">["options"];
|
||||
@@ -69,6 +84,8 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
|
||||
const { ref, width, height } = useElementSize();
|
||||
const isSmall = width < 256;
|
||||
|
||||
const normalizedEvents = useMemo(() => splitEvents(events), [events]);
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
defaultDate={new Date()}
|
||||
@@ -122,7 +139,7 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
|
||||
},
|
||||
}}
|
||||
renderDay={(tileDate) => {
|
||||
const eventsForDate = events
|
||||
const eventsForDate = normalizedEvents
|
||||
.filter((event) => dayjs(event.startDate).isSame(tileDate, "day"))
|
||||
.filter(
|
||||
(event) => event.metadata?.type !== "radarr" || options.releaseType.includes(event.metadata.releaseType),
|
||||
@@ -145,3 +162,42 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits multi-day events into multiple single-day events.
|
||||
* @param events The events to split.
|
||||
* @returns The split events.
|
||||
*/
|
||||
export const splitEvents = (events: CalendarEvent[]): CalendarEvent[] => {
|
||||
const splitEvents: CalendarEvent[] = [];
|
||||
for (const event of events) {
|
||||
if (!event.endDate) {
|
||||
splitEvents.push(event);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dayjs(event.startDate).isSame(event.endDate, "day")) {
|
||||
splitEvents.push(event);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dayjs(event.startDate).isAfter(event.endDate)) {
|
||||
// Invalid event, skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
// Event spans multiple days, split it
|
||||
let currentStart = dayjs(event.startDate);
|
||||
|
||||
while (currentStart.isBefore(event.endDate)) {
|
||||
splitEvents.push({
|
||||
...event,
|
||||
startDate: currentStart.toDate(),
|
||||
endDate: currentStart.endOf("day").isAfter(event.endDate) ? event.endDate : currentStart.endOf("day").toDate(),
|
||||
});
|
||||
|
||||
currentStart = currentStart.add(1, "day").startOf("day");
|
||||
}
|
||||
}
|
||||
return splitEvents;
|
||||
};
|
||||
|
||||
@@ -13,17 +13,20 @@ import type { WidgetComponentProps } from "../definition";
|
||||
import { WeatherDescription, WeatherIcon } from "./icon";
|
||||
|
||||
export default function WeatherWidget({ isEditMode, options }: WidgetComponentProps<"weather">) {
|
||||
const [weather] = clientApi.widget.weather.atLocation.useSuspenseQuery(
|
||||
{
|
||||
latitude: options.location.latitude,
|
||||
longitude: options.location.longitude,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
},
|
||||
);
|
||||
const input = {
|
||||
latitude: options.location.latitude,
|
||||
longitude: options.location.longitude,
|
||||
};
|
||||
const [weather] = clientApi.widget.weather.atLocation.useSuspenseQuery(input, {
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
const utils = clientApi.useUtils();
|
||||
clientApi.widget.weather.subscribeAtLocation.useSubscription(input, {
|
||||
onData: (data) => utils.widget.weather.atLocation.setData(input, data),
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack
|
||||
|
||||
1450
pnpm-lock.yaml
generated
1450
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -24,12 +24,12 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"typescript-eslint": "^8.43.0"
|
||||
"typescript-eslint": "^8.44.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.36.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user