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:
|
options:
|
||||||
# The below comment is used to insert a new version with on-release.yml
|
# The below comment is used to insert a new version with on-release.yml
|
||||||
#NEXT_VERSION#
|
#NEXT_VERSION#
|
||||||
|
- 1.38.0
|
||||||
- 1.37.0
|
- 1.37.0
|
||||||
- 1.36.1
|
- 1.36.1
|
||||||
- 1.36.0
|
- 1.36.0
|
||||||
|
|||||||
@@ -57,10 +57,10 @@
|
|||||||
"@mantine/modals": "^8.3.1",
|
"@mantine/modals": "^8.3.1",
|
||||||
"@mantine/tiptap": "^8.3.1",
|
"@mantine/tiptap": "^8.3.1",
|
||||||
"@million/lint": "1.0.14",
|
"@million/lint": "1.0.14",
|
||||||
"@tabler/icons-react": "^3.34.1",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"@tanstack/react-query": "^5.87.4",
|
"@tanstack/react-query": "^5.89.0",
|
||||||
"@tanstack/react-query-devtools": "^5.87.4",
|
"@tanstack/react-query-devtools": "^5.89.0",
|
||||||
"@tanstack/react-query-next-experimental": "^5.87.4",
|
"@tanstack/react-query-next-experimental": "^5.89.0",
|
||||||
"@trpc/client": "^11.5.1",
|
"@trpc/client": "^11.5.1",
|
||||||
"@trpc/next": "^11.5.1",
|
"@trpc/next": "^11.5.1",
|
||||||
"@trpc/react-query": "^11.5.1",
|
"@trpc/react-query": "^11.5.1",
|
||||||
@@ -83,24 +83,24 @@
|
|||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-simple-code-editor": "^0.14.1",
|
"react-simple-code-editor": "^0.14.1",
|
||||||
"sass": "^1.92.1",
|
"sass": "^1.93.0",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.2",
|
||||||
"swagger-ui-react": "^5.29.0",
|
"swagger-ui-react": "^5.29.0",
|
||||||
"use-deep-compare-effect": "^1.8.1",
|
"use-deep-compare-effect": "^1.8.1",
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/chroma-js": "3.1.1",
|
"@types/chroma-js": "3.1.1",
|
||||||
"@types/node": "^22.18.3",
|
"@types/node": "^22.18.6",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/react": "19.1.13",
|
"@types/react": "19.1.13",
|
||||||
"@types/react-dom": "19.1.9",
|
"@types/react-dom": "19.1.9",
|
||||||
"@types/swagger-ui-react": "^5.18.0",
|
"@types/swagger-ui-react": "^5.18.0",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"node-loader": "^2.1.0",
|
"node-loader": "^2.1.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
|
|||||||
@@ -47,10 +47,10 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "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",
|
"dotenv-cli": "^10.0.0",
|
||||||
"esbuild": "^0.25.9",
|
"esbuild": "^0.25.10",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"tsx": "4.20.4",
|
"tsx": "4.20.4",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
|
|||||||
@@ -34,8 +34,8 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"esbuild": "^0.25.9",
|
"esbuild": "^0.25.10",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
|
|||||||
22
package.json
22
package.json
@@ -42,23 +42,23 @@
|
|||||||
"@semantic-release/github": "^11.0.6",
|
"@semantic-release/github": "^11.0.6",
|
||||||
"@semantic-release/npm": "^12.0.2",
|
"@semantic-release/npm": "^12.0.2",
|
||||||
"@semantic-release/release-notes-generator": "^14.1.0",
|
"@semantic-release/release-notes-generator": "^14.1.0",
|
||||||
"@testcontainers/redis": "^11.5.1",
|
"@testcontainers/redis": "^11.6.0",
|
||||||
"@turbo/gen": "^2.5.6",
|
"@turbo/gen": "^2.5.6",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^5.0.3",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
||||||
"cross-env": "^10.0.0",
|
"cross-env": "^10.0.0",
|
||||||
"jsdom": "^27.0.0",
|
"jsdom": "^27.0.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"semantic-release": "^24.2.8",
|
"semantic-release": "^24.2.9",
|
||||||
"testcontainers": "^11.5.1",
|
"testcontainers": "^11.6.0",
|
||||||
"turbo": "^2.5.6",
|
"turbo": "^2.5.6",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.16.1",
|
"packageManager": "pnpm@10.17.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.19.0"
|
"node": ">=22.19.0"
|
||||||
},
|
},
|
||||||
@@ -80,20 +80,20 @@
|
|||||||
"axios@>=1.0.0 <1.8.2": ">=1.12.2",
|
"axios@>=1.0.0 <1.8.2": ">=1.12.2",
|
||||||
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
|
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
|
||||||
"brace-expansion@>=1.0.0 <=1.1.11": ">=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",
|
"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",
|
"linkifyjs@<4.3.2": ">=4.3.2",
|
||||||
"nanoid@>=4.0.0 <5.0.9": ">=5.1.5",
|
"nanoid@>=4.0.0 <5.0.9": ">=5.1.5",
|
||||||
"prismjs@<1.30.0": ">=1.30.0",
|
"prismjs@<1.30.0": ">=1.30.0",
|
||||||
"proxmox-api>undici": "7.16.0",
|
"proxmox-api>undici": "7.16.0",
|
||||||
"react-is": "^19.1.1",
|
"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",
|
"sha.js@<=2.4.11": ">=2.4.12",
|
||||||
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.0",
|
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
|
||||||
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.0",
|
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
|
||||||
"tmp@<=0.2.3": ">=0.2.5",
|
"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": {
|
"patchedDependencies": {
|
||||||
"@types/node-unifi": "patches/@types__node-unifi.patch",
|
"@types/node-unifi": "patches/@types__node-unifi.patch",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@kubernetes/client-node": "^1.3.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/client": "^11.5.1",
|
||||||
"@trpc/react-query": "^11.5.1",
|
"@trpc/react-query": "^11.5.1",
|
||||||
"@trpc/server": "^11.5.1",
|
"@trpc/server": "^11.5.1",
|
||||||
@@ -53,13 +53,13 @@
|
|||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.2",
|
||||||
"trpc-to-openapi": "^3.0.1",
|
"trpc-to-openapi": "^3.0.1",
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
import { observable } from "@trpc/server/observable";
|
||||||
import { z } from "zod/v4";
|
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 { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
|
import type { CalendarEvent } from "@homarr/integrations/types";
|
||||||
import { radarrReleaseTypes } from "@homarr/integrations/types";
|
import { radarrReleaseTypes } from "@homarr/integrations/types";
|
||||||
import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar";
|
import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar";
|
||||||
|
|
||||||
@@ -19,14 +24,56 @@ export const calendarRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar")))
|
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar")))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const results = await Promise.all(
|
return await Promise.all(
|
||||||
ctx.integrations.map(async (integration) => {
|
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 });
|
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 { 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";
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
@@ -9,45 +11,19 @@ const atLocationInput = z.object({
|
|||||||
latitude: z.number(),
|
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({
|
export const weatherRouter = createTRPCRouter({
|
||||||
atLocation: publicProcedure.input(atLocationInput).query(async ({ input }) => {
|
atLocation: publicProcedure.input(atLocationInput).query(async ({ input }) => {
|
||||||
const res = await fetchWithTimeout(
|
const handler = weatherRequestHandler.handler(input);
|
||||||
`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`,
|
return await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false }).then((result) => result.data);
|
||||||
);
|
}),
|
||||||
const json: unknown = await res.json();
|
subscribeAtLocation: publicProcedure.input(atLocationInput).subscription(({ input }) => {
|
||||||
const weather = await atLocationOutput.parseAsync(json);
|
return observable<Weather>((emit) => {
|
||||||
return {
|
const handler = weatherRequestHandler.handler(input);
|
||||||
current: weather.current_weather,
|
const unsubscribe = handler.subscribe((data) => {
|
||||||
daily: weather.daily.time.map((value, index) => {
|
emit.next(data);
|
||||||
return {
|
});
|
||||||
time: value,
|
|
||||||
weatherCode: weather.daily.weathercode[index] ?? 404,
|
return unsubscribe;
|
||||||
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],
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
"next-auth": "5.0.0-beta.29",
|
"next-auth": "5.0.0-beta.29",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/bcrypt": "6.0.0",
|
"@types/bcrypt": "6.0.0",
|
||||||
"@types/cookies": "0.9.1",
|
"@types/cookies": "0.9.1",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,8 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"esbuild": "^0.25.9",
|
"esbuild": "^0.25.10",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,20 +31,20 @@
|
|||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@paralleldrive/cuid2": "^2.2.2",
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
"dns-caching": "^0.2.5",
|
"dns-caching": "^0.2.7",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
"octokit": "^5.0.3",
|
"octokit": "^5.0.3",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"undici": "7.16.0",
|
"undici": "7.16.0",
|
||||||
"zod": "^4.1.8",
|
"zod": "^4.1.11",
|
||||||
"zod-validation-error": "^4.0.1"
|
"zod-validation-error": "^4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,17 @@ const calculateTimeAgo = (timestamp: Date) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useTimeAgo = (timestamp: Date, updateFrequency = 1000) => {
|
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(() => {
|
useEffect(() => {
|
||||||
const intervalId = setInterval(() => setTimeAgo(calculateTimeAgo(timestamp)), updateFrequency);
|
const intervalId = setInterval(() => setTimeAgo(calculateTimeAgo(timestamp)), updateFrequency);
|
||||||
|
|
||||||
return () => clearInterval(intervalId); // clear interval on hook unmount
|
return () => clearInterval(intervalId); // clear interval on hook unmount
|
||||||
}, [timestamp]);
|
}, [timestamp, updateFrequency]);
|
||||||
|
|
||||||
return timeAgo;
|
return timeAgo;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,13 +26,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@t3-oss/env-nextjs": "^0.13.8",
|
"@t3-oss/env-nextjs": "^0.13.8",
|
||||||
"ioredis": "5.7.0",
|
"ioredis": "5.7.0",
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,13 +29,13 @@
|
|||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||||
"@homarr/log": "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/client": "^11.5.1",
|
||||||
"@trpc/server": "^11.5.1",
|
"@trpc/server": "^11.5.1",
|
||||||
"@trpc/tanstack-react-query": "^11.5.1",
|
"@trpc/tanstack-react-query": "^11.5.1",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/react": "19.1.13",
|
"@types/react": "19.1.13",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
|
|||||||
import { pingJob } from "./jobs/ping";
|
import { pingJob } from "./jobs/ping";
|
||||||
import { rssFeedsJob } from "./jobs/rss-feeds";
|
import { rssFeedsJob } from "./jobs/rss-feeds";
|
||||||
import { updateCheckerJob } from "./jobs/update-checker";
|
import { updateCheckerJob } from "./jobs/update-checker";
|
||||||
|
import { weatherJob } from "./jobs/weather";
|
||||||
import { createCronJobGroup } from "./lib";
|
import { createCronJobGroup } from "./lib";
|
||||||
|
|
||||||
export const jobGroup = createCronJobGroup({
|
export const jobGroup = createCronJobGroup({
|
||||||
@@ -48,6 +49,7 @@ export const jobGroup = createCronJobGroup({
|
|||||||
firewallVersion: firewallVersionJob,
|
firewallVersion: firewallVersionJob,
|
||||||
firewallInterfaces: firewallInterfacesJob,
|
firewallInterfaces: firewallInterfacesJob,
|
||||||
refreshNotifications: refreshNotificationsJob,
|
refreshNotifications: refreshNotificationsJob,
|
||||||
|
weather: weatherJob,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
|
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",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.1",
|
"@mantine/core": "^8.3.1",
|
||||||
"@paralleldrive/cuid2": "^2.2.2",
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"@testcontainers/mysql": "^11.5.1",
|
"@testcontainers/mysql": "^11.6.0",
|
||||||
"@testcontainers/postgresql": "^11.5.1",
|
"@testcontainers/postgresql": "^11.6.0",
|
||||||
"better-sqlite3": "^12.2.0",
|
"better-sqlite3": "^12.2.0",
|
||||||
"dotenv": "^17.2.2",
|
"dotenv": "^17.2.2",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
"drizzle-orm": "^0.44.5",
|
"drizzle-orm": "^0.44.5",
|
||||||
"drizzle-zod": "^0.8.3",
|
"drizzle-zod": "^0.8.3",
|
||||||
"mysql2": "3.14.5",
|
"mysql2": "3.15.0",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"superjson": "2.2.2"
|
"superjson": "2.2.2"
|
||||||
},
|
},
|
||||||
@@ -69,8 +69,8 @@
|
|||||||
"@types/better-sqlite3": "7.6.13",
|
"@types/better-sqlite3": "7.6.13",
|
||||||
"@types/pg": "^8.15.5",
|
"@types/pg": "^8.15.5",
|
||||||
"dotenv-cli": "^10.0.0",
|
"dotenv-cli": "^10.0.0",
|
||||||
"esbuild": "^0.25.9",
|
"esbuild": "^0.25.10",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"tsx": "4.20.4",
|
"tsx": "4.20.4",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
|
|||||||
@@ -25,13 +25,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"fast-xml-parser": "^5.2.5",
|
"fast-xml-parser": "^5.2.5",
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"tsx": "4.20.4",
|
"tsx": "4.20.4",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/tags/programming"
|
| "/docs/tags/programming"
|
||||||
| "/docs/tags/proxy"
|
| "/docs/tags/proxy"
|
||||||
| "/docs/tags/puid"
|
| "/docs/tags/puid"
|
||||||
|
| "/docs/tags/redis"
|
||||||
| "/docs/tags/responsive"
|
| "/docs/tags/responsive"
|
||||||
| "/docs/tags/roles"
|
| "/docs/tags/roles"
|
||||||
| "/docs/tags/search"
|
| "/docs/tags/search"
|
||||||
@@ -159,6 +160,7 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/integrations/github"
|
| "/docs/integrations/github"
|
||||||
| "/docs/integrations/gitlab"
|
| "/docs/integrations/gitlab"
|
||||||
| "/docs/integrations/home-assistant"
|
| "/docs/integrations/home-assistant"
|
||||||
|
| "/docs/integrations/ical"
|
||||||
| "/docs/integrations/jellyfin"
|
| "/docs/integrations/jellyfin"
|
||||||
| "/docs/integrations/jellyseerr"
|
| "/docs/integrations/jellyseerr"
|
||||||
| "/docs/integrations/kubernetes"
|
| "/docs/integrations/kubernetes"
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export const integrationDefs = {
|
|||||||
name: "Home Assistant",
|
name: "Home Assistant",
|
||||||
secretKinds: [["apiKey"]],
|
secretKinds: [["apiKey"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/home-assistant.svg",
|
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"),
|
documentationUrl: createDocumentationLink("/docs/integrations/home-assistant"),
|
||||||
},
|
},
|
||||||
openmediavault: {
|
openmediavault: {
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/dockerode": "^3.3.43",
|
"@types/dockerode": "^3.3.44",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,13 +28,13 @@
|
|||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/form": "^8.3.1",
|
"@mantine/form": "^8.3.1",
|
||||||
"mantine-form-zod-resolver": "^1.3.0",
|
"mantine-form-zod-resolver": "^1.3.0",
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,13 +31,13 @@
|
|||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.1",
|
"@mantine/core": "^8.3.1",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/bcrypt": "6.0.0",
|
"@types/bcrypt": "6.0.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,9 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ctrl/deluge": "^7.2.0",
|
"@ctrl/deluge": "^7.3.0",
|
||||||
"@ctrl/qbittorrent": "^9.7.0",
|
"@ctrl/qbittorrent": "^9.8.0",
|
||||||
"@ctrl/transmission": "^7.3.0",
|
"@ctrl/transmission": "^7.4.0",
|
||||||
"@gitbeaker/rest": "^43.5.0",
|
"@gitbeaker/rest": "^43.5.0",
|
||||||
"@homarr/certificates": "workspace:^0.1.0",
|
"@homarr/certificates": "workspace:^0.1.0",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
@@ -43,13 +43,13 @@
|
|||||||
"@octokit/auth-app": "^8.1.0",
|
"@octokit/auth-app": "^8.1.0",
|
||||||
"ical.js": "^2.2.1",
|
"ical.js": "^2.2.1",
|
||||||
"maria2": "^0.4.1",
|
"maria2": "^0.4.1",
|
||||||
"node-ical": "^0.20.1",
|
"node-ical": "^0.21.0",
|
||||||
"octokit": "^5.0.3",
|
"octokit": "^5.0.3",
|
||||||
"proxmox-api": "1.1.1",
|
"proxmox-api": "1.1.1",
|
||||||
"tsdav": "^2.1.5",
|
"tsdav": "^2.1.5",
|
||||||
"undici": "7.16.0",
|
"undici": "7.16.0",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node-unifi": "^2.5.1",
|
"@types/node-unifi": "^2.5.1",
|
||||||
"@types/xml2js": "^0.4.14",
|
"@types/xml2js": "^0.4.14",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
|
import { ResponseError } from "@homarr/common/server";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
import type { IntegrationTestingInput } from "../base/integration";
|
import type { IntegrationTestingInput } from "../base/integration";
|
||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
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 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) {
|
public async getEntityStateAsync(entityId: string) {
|
||||||
try {
|
try {
|
||||||
const response = await this.getAsync(`/api/states/${entityId}`);
|
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> {
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
const response = await input.fetchAsync(this.url("/api/config"), {
|
const response = await input.fetchAsync(this.url("/api/config"), {
|
||||||
headers: this.getAuthHeaders(),
|
headers: this.getAuthHeaders(),
|
||||||
@@ -82,8 +116,8 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI
|
|||||||
* @param path full path to the API endpoint
|
* @param path full path to the API endpoint
|
||||||
* @returns the response from the API
|
* @returns the response from the API
|
||||||
*/
|
*/
|
||||||
private async getAsync(path: `/api/${string}`) {
|
private async getAsync(path: `/api/${string}`, queryParams?: Record<string, string | Date | number | boolean>) {
|
||||||
return await fetchWithTrustedCertificatesAsync(this.url(path), {
|
return await fetchWithTrustedCertificatesAsync(this.url(path, queryParams), {
|
||||||
headers: this.getAuthHeaders(),
|
headers: this.getAuthHeaders(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,3 +12,27 @@ export const entityStateSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type EntityState = z.infer<typeof entityStateSchema>;
|
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,
|
src: imageSrc,
|
||||||
aspectRatio: { width: 7, height: 12 },
|
aspectRatio: { width: 7, height: 12 },
|
||||||
|
badge: {
|
||||||
|
color: "red",
|
||||||
|
content: `S${event.seasonNumber}/E${event.episodeNumber}`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
location: null,
|
location: null,
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const seriesRelease = (start: Date, end: Date): CalendarEvent => ({
|
|||||||
src: "https://image.tmdb.org/t/p/original/sWgBv7LV2PRoQgkxwlibdGXKz1S.jpg",
|
src: "https://image.tmdb.org/t/p/original/sWgBv7LV2PRoQgkxwlibdGXKz1S.jpg",
|
||||||
aspectRatio: { width: 7, height: 12 },
|
aspectRatio: { width: 7, height: 12 },
|
||||||
badge: {
|
badge: {
|
||||||
content: "S1:E1",
|
content: "S1/E1",
|
||||||
color: "red",
|
color: "red",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,13 +27,13 @@
|
|||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.2",
|
||||||
"winston": "3.17.0",
|
"winston": "3.17.0",
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,18 +34,18 @@
|
|||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.1",
|
"@mantine/core": "^8.3.1",
|
||||||
"@tabler/icons-react": "^3.34.1",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/notifications": "^8.3.1",
|
"@mantine/notifications": "^8.3.1",
|
||||||
"@tabler/icons-react": "^3.34.1"
|
"@tabler/icons-react": "^3.35.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.2",
|
||||||
"zod": "^4.1.8",
|
"zod": "^4.1.11",
|
||||||
"zod-form-data": "^3.0.1"
|
"zod-form-data": "^3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/adm-zip": "0.5.7",
|
"@types/adm-zip": "0.5.7",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,13 +23,13 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,13 +32,14 @@
|
|||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
"octokit": "^5.0.3",
|
"octokit": "^5.0.3",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.2",
|
||||||
"undici": "7.16.0"
|
"undici": "7.16.0",
|
||||||
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"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/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
"@mantine/core": "^8.3.1",
|
"@mantine/core": "^8.3.1",
|
||||||
"@mantine/hooks": "^8.3.1",
|
"@mantine/hooks": "^8.3.1",
|
||||||
"@mantine/spotlight": "^8.3.1",
|
"@mantine/spotlight": "^8.3.1",
|
||||||
"@tabler/icons-react": "^3.34.1",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"jotai": "^2.14.0",
|
"jotai": "^2.14.0",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"deepmerge": "4.3.1",
|
"deepmerge": "4.3.1",
|
||||||
"mantine-react-table": "2.0.0-beta.9",
|
"mantine-react-table": "2.0.0-beta.9",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
"next-intl": "4.3.8",
|
"next-intl": "4.3.9",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1"
|
"react-dom": "19.1.1"
|
||||||
},
|
},
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1707,6 +1707,9 @@
|
|||||||
"calendar": {
|
"calendar": {
|
||||||
"name": "Calendar",
|
"name": "Calendar",
|
||||||
"description": "Display events from your integrations in a calendar view within a certain relative time period",
|
"description": "Display events from your integrations in a calendar view within a certain relative time period",
|
||||||
|
"duration": {
|
||||||
|
"allDay": "All day"
|
||||||
|
},
|
||||||
"option": {
|
"option": {
|
||||||
"releaseType": {
|
"releaseType": {
|
||||||
"label": "Radarr release type",
|
"label": "Radarr release type",
|
||||||
@@ -3315,6 +3318,9 @@
|
|||||||
},
|
},
|
||||||
"firewallInterfaces": {
|
"firewallInterfaces": {
|
||||||
"label": "Firewall Interfaces"
|
"label": "Firewall Interfaces"
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"label": "Weather"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"interval": {
|
"interval": {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"@mantine/core": "^8.3.1",
|
"@mantine/core": "^8.3.1",
|
||||||
"@mantine/dates": "^8.3.1",
|
"@mantine/dates": "^8.3.1",
|
||||||
"@mantine/hooks": "^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",
|
"mantine-react-table": "2.0.0-beta.9",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/css-modules": "^1.0.5",
|
"@types/css-modules": "^1.0.5",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,14 +24,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"zod": "^4.1.8",
|
"zod": "^4.1.11",
|
||||||
"zod-form-data": "^3.0.1"
|
"zod-form-data": "^3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
"@mantine/charts": "^8.3.1",
|
"@mantine/charts": "^8.3.1",
|
||||||
"@mantine/core": "^8.3.1",
|
"@mantine/core": "^8.3.1",
|
||||||
"@mantine/hooks": "^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-color": "2.26.1",
|
||||||
"@tiptap/extension-highlight": "2.26.1",
|
"@tiptap/extension-highlight": "2.26.1",
|
||||||
"@tiptap/extension-image": "2.26.1",
|
"@tiptap/extension-image": "2.26.1",
|
||||||
@@ -78,14 +78,14 @@
|
|||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"recharts": "^2.15.4",
|
"recharts": "^2.15.4",
|
||||||
"video.js": "^8.23.4",
|
"video.js": "^8.23.4",
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/video.js": "^7.3.58",
|
"@types/video.js": "^7.3.58",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,16 +84,24 @@ export const CalendarEventList = ({ events }: CalendarEventListProps) => {
|
|||||||
|
|
||||||
<Group gap={3} wrap="nowrap" align={"center"}>
|
<Group gap={3} wrap="nowrap" align={"center"}>
|
||||||
<IconClock opacity={0.7} size={"1rem"} />
|
<IconClock opacity={0.7} size={"1rem"} />
|
||||||
<Text c={"dimmed"} size={"sm"}>
|
{isAllDay(event) ? (
|
||||||
{dayjs(event.startDate).format("HH:mm")}
|
<Text c={"dimmed"} size={"sm"}>
|
||||||
</Text>
|
{t("widget.calendar.duration.allDay")}
|
||||||
|
</Text>
|
||||||
{event.endDate !== null && (
|
) : (
|
||||||
<>
|
<>
|
||||||
-{" "}
|
|
||||||
<Text c={"dimmed"} size={"sm"}>
|
<Text c={"dimmed"} size={"sm"}>
|
||||||
{dayjs(event.endDate).format("HH:mm")}
|
{dayjs(event.startDate).format("HH:mm")}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
{event.endDate !== null && (
|
||||||
|
<>
|
||||||
|
-{" "}
|
||||||
|
<Text c={"dimmed"} size={"sm"}>
|
||||||
|
{dayjs(event.endDate).format("HH:mm")}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
@@ -152,3 +160,12 @@ export const CalendarEventList = ({ events }: CalendarEventListProps) => {
|
|||||||
</ScrollArea>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useMantineTheme } from "@mantine/core";
|
import { useMantineTheme } from "@mantine/core";
|
||||||
import { Calendar } from "@mantine/dates";
|
import { Calendar } from "@mantine/dates";
|
||||||
import { useElementSize } from "@mantine/hooks";
|
import { useElementSize } from "@mantine/hooks";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useRequiredBoard } from "@homarr/boards/context";
|
import { useRequiredBoard } from "@homarr/boards/context";
|
||||||
|
import type { CalendarEvent } from "@homarr/integrations/types";
|
||||||
import { useSettings } from "@homarr/settings";
|
import { useSettings } from "@homarr/settings";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../definition";
|
import type { WidgetComponentProps } from "../definition";
|
||||||
@@ -32,28 +32,43 @@ interface FetchCalendarProps extends WidgetComponentProps<"calendar"> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }: FetchCalendarProps) => {
|
const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }: FetchCalendarProps) => {
|
||||||
const [events] = clientApi.widget.calendar.findAllEvents.useSuspenseQuery(
|
const input = {
|
||||||
{
|
integrationIds,
|
||||||
integrationIds,
|
month: month.getMonth(),
|
||||||
month: month.getMonth(),
|
year: month.getFullYear(),
|
||||||
year: month.getFullYear(),
|
releaseType: options.releaseType,
|
||||||
releaseType: options.releaseType,
|
showUnmonitored: options.showUnmonitored,
|
||||||
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,
|
const events = useMemo(() => data.flatMap((item) => item.events), [data]);
|
||||||
refetchOnReconnect: false,
|
|
||||||
retry: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return <CalendarBase isEditMode={isEditMode} events={events} month={month} setMonth={setMonth} options={options} />;
|
return <CalendarBase isEditMode={isEditMode} events={events} month={month} setMonth={setMonth} options={options} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CalendarBaseProps {
|
interface CalendarBaseProps {
|
||||||
isEditMode: boolean;
|
isEditMode: boolean;
|
||||||
events: RouterOutputs["widget"]["calendar"]["findAllEvents"];
|
events: CalendarEvent[];
|
||||||
month: Date;
|
month: Date;
|
||||||
setMonth: (date: Date) => void;
|
setMonth: (date: Date) => void;
|
||||||
options: WidgetComponentProps<"calendar">["options"];
|
options: WidgetComponentProps<"calendar">["options"];
|
||||||
@@ -69,6 +84,8 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
|
|||||||
const { ref, width, height } = useElementSize();
|
const { ref, width, height } = useElementSize();
|
||||||
const isSmall = width < 256;
|
const isSmall = width < 256;
|
||||||
|
|
||||||
|
const normalizedEvents = useMemo(() => splitEvents(events), [events]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Calendar
|
<Calendar
|
||||||
defaultDate={new Date()}
|
defaultDate={new Date()}
|
||||||
@@ -122,7 +139,7 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
renderDay={(tileDate) => {
|
renderDay={(tileDate) => {
|
||||||
const eventsForDate = events
|
const eventsForDate = normalizedEvents
|
||||||
.filter((event) => dayjs(event.startDate).isSame(tileDate, "day"))
|
.filter((event) => dayjs(event.startDate).isSame(tileDate, "day"))
|
||||||
.filter(
|
.filter(
|
||||||
(event) => event.metadata?.type !== "radarr" || options.releaseType.includes(event.metadata.releaseType),
|
(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";
|
import { WeatherDescription, WeatherIcon } from "./icon";
|
||||||
|
|
||||||
export default function WeatherWidget({ isEditMode, options }: WidgetComponentProps<"weather">) {
|
export default function WeatherWidget({ isEditMode, options }: WidgetComponentProps<"weather">) {
|
||||||
const [weather] = clientApi.widget.weather.atLocation.useSuspenseQuery(
|
const input = {
|
||||||
{
|
latitude: options.location.latitude,
|
||||||
latitude: options.location.latitude,
|
longitude: options.location.longitude,
|
||||||
longitude: options.location.longitude,
|
};
|
||||||
},
|
const [weather] = clientApi.widget.weather.atLocation.useSuspenseQuery(input, {
|
||||||
{
|
refetchOnMount: false,
|
||||||
refetchOnMount: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnReconnect: false,
|
||||||
refetchOnReconnect: false,
|
});
|
||||||
},
|
|
||||||
);
|
const utils = clientApi.useUtils();
|
||||||
|
clientApi.widget.weather.subscribeAtLocation.useSubscription(input, {
|
||||||
|
onData: (data) => utils.widget.weather.atLocation.setData(input, data),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<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-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"typescript-eslint": "^8.43.0"
|
"typescript-eslint": "^8.44.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user