diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index cb8d4ae49..cdf4ece79 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -48,17 +48,17 @@ "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "@homarr/widgets": "workspace:^0.1.0", - "@mantine/colors-generator": "^7.17.0", - "@mantine/core": "^7.17.0", - "@mantine/dropzone": "^7.17.0", - "@mantine/hooks": "^7.17.0", - "@mantine/modals": "^7.17.0", - "@mantine/tiptap": "^7.17.0", + "@mantine/colors-generator": "^7.17.1", + "@mantine/core": "^7.17.1", + "@mantine/dropzone": "^7.17.1", + "@mantine/hooks": "^7.17.1", + "@mantine/modals": "^7.17.1", + "@mantine/tiptap": "^7.17.1", "@million/lint": "1.0.14", - "@tabler/icons-react": "^3.30.0", - "@tanstack/react-query": "^5.66.11", - "@tanstack/react-query-devtools": "^5.66.11", - "@tanstack/react-query-next-experimental": "^5.66.11", + "@tabler/icons-react": "^3.31.0", + "@tanstack/react-query": "^5.67.1", + "@tanstack/react-query-devtools": "^5.67.1", + "@tanstack/react-query-next-experimental": "^5.67.1", "@trpc/client": "next", "@trpc/next": "next", "@trpc/react-query": "next", @@ -92,7 +92,7 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/chroma-js": "3.1.1", - "@types/node": "^22.13.5", + "@types/node": "^22.13.9", "@types/prismjs": "^1.26.5", "@types/react": "19.0.10", "@types/react-dom": "19.0.4", @@ -100,7 +100,7 @@ "concurrently": "^9.1.2", "eslint": "^9.21.0", "node-loader": "^2.1.0", - "prettier": "^3.5.2", + "prettier": "^3.5.3", "typescript": "^5.8.2" } } diff --git a/apps/nextjs/src/app/[locale]/manage/apps/page.tsx b/apps/nextjs/src/app/[locale]/manage/apps/page.tsx index 4479c8ccc..b3dc291fc 100644 --- a/apps/nextjs/src/app/[locale]/manage/apps/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/apps/page.tsx @@ -61,7 +61,8 @@ export default async function AppsPage(props: AppsPageProps) { )} - + {/* Added margin to not hide pagination behind affix-button */} + diff --git a/apps/nextjs/src/app/[locale]/manage/medias/page.tsx b/apps/nextjs/src/app/[locale]/manage/medias/page.tsx index 8fd2e04f2..569168cab 100644 --- a/apps/nextjs/src/app/[locale]/manage/medias/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/medias/page.tsx @@ -92,7 +92,8 @@ export default async function GroupsListPage(props: MediaListPageProps) { - + {/* Added margin to not hide pagination behind affix-button */} + diff --git a/apps/nextjs/src/app/[locale]/manage/search-engines/page.tsx b/apps/nextjs/src/app/[locale]/manage/search-engines/page.tsx index 2423e2a93..7634d78be 100644 --- a/apps/nextjs/src/app/[locale]/manage/search-engines/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/search-engines/page.tsx @@ -61,7 +61,8 @@ export default async function SearchEnginesPage(props: SearchEnginesPageProps) { )} - + {/* Added margin to not hide pagination behind affix-button */} + diff --git a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx index b3d8663d2..b2478d5d9 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx @@ -35,7 +35,13 @@ const createColumns = ( Cell({ renderedCellValue, row }) { return ( - + {row.original.name.at(0)?.toUpperCase()} {renderedCellValue} diff --git a/apps/nextjs/src/components/board/items/actions/test/mocks/dynamic-section-mock.ts b/apps/nextjs/src/components/board/items/actions/test/mocks/dynamic-section-mock.ts index 40fdd059f..7de8db63c 100644 --- a/apps/nextjs/src/components/board/items/actions/test/mocks/dynamic-section-mock.ts +++ b/apps/nextjs/src/components/board/items/actions/test/mocks/dynamic-section-mock.ts @@ -9,6 +9,9 @@ export class DynamicSectionMockBuilder { this.section = { id: createId(), kind: "dynamic", + options: { + borderColor: "", + }, layouts: [], ...section, } satisfies DynamicSection; diff --git a/apps/nextjs/src/components/board/sections/dynamic-section.tsx b/apps/nextjs/src/components/board/sections/dynamic-section.tsx index b2cb0b417..90525715a 100644 --- a/apps/nextjs/src/components/board/sections/dynamic-section.tsx +++ b/apps/nextjs/src/components/board/sections/dynamic-section.tsx @@ -14,6 +14,8 @@ interface Props { export const BoardDynamicSection = ({ section }: Props) => { const board = useRequiredBoard(); const currentLayoutId = useCurrentLayout(); + const options = section.options; + return ( { root: { "--opacity": board.opacity / 100, overflow: "hidden", + "--border-color": options.borderColor !== "" ? options.borderColor : undefined, }, }} radius={board.itemRadius} diff --git a/apps/nextjs/src/components/board/sections/dynamic/actions/add-dynamic-section.ts b/apps/nextjs/src/components/board/sections/dynamic/actions/add-dynamic-section.ts index eea8deebd..415d80572 100644 --- a/apps/nextjs/src/components/board/sections/dynamic/actions/add-dynamic-section.ts +++ b/apps/nextjs/src/components/board/sections/dynamic/actions/add-dynamic-section.ts @@ -16,6 +16,9 @@ export const addDynamicSectionCallback = () => (board: Board) => { const newSection = { id: createId(), kind: "dynamic", + options: { + borderColor: "", + }, layouts: createDynamicSectionLayouts(board, firstSection), } satisfies DynamicSection; diff --git a/apps/nextjs/src/components/board/sections/dynamic/dynamic-actions.ts b/apps/nextjs/src/components/board/sections/dynamic/dynamic-actions.ts index 2684bdea4..324f4df1b 100644 --- a/apps/nextjs/src/components/board/sections/dynamic/dynamic-actions.ts +++ b/apps/nextjs/src/components/board/sections/dynamic/dynamic-actions.ts @@ -1,11 +1,18 @@ import { useCallback } from "react"; +import type { z } from "zod"; import { useUpdateBoard } from "@homarr/boards/updater"; +import type { dynamicSectionOptionsSchema } from "@homarr/validation"; import { addDynamicSectionCallback } from "./actions/add-dynamic-section"; import type { RemoveDynamicSectionInput } from "./actions/remove-dynamic-section"; import { removeDynamicSectionCallback } from "./actions/remove-dynamic-section"; +interface UpdateDynamicOptions { + itemId: string; + newOptions: z.infer; +} + export const useDynamicSectionActions = () => { const { updateBoard } = useUpdateBoard(); @@ -13,6 +20,16 @@ export const useDynamicSectionActions = () => { updateBoard(addDynamicSectionCallback()); }, [updateBoard]); + const updateDynamicSection = useCallback( + ({ itemId, newOptions }: UpdateDynamicOptions) => { + updateBoard((previous) => ({ + ...previous, + sections: previous.sections.map((item) => (item.id !== itemId ? item : { ...item, options: newOptions })), + })); + }, + [updateBoard], + ); + const removeDynamicSection = useCallback( (input: RemoveDynamicSectionInput) => { updateBoard(removeDynamicSectionCallback(input)); @@ -22,6 +39,7 @@ export const useDynamicSectionActions = () => { return { addDynamicSection, + updateDynamicSection, removeDynamicSection, }; }; diff --git a/apps/nextjs/src/components/board/sections/dynamic/dynamic-edit-modal.tsx b/apps/nextjs/src/components/board/sections/dynamic/dynamic-edit-modal.tsx new file mode 100644 index 000000000..001af145a --- /dev/null +++ b/apps/nextjs/src/components/board/sections/dynamic/dynamic-edit-modal.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { Button, CloseButton, ColorInput, Group, Stack, useMantineTheme } from "@mantine/core"; +import type { z } from "zod"; + +import { useZodForm } from "@homarr/form"; +import { createModal } from "@homarr/modals"; +import { useI18n } from "@homarr/translation/client"; +import { dynamicSectionOptionsSchema } from "@homarr/validation"; + +interface ModalProps { + value: z.infer; + onSuccessfulEdit: (value: z.infer) => void; +} + +export const DynamicSectionEditModal = createModal(({ actions, innerProps }) => { + const t = useI18n(); + const theme = useMantineTheme(); + + const form = useZodForm(dynamicSectionOptionsSchema, { + mode: "controlled", + initialValues: { ...innerProps.value }, + }); + + return ( +
{ + innerProps.onSuccessfulEdit(values); + actions.closeModal(); + })} + > + + color[6])} + rightSection={ + form.setFieldValue("borderColor", "")} + style={{ display: form.getInputProps("borderColor").value ? undefined : "none" }} + /> + } + {...form.getInputProps("borderColor")} + /> + + + + + + + +
+ ); +}).withOptions({ + defaultTitle(t) { + return t("item.edit.title"); + }, + size: "lg", +}); diff --git a/apps/nextjs/src/components/board/sections/dynamic/dynamic-menu.tsx b/apps/nextjs/src/components/board/sections/dynamic/dynamic-menu.tsx index 3f38c8681..f73328024 100644 --- a/apps/nextjs/src/components/board/sections/dynamic/dynamic-menu.tsx +++ b/apps/nextjs/src/components/board/sections/dynamic/dynamic-menu.tsx @@ -1,22 +1,37 @@ import { ActionIcon, Menu } from "@mantine/core"; -import { IconDotsVertical, IconTrash } from "@tabler/icons-react"; +import { IconDotsVertical, IconPencil, IconTrash } from "@tabler/icons-react"; import { useEditMode } from "@homarr/boards/edit-mode"; -import { useConfirmModal } from "@homarr/modals"; +import { useConfirmModal, useModalAction } from "@homarr/modals"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; import type { DynamicSectionItem } from "~/app/[locale]/boards/_types"; import { useDynamicSectionActions } from "./dynamic-actions"; +import { DynamicSectionEditModal } from "./dynamic-edit-modal"; export const BoardDynamicSectionMenu = ({ section }: { section: DynamicSectionItem }) => { const t = useI18n(); const tDynamic = useScopedI18n("section.dynamic"); - const { removeDynamicSection } = useDynamicSectionActions(); + const tItem = useScopedI18n("item"); + const { openModal } = useModalAction(DynamicSectionEditModal); + const { updateDynamicSection, removeDynamicSection } = useDynamicSectionActions(); const { openConfirmModal } = useConfirmModal(); const [isEditMode] = useEditMode(); if (!isEditMode) return null; + const openEditModal = () => { + openModal({ + value: section.options, + onSuccessfulEdit: (options) => { + updateDynamicSection({ + itemId: section.id, + newOptions: options, + }); + }, + }); + }; + const openRemoveModal = () => { openConfirmModal({ title: tDynamic("remove.title"), @@ -35,6 +50,11 @@ export const BoardDynamicSectionMenu = ({ section }: { section: DynamicSectionIt + {tItem("menu.label.settings")} + } onClick={openEditModal}> + {tItem("action.edit")} + + {t("common.dangerZone")} } onClick={openRemoveModal}> {tDynamic("action.remove")} diff --git a/apps/tasks/package.json b/apps/tasks/package.json index 2fc92cbea..3dfb74472 100644 --- a/apps/tasks/package.json +++ b/apps/tasks/package.json @@ -2,7 +2,7 @@ "name": "@homarr/tasks", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./src/index.ts" @@ -44,10 +44,10 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "@types/node": "^22.13.5", + "@types/node": "^22.13.9", "dotenv-cli": "^8.0.0", "eslint": "^9.21.0", - "prettier": "^3.5.2", + "prettier": "^3.5.3", "tsx": "4.19.3", "typescript": "^5.8.2" } diff --git a/apps/websocket/package.json b/apps/websocket/package.json index 7c7bfd3cf..77e9443a8 100644 --- a/apps/websocket/package.json +++ b/apps/websocket/package.json @@ -2,7 +2,7 @@ "name": "@homarr/websocket", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "main": "./src/main.ts", "types": "./src/main.ts", @@ -33,9 +33,9 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "@types/ws": "^8.5.14", + "@types/ws": "^8.18.0", "eslint": "^9.21.0", - "prettier": "^3.5.2", + "prettier": "^3.5.3", "typescript": "^5.8.2" } } diff --git a/development/docker-run.cmd b/development/docker-run.cmd index 1f608898f..2ef22223b 100644 --- a/development/docker-run.cmd +++ b/development/docker-run.cmd @@ -1 +1,2 @@ -docker run -p 7575:7575 homarr:latest \ No newline at end of file +:: Please do not run this command in production. It is only for local testing. +docker run -p 7575:7575 -e SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 homarr:latest \ No newline at end of file diff --git a/package.json b/package.json index 1e5a6a7b2..fc58a2e09 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "conventional-changelog-conventionalcommits": "^8.0.0", "cross-env": "^7.0.3", "jsdom": "^26.0.0", - "prettier": "^3.5.2", + "prettier": "^3.5.3", "semantic-release": "^24.2.3", "testcontainers": "^10.18.0", "turbo": "^2.4.4", diff --git a/packages/analytics/package.json b/packages/analytics/package.json index a70de85b6..b6b339c9c 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -2,7 +2,7 @@ "name": "@homarr/analytics", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts" diff --git a/packages/analytics/src/send-server-analytics.ts b/packages/analytics/src/send-server-analytics.ts index e09bd71da..cdf94e134 100644 --- a/packages/analytics/src/send-server-analytics.ts +++ b/packages/analytics/src/send-server-analytics.ts @@ -36,7 +36,7 @@ const sendWidgetDataAsync = async (umamiInstance: Umami, analyticsSettings: type if (!analyticsSettings.enableWidgetData) { return; } - const widgetCount = (await db.select({ count: count(items.id) }).from(items))[0]?.count ?? 0; + const widgetCount = await db.$count(items); const response = await umamiInstance.track("server-widget-data", { countWidgets: widgetCount, @@ -52,7 +52,7 @@ const sendUserDataAsync = async (umamiInstance: Umami, analyticsSettings: typeof if (!analyticsSettings.enableUserData) { return; } - const userCount = (await db.select({ count: count(users.id) }).from(users))[0]?.count ?? 0; + const userCount = await db.$count(users); const response = await umamiInstance.track("server-user-data", { countUsers: userCount, diff --git a/packages/api/package.json b/packages/api/package.json index 0657e4611..943b15024 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -2,7 +2,7 @@ "name": "@homarr/api", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./src/index.ts", @@ -57,7 +57,7 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "eslint": "^9.21.0", - "prettier": "^3.5.2", + "prettier": "^3.5.3", "typescript": "^5.8.2" } } diff --git a/packages/api/src/router/board.ts b/packages/api/src/router/board.ts index 900b49ca6..ffdff8228 100644 --- a/packages/api/src/router/board.ts +++ b/packages/api/src/router/board.ts @@ -5,7 +5,7 @@ import { z } from "zod"; import { constructBoardPermissions } from "@homarr/auth/shared"; import type { DeviceType } from "@homarr/common/server"; import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db"; -import { and, asc, createId, eq, handleTransactionsAsync, inArray, isNull, like, not, or } from "@homarr/db"; +import { and, asc, createId, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db"; import { createDbInsertCollectionWithoutTransaction } from "@homarr/db/collection"; import { getServerSettingByKeyAsync } from "@homarr/db/queries"; import { @@ -27,7 +27,13 @@ import { users, } from "@homarr/db/schema"; import type { WidgetKind } from "@homarr/definitions"; -import { everyoneGroup, getPermissionsWithChildren, getPermissionsWithParents, widgetKinds } from "@homarr/definitions"; +import { + emptySuperJSON, + everyoneGroup, + getPermissionsWithChildren, + getPermissionsWithParents, + widgetKinds, +} from "@homarr/definitions"; import { importOldmarrAsync } from "@homarr/old-import"; import { importJsonFileSchema } from "@homarr/old-import/shared"; import { oldmarrConfigSchema } from "@homarr/old-schema"; @@ -554,7 +560,7 @@ export const boardRouter = createTRPCRouter({ return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null); }), getBoardByName: publicProcedure.input(validation.board.byName).query(async ({ input, ctx }) => { - const boardWhere = eq(boards.name, input.name); + const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase()); await throwIfActionForbiddenAsync(ctx, boardWhere, "view"); return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null); @@ -736,6 +742,7 @@ export const boardRouter = createTRPCRouter({ kind: section.kind, yOffset: section.kind !== "dynamic" ? section.yOffset : null, xOffset: section.kind === "dynamic" ? null : 0, + options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON, name: "name" in section ? section.name : null, boardId: dbBoard.id, })), @@ -861,6 +868,7 @@ export const boardRouter = createTRPCRouter({ .set({ yOffset: prev?.kind !== "dynamic" && "yOffset" in section ? section.yOffset : null, xOffset: prev?.kind !== "dynamic" && "yOffset" in section ? 0 : null, + options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON, name: prev?.kind === "category" && "name" in section ? section.name : null, }) .where(eq(schema.sections.id, section.id)); @@ -934,6 +942,7 @@ export const boardRouter = createTRPCRouter({ kind: section.kind, yOffset: section.kind !== "dynamic" ? section.yOffset : null, xOffset: section.kind === "dynamic" ? null : 0, + options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON, name: "name" in section ? section.name : null, boardId: dbBoard.id, })), @@ -1069,6 +1078,7 @@ export const boardRouter = createTRPCRouter({ .set({ yOffset: prev?.kind !== "dynamic" && "yOffset" in section ? section.yOffset : null, xOffset: prev?.kind !== "dynamic" && "yOffset" in section ? 0 : null, + options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON, name: prev?.kind === "category" && "name" in section ? section.name : null, }) .where(eq(sections.id, section.id)) @@ -1561,6 +1571,7 @@ const getFullBoardWithWhereAsync = async (db: Database, where: SQL, use ...section, xOffset: section.xOffset, yOffset: section.yOffset, + options: superjson.parse(section.options ?? emptySuperJSON), layouts: section.layouts.map((layout) => ({ xOffset: layout.xOffset, yOffset: layout.yOffset, diff --git a/packages/api/src/router/group.ts b/packages/api/src/router/group.ts index 37dde6e16..36fa8ba3d 100644 --- a/packages/api/src/router/group.ts +++ b/packages/api/src/router/group.ts @@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; import type { Database } from "@homarr/db"; -import { and, createId, eq, handleTransactionsAsync, like, not, sql } from "@homarr/db"; +import { and, createId, eq, handleTransactionsAsync, like, not } from "@homarr/db"; import { getMaxGroupPositionAsync } from "@homarr/db/queries"; import { groupMembers, groupPermissions, groups } from "@homarr/db/schema"; import { everyoneGroup } from "@homarr/definitions"; @@ -42,12 +42,7 @@ export const groupRouter = createTRPCRouter({ .input(validation.common.paginated) .query(async ({ input, ctx }) => { const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined; - const groupCount = await ctx.db - .select({ - count: sql`count(*)`, - }) - .from(groups) - .where(whereQuery); + const groupCount = await ctx.db.$count(groups, whereQuery); const dbGroups = await ctx.db.query.groups.findMany({ with: { @@ -74,7 +69,7 @@ export const groupRouter = createTRPCRouter({ ...group, members: group.members.map((member) => member.user), })), - totalCount: groupCount[0]?.count ?? 0, + totalCount: groupCount, }; }), getById: permissionRequiredProcedure diff --git a/packages/api/src/router/home.ts b/packages/api/src/router/home.ts index 7b65c74f3..16d31ffba 100644 --- a/packages/api/src/router/home.ts +++ b/packages/api/src/router/home.ts @@ -2,7 +2,6 @@ import type { AnySQLiteTable } from "drizzle-orm/sqlite-core"; import { isProviderEnabled } from "@homarr/auth/server"; import type { Database } from "@homarr/db"; -import { count } from "@homarr/db"; import { apps, boards, groups, integrations, invites, users } from "@homarr/db/schema"; import { createTRPCRouter, publicProcedure } from "../trpc"; @@ -28,5 +27,5 @@ const getCountForTableAsync = async (db: Database, table: AnySQLiteTable, canVie return 0; } - return (await db.select({ count: count() }).from(table))[0]?.count ?? 0; + return await db.$count(table); }; diff --git a/packages/api/src/router/icons.ts b/packages/api/src/router/icons.ts index 9a273336d..4a3b33051 100644 --- a/packages/api/src/router/icons.ts +++ b/packages/api/src/router/icons.ts @@ -1,4 +1,4 @@ -import { and, count, like } from "@homarr/db"; +import { and, like } from "@homarr/db"; import { icons } from "@homarr/db/schema"; import { validation } from "@homarr/validation"; @@ -24,7 +24,7 @@ export const iconsRouter = createTRPCRouter({ }, }, }), - countIcons: (await ctx.db.select({ count: count() }).from(icons))[0]?.count ?? 0, + countIcons: await ctx.db.$count(icons), }; }), }); diff --git a/packages/api/src/router/integration/integration-router.ts b/packages/api/src/router/integration/integration-router.ts index 8fa570f8d..e133e2b9c 100644 --- a/packages/api/src/router/integration/integration-router.ts +++ b/packages/api/src/router/integration/integration-router.ts @@ -23,7 +23,7 @@ import { integrationKinds, integrationSecretKindObject, } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import { validation } from "@homarr/validation"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; @@ -465,7 +465,7 @@ export const integrationRouter = createTRPCRouter({ .unstable_concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("search"))) .input(z.object({ integrationId: z.string(), query: z.string() })) .query(async ({ ctx, input }) => { - const integrationInstance = integrationCreator(ctx.integration); + const integrationInstance = await createIntegrationAsync(ctx.integration); return await integrationInstance.searchAsync(encodeURI(input.query)); }), }); diff --git a/packages/api/src/router/integration/integration-test-connection.ts b/packages/api/src/router/integration/integration-test-connection.ts index db36ef568..96ae9f4f4 100644 --- a/packages/api/src/router/integration/integration-test-connection.ts +++ b/packages/api/src/router/integration/integration-test-connection.ts @@ -4,7 +4,7 @@ import { decryptSecret } from "@homarr/common/server"; import type { Integration } from "@homarr/db/schema"; import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions"; import { getAllSecretKindOptions } from "@homarr/definitions"; -import { integrationCreator, IntegrationTestConnectionError } from "@homarr/integrations"; +import { createIntegrationAsync, IntegrationTestConnectionError } from "@homarr/integrations"; import { logger } from "@homarr/log"; type FormIntegration = Integration & { @@ -66,7 +66,7 @@ export const testConnectionAsync = async ( const { secrets: _, ...baseIntegration } = integration; - const integrationInstance = integrationCreator({ + const integrationInstance = await createIntegrationAsync({ ...baseIntegration, decryptedSecrets, }); diff --git a/packages/api/src/router/search-engine/search-engine-router.ts b/packages/api/src/router/search-engine/search-engine-router.ts index 4b99e476a..7b6eee219 100644 --- a/packages/api/src/router/search-engine/search-engine-router.ts +++ b/packages/api/src/router/search-engine/search-engine-router.ts @@ -1,10 +1,10 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; -import { asc, createId, eq, like, sql } from "@homarr/db"; +import { asc, createId, eq, like } from "@homarr/db"; import { getServerSettingByKeyAsync } from "@homarr/db/queries"; import { searchEngines, users } from "@homarr/db/schema"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import { validation } from "@homarr/validation"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; @@ -13,12 +13,7 @@ import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publ export const searchEngineRouter = createTRPCRouter({ getPaginated: protectedProcedure.input(validation.common.paginated).query(async ({ input, ctx }) => { const whereQuery = input.search ? like(searchEngines.name, `%${input.search.trim()}%`) : undefined; - const searchEngineCount = await ctx.db - .select({ - count: sql`count(*)`, - }) - .from(searchEngines) - .where(whereQuery); + const searchEngineCount = await ctx.db.$count(searchEngines, whereQuery); const dbSearachEngines = await ctx.db.query.searchEngines.findMany({ limit: input.pageSize, @@ -28,7 +23,7 @@ export const searchEngineRouter = createTRPCRouter({ return { items: dbSearachEngines, - totalCount: searchEngineCount[0]?.count ?? 0, + totalCount: searchEngineCount, }; }), getSelectable: protectedProcedure @@ -139,14 +134,14 @@ export const searchEngineRouter = createTRPCRouter({ .unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr")) .input(validation.common.mediaRequestOptions) .query(async ({ ctx, input }) => { - const integration = integrationCreator(ctx.integration); + const integration = await createIntegrationAsync(ctx.integration); return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId); }), requestMedia: protectedProcedure .unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr")) .input(validation.common.requestMedia) .mutation(async ({ ctx, input }) => { - const integration = integrationCreator(ctx.integration); + const integration = await createIntegrationAsync(ctx.integration); return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons); }), create: permissionRequiredProcedure diff --git a/packages/api/src/router/test/integration/integration-test-connection.spec.ts b/packages/api/src/router/test/integration/integration-test-connection.spec.ts index e692543b6..fad9e961e 100644 --- a/packages/api/src/router/test/integration/integration-test-connection.spec.ts +++ b/packages/api/src/router/test/integration/integration-test-connection.spec.ts @@ -18,11 +18,13 @@ vi.mock("@homarr/common/server", async (importActual) => { describe("testConnectionAsync should run test connection of integration", () => { test("with input of only form secrets matching api key kind it should use form apiKey", async () => { // Arrange - const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator"); + const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync"); const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); - factorySpy.mockReturnValue({ - testConnectionAsync: async () => await Promise.resolve(), - } as homarrIntegrations.PiHoleIntegration); + factorySpy.mockReturnValue( + Promise.resolve({ + testConnectionAsync: async () => await Promise.resolve(), + } as homarrIntegrations.PiHoleIntegrationV6), + ); optionsSpy.mockReturnValue([["apiKey"]]); const integration = { @@ -58,11 +60,13 @@ describe("testConnectionAsync should run test connection of integration", () => test("with input of only null form secrets and the required db secrets matching api key kind it should use db apiKey", async () => { // Arrange - const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator"); + const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync"); const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); - factorySpy.mockReturnValue({ - testConnectionAsync: async () => await Promise.resolve(), - } as homarrIntegrations.PiHoleIntegration); + factorySpy.mockReturnValue( + Promise.resolve({ + testConnectionAsync: async () => await Promise.resolve(), + } as homarrIntegrations.PiHoleIntegrationV6), + ); optionsSpy.mockReturnValue([["apiKey"]]); const integration = { @@ -105,11 +109,13 @@ describe("testConnectionAsync should run test connection of integration", () => test("with input of form and db secrets matching api key kind it should use form apiKey", async () => { // Arrange - const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator"); + const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync"); const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); - factorySpy.mockReturnValue({ - testConnectionAsync: async () => await Promise.resolve(), - } as homarrIntegrations.PiHoleIntegration); + factorySpy.mockReturnValue( + Promise.resolve({ + testConnectionAsync: async () => await Promise.resolve(), + } as homarrIntegrations.PiHoleIntegrationV6), + ); optionsSpy.mockReturnValue([["apiKey"]]); const integration = { @@ -152,11 +158,13 @@ describe("testConnectionAsync should run test connection of integration", () => test("with input of form apiKey and db secrets for username and password it should use form apiKey when both is allowed", async () => { // Arrange - const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator"); + const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync"); const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); - factorySpy.mockReturnValue({ - testConnectionAsync: async () => await Promise.resolve(), - } as homarrIntegrations.PiHoleIntegration); + factorySpy.mockReturnValue( + Promise.resolve({ + testConnectionAsync: async () => await Promise.resolve(), + } as homarrIntegrations.PiHoleIntegrationV6), + ); optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]); const integration = { @@ -203,11 +211,13 @@ describe("testConnectionAsync should run test connection of integration", () => test("with input of null form apiKey and db secrets for username and password it should use db username and password when both is allowed", async () => { // Arrange - const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator"); + const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync"); const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); - factorySpy.mockReturnValue({ - testConnectionAsync: async () => await Promise.resolve(), - } as homarrIntegrations.PiHoleIntegration); + factorySpy.mockReturnValue( + Promise.resolve({ + testConnectionAsync: async () => await Promise.resolve(), + } as homarrIntegrations.PiHoleIntegrationV6), + ); optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]); const integration = { diff --git a/packages/api/src/router/widgets/dns-hole.ts b/packages/api/src/router/widgets/dns-hole.ts index 603fe992f..38c016913 100644 --- a/packages/api/src/router/widgets/dns-hole.ts +++ b/packages/api/src/router/widgets/dns-hole.ts @@ -1,12 +1,12 @@ import { observable } from "@trpc/server/observable"; +import { z } from "zod"; 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 { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { DnsHoleSummary } from "@homarr/integrations/types"; -import { controlsInputSchema } from "@homarr/integrations/types"; import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole"; import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration"; @@ -65,7 +65,7 @@ export const dnsHoleRouter = createTRPCRouter({ enable: protectedProcedure .unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole"))) .mutation(async ({ ctx: { integration } }) => { - const client = integrationCreator(integration); + const client = await createIntegrationAsync(integration); await client.enableAsync(); const innerHandler = dnsHoleRequestHandler.handler(integration, {}); @@ -76,10 +76,14 @@ export const dnsHoleRouter = createTRPCRouter({ }), disable: protectedProcedure - .input(controlsInputSchema) + .input( + z.object({ + duration: z.number().optional(), + }), + ) .unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole"))) .mutation(async ({ ctx: { integration }, input }) => { - const client = integrationCreator(integration); + const client = await createIntegrationAsync(integration); await client.disableAsync(input.duration); const innerHandler = dnsHoleRequestHandler.handler(integration, {}); diff --git a/packages/api/src/router/widgets/downloads.ts b/packages/api/src/router/widgets/downloads.ts index c0a72d577..10eb7c021 100644 --- a/packages/api/src/router/widgets/downloads.ts +++ b/packages/api/src/router/widgets/downloads.ts @@ -6,7 +6,7 @@ import type { Integration } from "@homarr/db/schema"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; import type { DownloadClientJobsAndStatus } from "@homarr/integrations"; -import { downloadClientItemSchema, integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync, downloadClientItemSchema } from "@homarr/integrations"; import { downloadClientRequestHandler } from "@homarr/request-handler/downloads"; import type { IntegrationAction } from "../../middlewares/integration"; @@ -69,7 +69,7 @@ export const downloadsRouter = createTRPCRouter({ .mutation(async ({ ctx }) => { await Promise.all( ctx.integrations.map(async (integration) => { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); await integrationInstance.pauseQueueAsync(); }), ); @@ -80,7 +80,7 @@ export const downloadsRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { await Promise.all( ctx.integrations.map(async (integration) => { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); await integrationInstance.pauseItemAsync(input.item); }), ); @@ -90,7 +90,7 @@ export const downloadsRouter = createTRPCRouter({ .mutation(async ({ ctx }) => { await Promise.all( ctx.integrations.map(async (integration) => { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); await integrationInstance.resumeQueueAsync(); }), ); @@ -101,7 +101,7 @@ export const downloadsRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { await Promise.all( ctx.integrations.map(async (integration) => { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); await integrationInstance.resumeItemAsync(input.item); }), ); @@ -112,7 +112,7 @@ export const downloadsRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { await Promise.all( ctx.integrations.map(async (integration) => { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); await integrationInstance.deleteItemAsync(input.item, input.fromDisk); }), ); diff --git a/packages/api/src/router/widgets/indexer-manager.ts b/packages/api/src/router/widgets/indexer-manager.ts index 2bbf1356d..74bffb2af 100644 --- a/packages/api/src/router/widgets/indexer-manager.ts +++ b/packages/api/src/router/widgets/indexer-manager.ts @@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server"; import { observable } from "@trpc/server/observable"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { Indexer } from "@homarr/integrations/types"; import { logger } from "@homarr/log"; import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-manager"; @@ -59,7 +59,7 @@ export const indexerManagerRouter = createTRPCRouter({ .mutation(async ({ ctx }) => { await Promise.all( ctx.integrations.map(async (integration) => { - const client = integrationCreator(integration); + const client = await createIntegrationAsync(integration); await client.testAllAsync().catch((err) => { logger.error("indexer-manager router - ", err); throw new TRPCError({ diff --git a/packages/api/src/router/widgets/media-requests.ts b/packages/api/src/router/widgets/media-requests.ts index 8469b3b31..4af50a583 100644 --- a/packages/api/src/router/widgets/media-requests.ts +++ b/packages/api/src/router/widgets/media-requests.ts @@ -2,7 +2,7 @@ import { observable } from "@trpc/server/observable"; import { z } from "zod"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import { integrationCreator, MediaRequestStatus } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { MediaRequest } from "@homarr/integrations/types"; import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list"; import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats"; @@ -30,14 +30,12 @@ export const mediaRequestsRouter = createTRPCRouter({ ); return results .flatMap(({ data, integration }) => data.map((request) => ({ ...request, integrationId: integration.id }))) - .sort(({ status: statusA }, { status: statusB }) => { - if (statusA === MediaRequestStatus.PendingApproval) { - return -1; + .sort((dataA, dataB) => { + if (dataA.status === dataB.status) { + return dataB.createdAt.getTime() - dataA.createdAt.getTime(); } - if (statusB === MediaRequestStatus.PendingApproval) { - return 1; - } - return 0; + + return dataA.status - dataB.status; }); }), subscribeToLatestRequests: publicProcedure @@ -96,7 +94,7 @@ export const mediaRequestsRouter = createTRPCRouter({ .unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("mediaRequest"))) .input(z.object({ requestId: z.number(), answer: z.enum(["approve", "decline"]) })) .mutation(async ({ ctx: { integration }, input }) => { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); const innerHandler = mediaRequestListRequestHandler.handler(integration, {}); if (input.answer === "approve") { diff --git a/packages/api/src/router/widgets/smart-home.ts b/packages/api/src/router/widgets/smart-home.ts index 13979ad28..2294bf6bb 100644 --- a/packages/api/src/router/widgets/smart-home.ts +++ b/packages/api/src/router/widgets/smart-home.ts @@ -2,7 +2,7 @@ import { observable } from "@trpc/server/observable"; import { z } from "zod"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import { smartHomeEntityStateRequestHandler } from "@homarr/request-handler/smart-home-entity-state"; import type { IntegrationAction } from "../../middlewares/integration"; @@ -45,7 +45,7 @@ export const smartHomeRouter = createTRPCRouter({ .unstable_concat(createSmartHomeIntegrationMiddleware("interact")) .input(z.object({ entityId: z.string() })) .mutation(async ({ ctx: { integration }, input }) => { - const client = integrationCreator(integration); + const client = await createIntegrationAsync(integration); const success = await client.triggerToggleAsync(input.entityId); const innerHandler = smartHomeEntityStateRequestHandler.handler(integration, { entityId: input.entityId }); @@ -57,7 +57,7 @@ export const smartHomeRouter = createTRPCRouter({ .unstable_concat(createSmartHomeIntegrationMiddleware("interact")) .input(z.object({ automationId: z.string() })) .mutation(async ({ ctx: { integration }, input }) => { - const client = integrationCreator(integration); + const client = await createIntegrationAsync(integration); await client.triggerAutomationAsync(input.automationId); }), }); diff --git a/packages/auth/package.json b/packages/auth/package.json index c25085c3a..73a6210c9 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -2,7 +2,7 @@ "name": "@homarr/auth", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", @@ -25,6 +25,7 @@ "dependencies": { "@auth/core": "^0.38.0", "@auth/drizzle-adapter": "^1.8.0", + "@homarr/certificates": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", @@ -48,7 +49,7 @@ "@types/bcrypt": "5.0.2", "@types/cookies": "0.9.0", "eslint": "^9.21.0", - "prettier": "^3.5.2", + "prettier": "^3.5.3", "typescript": "^5.8.2" } } diff --git a/packages/auth/providers/oidc/oidc-provider.ts b/packages/auth/providers/oidc/oidc-provider.ts index 82328b3ff..d03d9c736 100644 --- a/packages/auth/providers/oidc/oidc-provider.ts +++ b/packages/auth/providers/oidc/oidc-provider.ts @@ -1,6 +1,9 @@ import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers"; import type { OIDCConfig } from "@auth/core/providers"; import type { Profile } from "@auth/core/types"; +import { customFetch } from "next-auth"; + +import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { env } from "../../env"; import { createRedirectUri } from "../../redirect"; @@ -35,6 +38,10 @@ export const OidcProvider = (headers: ReadonlyHeaders | null): OIDCConfig { diff --git a/packages/auth/test/events.spec.ts b/packages/auth/test/events.spec.ts index 0fe01ddf1..ad6178ab2 100644 --- a/packages/auth/test/events.spec.ts +++ b/packages/auth/test/events.spec.ts @@ -11,6 +11,7 @@ import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions"; import { createSignInEventHandler } from "../events"; +vi.mock("next-auth", () => ({})); vi.mock("../env", () => { return { env: { diff --git a/packages/boards/package.json b/packages/boards/package.json index 6de9b1d10..ba39d8e39 100644 --- a/packages/boards/package.json +++ b/packages/boards/package.json @@ -2,7 +2,7 @@ "name": "@homarr/boards", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { "./context": "./src/context.tsx", diff --git a/packages/certificates/package.json b/packages/certificates/package.json index 054cd66d1..1e81cfe26 100644 --- a/packages/certificates/package.json +++ b/packages/certificates/package.json @@ -2,7 +2,7 @@ "name": "@homarr/certificates", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { "./server": "./src/server.ts" diff --git a/packages/cli/package.json b/packages/cli/package.json index dfc006419..6c9201081 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -2,7 +2,7 @@ "name": "@homarr/cli", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts" diff --git a/packages/common/package.json b/packages/common/package.json index 1ebf6cec1..89793d33a 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -2,7 +2,7 @@ "name": "@homarr/common", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", diff --git a/packages/cron-job-runner/package.json b/packages/cron-job-runner/package.json index 8fa1073c7..f2aab14c1 100644 --- a/packages/cron-job-runner/package.json +++ b/packages/cron-job-runner/package.json @@ -2,7 +2,7 @@ "name": "@homarr/cron-job-runner", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts" diff --git a/packages/cron-job-status/package.json b/packages/cron-job-status/package.json index 1599256cc..549ad16cb 100644 --- a/packages/cron-job-status/package.json +++ b/packages/cron-job-status/package.json @@ -2,7 +2,7 @@ "name": "@homarr/cron-job-status", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", diff --git a/packages/cron-jobs-core/package.json b/packages/cron-jobs-core/package.json index 77bc842f0..cb9791368 100644 --- a/packages/cron-jobs-core/package.json +++ b/packages/cron-jobs-core/package.json @@ -2,7 +2,7 @@ "name": "@homarr/cron-jobs-core", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", diff --git a/packages/cron-jobs-core/src/creator.ts b/packages/cron-jobs-core/src/creator.ts index 3987ad82f..56b92c34a 100644 --- a/packages/cron-jobs-core/src/creator.ts +++ b/packages/cron-jobs-core/src/creator.ts @@ -1,3 +1,4 @@ +import { AxiosError } from "axios"; import cron from "node-cron"; import { Stopwatch } from "@homarr/common"; @@ -48,8 +49,15 @@ const createCallback = ({ dataType() { @@ -388,6 +393,7 @@ export const sections = mysqlTable("section", { xOffset: int(), yOffset: int(), name: text(), + options: text().default(emptySuperJSON), }); export const sectionCollapseStates = mysqlTable( @@ -414,8 +420,8 @@ export const items = mysqlTable("item", { .notNull() .references(() => boards.id, { onDelete: "cascade" }), kind: text().$type().notNull(), - options: text().default('{"json": {}}').notNull(), // empty superjson object - advancedOptions: text().default('{"json": {}}').notNull(), // empty superjson object + options: text().default(emptySuperJSON).notNull(), + advancedOptions: text().default(emptySuperJSON).notNull(), }); export const apps = mysqlTable("app", { @@ -461,7 +467,7 @@ export const iconRepositories = mysqlTable("iconRepository", { export const serverSettings = mysqlTable("serverSetting", { settingKey: varchar({ length: 64 }).notNull().unique().primaryKey(), - value: text().default('{"json": {}}').notNull(), // empty superjson object + value: text().default(emptySuperJSON).notNull(), }); export const apiKeyRelations = relations(apiKeys, ({ one }) => ({ diff --git a/packages/db/schema/sqlite.ts b/packages/db/schema/sqlite.ts index 2d5a4a8e3..e1b4ad124 100644 --- a/packages/db/schema/sqlite.ts +++ b/packages/db/schema/sqlite.ts @@ -5,7 +5,12 @@ import { relations, sql } from "drizzle-orm"; import type { AnySQLiteColumn } from "drizzle-orm/sqlite-core"; import { blob, index, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"; -import { backgroundImageAttachments, backgroundImageRepeats, backgroundImageSizes } from "@homarr/definitions"; +import { + backgroundImageAttachments, + backgroundImageRepeats, + backgroundImageSizes, + emptySuperJSON, +} from "@homarr/definitions"; import type { BackgroundImageAttachment, BackgroundImageRepeat, @@ -373,6 +378,7 @@ export const sections = sqliteTable("section", { xOffset: int(), yOffset: int(), name: text(), + options: text().default(emptySuperJSON), }); export const sectionCollapseStates = sqliteTable( @@ -399,8 +405,8 @@ export const items = sqliteTable("item", { .notNull() .references(() => boards.id, { onDelete: "cascade" }), kind: text().$type().notNull(), - options: text().default('{"json": {}}').notNull(), // empty superjson object - advancedOptions: text().default('{"json": {}}').notNull(), // empty superjson object + options: text().default(emptySuperJSON).notNull(), + advancedOptions: text().default(emptySuperJSON).notNull(), }); export const apps = sqliteTable("app", { @@ -446,7 +452,7 @@ export const iconRepositories = sqliteTable("iconRepository", { export const serverSettings = sqliteTable("serverSetting", { settingKey: text().notNull().unique().primaryKey(), - value: text().default('{"json": {}}').notNull(), // empty superjson object + value: text().default(emptySuperJSON).notNull(), }); export const apiKeyRelations = relations(apiKeys, ({ one }) => ({ diff --git a/packages/definitions/package.json b/packages/definitions/package.json index 191f0f363..6ea5961fc 100644 --- a/packages/definitions/package.json +++ b/packages/definitions/package.json @@ -2,7 +2,7 @@ "name": "@homarr/definitions", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts" diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts index 006cdecde..f05c9c63f 100644 --- a/packages/definitions/src/docs/homarr-docs-sitemap.ts +++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts @@ -88,6 +88,7 @@ export type HomarrDocumentationPath = | "/docs/tags/integrations" | "/docs/tags/interface" | "/docs/tags/jellyserr" + | "/docs/tags/layout" | "/docs/tags/ldap" | "/docs/tags/links" | "/docs/tags/lists" @@ -110,6 +111,7 @@ export type HomarrDocumentationPath = | "/docs/tags/proxmox" | "/docs/tags/proxy" | "/docs/tags/puid" + | "/docs/tags/responsive" | "/docs/tags/roles" | "/docs/tags/rss" | "/docs/tags/search" diff --git a/packages/definitions/src/emptysuperjson.ts b/packages/definitions/src/emptysuperjson.ts new file mode 100644 index 000000000..4c4b95636 --- /dev/null +++ b/packages/definitions/src/emptysuperjson.ts @@ -0,0 +1 @@ +export const emptySuperJSON = '{"json": {}}'; diff --git a/packages/definitions/src/index.ts b/packages/definitions/src/index.ts index 13cc03dd5..45f655491 100644 --- a/packages/definitions/src/index.ts +++ b/packages/definitions/src/index.ts @@ -11,3 +11,4 @@ export * from "./docs"; export * from "./cookie"; export * from "./search-engine"; export * from "./onboarding"; +export * from "./emptysuperjson"; diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index 9ed2af4ac..e4bda472a 100644 --- a/packages/definitions/src/integration.ts +++ b/packages/definitions/src/integration.ts @@ -85,6 +85,12 @@ export const integrationDefs = { iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg", category: ["mediaService"], }, + emby: { + name: "Emby", + secretKinds: [["apiKey"]], + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/emby.svg", + category: ["mediaService"], + }, plex: { name: "Plex", secretKinds: [["apiKey"]], diff --git a/packages/docker/package.json b/packages/docker/package.json index 860039aed..b3631cbcf 100644 --- a/packages/docker/package.json +++ b/packages/docker/package.json @@ -2,7 +2,7 @@ "name": "@homarr/docker", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", diff --git a/packages/env/package.json b/packages/env/package.json index f329e74d5..998352fa7 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -2,7 +2,7 @@ "name": "@homarr/env", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", diff --git a/packages/form/package.json b/packages/form/package.json index 755423671..2ee644486 100644 --- a/packages/form/package.json +++ b/packages/form/package.json @@ -2,7 +2,7 @@ "name": "@homarr/form", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", @@ -26,7 +26,7 @@ "@homarr/common": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/form": "^7.17.0", + "@mantine/form": "^7.17.1", "zod": "^3.24.2" }, "devDependencies": { diff --git a/packages/forms-collection/package.json b/packages/forms-collection/package.json index 8ff76f44d..ba4b7cdb7 100644 --- a/packages/forms-collection/package.json +++ b/packages/forms-collection/package.json @@ -2,7 +2,7 @@ "name": "@homarr/forms-collection", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts" @@ -29,7 +29,7 @@ "@homarr/notifications": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/core": "^7.17.0", + "@mantine/core": "^7.17.1", "react": "19.0.0", "zod": "^3.24.2" }, diff --git a/packages/icons/package.json b/packages/icons/package.json index c974908e3..e41eeda98 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -2,7 +2,7 @@ "name": "@homarr/icons", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 421e84966..e93291dce 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -2,7 +2,7 @@ "name": "@homarr/integrations", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", diff --git a/packages/integrations/src/base/creator.ts b/packages/integrations/src/base/creator.ts index ecf184ba8..21df2a7e9 100644 --- a/packages/integrations/src/base/creator.ts +++ b/packages/integrations/src/base/creator.ts @@ -10,6 +10,7 @@ import { NzbGetIntegration } from "../download-client/nzbget/nzbget-integration" import { QBitTorrentIntegration } from "../download-client/qbittorrent/qbittorrent-integration"; import { SabnzbdIntegration } from "../download-client/sabnzbd/sabnzbd-integration"; import { TransmissionIntegration } from "../download-client/transmission/transmission-integration"; +import { EmbyIntegration } from "../emby/emby-integration"; import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration"; import { JellyfinIntegration } from "../jellyfin/jellyfin-integration"; import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration"; @@ -20,13 +21,13 @@ import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration" import { TdarrIntegration } from "../media-transcoding/tdarr-integration"; import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration"; import { OverseerrIntegration } from "../overseerr/overseerr-integration"; -import { PiHoleIntegration } from "../pi-hole/pi-hole-integration"; +import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-factory"; import { PlexIntegration } from "../plex/plex-integration"; import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration"; import { ProxmoxIntegration } from "../proxmox/proxmox-integration"; import type { Integration, IntegrationInput } from "./integration"; -export const integrationCreator = ( +export const createIntegrationAsync = async ( integration: IntegrationInput & { kind: TKind }, ) => { if (!(integration.kind in integrationCreators)) { @@ -35,15 +36,22 @@ export const integrationCreator = ; + const creator = integrationCreators[integration.kind]; + + // factories are an array, to differentiate in js between class constructors and functions + if (Array.isArray(creator)) { + return (await creator[0](integration)) as IntegrationInstanceOfKind; + } + + return new creator(integration) as IntegrationInstanceOfKind; }; -export const integrationCreatorFromSecrets = ( +export const createIntegrationAsyncFromSecrets = ( integration: Modify & { secrets: { kind: IntegrationSecretKind; value: `${string}.${string}` }[]; }, ) => { - return integrationCreator({ + return createIntegrationAsync({ ...integration, decryptedSecrets: integration.secrets.map((secret) => ({ ...secret, @@ -52,8 +60,11 @@ export const integrationCreatorFromSecrets = Integration; + +// factories are an array, to differentiate in js between class constructors and functions export const integrationCreators = { - piHole: PiHoleIntegration, + piHole: [createPiHoleIntegrationAsync], adGuardHome: AdGuardHomeIntegration, homeAssistant: HomeAssistantIntegration, jellyfin: JellyfinIntegration, @@ -74,4 +85,13 @@ export const integrationCreators = { dashDot: DashDotIntegration, tdarr: TdarrIntegration, proxmox: ProxmoxIntegration, -} satisfies Record Integration>; + emby: EmbyIntegration, +} satisfies Record Promise]>; + +type IntegrationInstanceOfKind = { + [kind in TKind]: (typeof integrationCreators)[kind] extends [(input: IntegrationInput) => Promise] + ? Awaited> + : (typeof integrationCreators)[kind] extends IntegrationInstance + ? InstanceType<(typeof integrationCreators)[kind]> + : never; +}[TKind]; diff --git a/packages/integrations/src/base/error.ts b/packages/integrations/src/base/error.ts new file mode 100644 index 000000000..46fe913cf --- /dev/null +++ b/packages/integrations/src/base/error.ts @@ -0,0 +1,47 @@ +import type { Response as UndiciResponse } from "undici"; +import type { z } from "zod"; + +import type { IntegrationInput } from "./integration"; + +export class ParseError extends Error { + public readonly zodError: z.ZodError; + public readonly input: unknown; + + constructor(dataName: string, zodError: z.ZodError, input?: unknown) { + super(`Failed to parse ${dataName}`); + this.zodError = zodError; + this.input = input; + } +} + +export class ResponseError extends Error { + public readonly statusCode: number; + public readonly url: string; + public readonly content?: string; + + constructor(response: Response | UndiciResponse, content: unknown) { + super("Response failed"); + + this.statusCode = response.status; + this.url = response.url; + + try { + this.content = JSON.stringify(content); + } catch { + this.content = content as string; + } + } +} + +export class IntegrationResponseError extends ResponseError { + public readonly integration: Pick; + + constructor(integration: IntegrationInput, response: Response | UndiciResponse, content: unknown) { + super(response, content); + this.integration = { + id: integration.id, + name: integration.name, + url: integration.url, + }; + } +} diff --git a/packages/integrations/src/base/session-store.ts b/packages/integrations/src/base/session-store.ts new file mode 100644 index 000000000..58e4dad46 --- /dev/null +++ b/packages/integrations/src/base/session-store.ts @@ -0,0 +1,40 @@ +import superjson from "superjson"; + +import { decryptSecret, encryptSecret } from "@homarr/common/server"; +import { logger } from "@homarr/log"; +import { createGetSetChannel } from "@homarr/redis"; + +const localLogger = logger.child({ module: "SessionStore" }); + +export const createSessionStore = (integration: { id: string }) => { + const channelName = `session-store:${integration.id}`; + const channel = createGetSetChannel<`${string}.${string}`>(channelName); + + return { + async getAsync() { + localLogger.debug("Getting session from store", { store: channelName }); + const value = await channel.getAsync(); + if (!value) return null; + try { + return superjson.parse(decryptSecret(value)); + } catch (error) { + localLogger.warn("Failed to load session", { store: channelName, error }); + return null; + } + }, + async setAsync(value: TValue) { + localLogger.debug("Updating session in store", { store: channelName }); + try { + await channel.setAsync(encryptSecret(superjson.stringify(value))); + } catch (error) { + localLogger.error("Failed to save session", { store: channelName, error }); + } + }, + async clearAsync() { + localLogger.debug("Cleared session in store", { store: channelName }); + await channel.removeAsync(); + }, + }; +}; + +export type SessionStore = ReturnType>; diff --git a/packages/integrations/src/emby/emby-integration.ts b/packages/integrations/src/emby/emby-integration.ts new file mode 100644 index 000000000..911901243 --- /dev/null +++ b/packages/integrations/src/emby/emby-integration.ts @@ -0,0 +1,98 @@ +import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client/models"; +import { z } from "zod"; + +import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; + +import { Integration } from "../base/integration"; +import type { StreamSession } from "../interfaces/media-server/session"; +import { convertJellyfinType } from "../jellyfin/jellyfin-integration"; + +const sessionSchema = z.object({ + NowPlayingItem: z + .object({ + Type: z.nativeEnum(BaseItemKind).optional(), + SeriesName: z.string().nullish(), + Name: z.string().nullish(), + SeasonName: z.string().nullish(), + EpisodeTitle: z.string().nullish(), + Album: z.string().nullish(), + EpisodeCount: z.number().nullish(), + }) + .optional(), + Id: z.string(), + Client: z.string().nullish(), + DeviceId: z.string().nullish(), + DeviceName: z.string().nullish(), + UserId: z.string().optional(), + UserName: z.string().nullish(), +}); + +export class EmbyIntegration extends Integration { + private static readonly apiKeyHeader = "X-Emby-Token"; + private static readonly deviceId = "homarr-emby-integration"; + private static readonly authorizationHeaderValue = `Emby Client="Dashboard", Device="Homarr", DeviceId="${EmbyIntegration.deviceId}", Version="0.0.1"`; + + public async testConnectionAsync(): Promise { + const apiKey = super.getSecretValue("apiKey"); + + await super.handleTestConnectionResponseAsync({ + queryFunctionAsync: async () => { + return await fetchWithTrustedCertificatesAsync(super.url("/emby/System/Ping"), { + headers: { + [EmbyIntegration.apiKeyHeader]: apiKey, + Authorization: EmbyIntegration.authorizationHeaderValue, + }, + }); + }, + }); + } + + public async getCurrentSessionsAsync(): Promise { + const apiKey = super.getSecretValue("apiKey"); + const response = await fetchWithTrustedCertificatesAsync(super.url("/emby/Sessions"), { + headers: { + [EmbyIntegration.apiKeyHeader]: apiKey, + Authorization: EmbyIntegration.authorizationHeaderValue, + }, + }); + + if (!response.ok) { + throw new Error(`Emby server ${this.integration.id} returned a non successful status code: ${response.status}`); + } + + const result = z.array(sessionSchema).safeParse(await response.json()); + + if (!result.success) { + throw new Error(`Emby server ${this.integration.id} returned an unexpected response: ${result.error.message}`); + } + + return result.data + .filter((sessionInfo) => sessionInfo.UserId !== undefined) + .filter((sessionInfo) => sessionInfo.DeviceId !== EmbyIntegration.deviceId) + .map((sessionInfo): StreamSession => { + let currentlyPlaying: StreamSession["currentlyPlaying"] | null = null; + + if (sessionInfo.NowPlayingItem) { + currentlyPlaying = { + type: convertJellyfinType(sessionInfo.NowPlayingItem.Type), + name: sessionInfo.NowPlayingItem.SeriesName ?? sessionInfo.NowPlayingItem.Name ?? "", + seasonName: sessionInfo.NowPlayingItem.SeasonName ?? "", + episodeName: sessionInfo.NowPlayingItem.EpisodeTitle, + albumName: sessionInfo.NowPlayingItem.Album ?? "", + episodeCount: sessionInfo.NowPlayingItem.EpisodeCount, + }; + } + + return { + sessionId: `${sessionInfo.Id}`, + sessionName: `${sessionInfo.Client} (${sessionInfo.DeviceName})`, + user: { + profilePictureUrl: super.url(`/Users/${sessionInfo.UserId}/Images/Primary`).toString(), + userId: sessionInfo.UserId ?? "", + username: sessionInfo.UserName ?? "", + }, + currentlyPlaying, + }; + }); + } +} diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index baa5a99b0..8a52261c3 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -13,7 +13,8 @@ export { RadarrIntegration } from "./media-organizer/radarr/radarr-integration"; export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration"; export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration"; export { OverseerrIntegration } from "./overseerr/overseerr-integration"; -export { PiHoleIntegration } from "./pi-hole/pi-hole-integration"; +export { PiHoleIntegrationV5 } from "./pi-hole/v5/pi-hole-integration-v5"; +export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6"; export { PlexIntegration } from "./plex/plex-integration"; export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration"; export { LidarrIntegration } from "./media-organizer/lidarr/lidarr-integration"; @@ -36,5 +37,5 @@ export type { TdarrWorker } from "./interfaces/media-transcoding/workers"; export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items"; // Helpers -export { integrationCreator, integrationCreatorFromSecrets } from "./base/creator"; +export { createIntegrationAsync, createIntegrationAsyncFromSecrets } from "./base/creator"; export { IntegrationTestConnectionError } from "./base/test-connection-error"; diff --git a/packages/integrations/src/interfaces/dns-hole-summary/dns-hole-summary-integration.ts b/packages/integrations/src/interfaces/dns-hole-summary/dns-hole-summary-integration.ts index d25be7fd0..0d0d189ac 100644 --- a/packages/integrations/src/interfaces/dns-hole-summary/dns-hole-summary-integration.ts +++ b/packages/integrations/src/interfaces/dns-hole-summary/dns-hole-summary-integration.ts @@ -2,4 +2,6 @@ import type { DnsHoleSummary } from "./dns-hole-summary-types"; export interface DnsHoleSummaryIntegration { getSummaryAsync(): Promise; + enableAsync(): Promise; + disableAsync(duration?: number): Promise; } diff --git a/packages/integrations/src/jellyfin/jellyfin-integration.ts b/packages/integrations/src/jellyfin/jellyfin-integration.ts index 2e40391cb..29a90c549 100644 --- a/packages/integrations/src/jellyfin/jellyfin-integration.ts +++ b/packages/integrations/src/jellyfin/jellyfin-integration.ts @@ -1,4 +1,5 @@ import { Jellyfin } from "@jellyfin/sdk"; +import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client/models"; import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api"; import { getSystemApi } from "@jellyfin/sdk/lib/utils/api/system-api"; @@ -34,31 +35,34 @@ export class JellyfinIntegration extends Integration { throw new Error(`Jellyfin server ${this.url("/")} returned a non successful status code: ${sessions.status}`); } - return sessions.data.map((sessionInfo): StreamSession => { - let nowPlaying: StreamSession["currentlyPlaying"] | null = null; + return sessions.data + .filter((sessionInfo) => sessionInfo.UserId !== undefined) + .filter((sessionInfo) => sessionInfo.DeviceId !== "homarr") + .map((sessionInfo): StreamSession => { + let currentlyPlaying: StreamSession["currentlyPlaying"] | null = null; - if (sessionInfo.NowPlayingItem) { - nowPlaying = { - type: "tv", - name: sessionInfo.NowPlayingItem.Name ?? "", - seasonName: sessionInfo.NowPlayingItem.SeasonName ?? "", - episodeName: sessionInfo.NowPlayingItem.EpisodeTitle, - albumName: sessionInfo.NowPlayingItem.Album ?? "", - episodeCount: sessionInfo.NowPlayingItem.EpisodeCount, + if (sessionInfo.NowPlayingItem) { + currentlyPlaying = { + type: convertJellyfinType(sessionInfo.NowPlayingItem.Type), + name: sessionInfo.NowPlayingItem.SeriesName ?? sessionInfo.NowPlayingItem.Name ?? "", + seasonName: sessionInfo.NowPlayingItem.SeasonName ?? "", + episodeName: sessionInfo.NowPlayingItem.EpisodeTitle, + albumName: sessionInfo.NowPlayingItem.Album ?? "", + episodeCount: sessionInfo.NowPlayingItem.EpisodeCount, + }; + } + + return { + sessionId: `${sessionInfo.Id}`, + sessionName: `${sessionInfo.Client} (${sessionInfo.DeviceName})`, + user: { + profilePictureUrl: this.url(`/Users/${sessionInfo.UserId}/Images/Primary`).toString(), + userId: sessionInfo.UserId ?? "", + username: sessionInfo.UserName ?? "", + }, + currentlyPlaying, }; - } - - return { - sessionId: `${sessionInfo.Id}`, - sessionName: `${sessionInfo.Client} (${sessionInfo.DeviceName})`, - user: { - profilePictureUrl: this.url(`/Users/${sessionInfo.UserId}/Images/Primary`).toString(), - userId: sessionInfo.UserId ?? "", - username: sessionInfo.UserName ?? "", - }, - currentlyPlaying: nowPlaying, - }; - }); + }); } /** @@ -81,3 +85,24 @@ export class JellyfinIntegration extends Integration { return apiClient; } } + +export const convertJellyfinType = ( + kind: BaseItemKind | undefined, +): Exclude["type"] => { + switch (kind) { + case BaseItemKind.Audio: + case BaseItemKind.MusicVideo: + return "audio"; + case BaseItemKind.Episode: + case BaseItemKind.Video: + return "video"; + case BaseItemKind.Movie: + return "movie"; + case BaseItemKind.TvChannel: + case BaseItemKind.TvProgram: + case BaseItemKind.LiveTvChannel: + case BaseItemKind.LiveTvProgram: + default: + return "tv"; + } +}; diff --git a/packages/integrations/src/openmediavault/openmediavault-integration.ts b/packages/integrations/src/openmediavault/openmediavault-integration.ts index c639d3377..09f718954 100644 --- a/packages/integrations/src/openmediavault/openmediavault-integration.ts +++ b/packages/integrations/src/openmediavault/openmediavault-integration.ts @@ -1,53 +1,40 @@ -import type { Response } from "undici"; +import type { Headers, HeadersInit, Response as UndiciResponse } from "undici"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { logger } from "@homarr/log"; +import { ResponseError } from "../base/error"; +import type { IntegrationInput } from "../base/integration"; import { Integration } from "../base/integration"; +import type { SessionStore } from "../base/session-store"; +import { createSessionStore } from "../base/session-store"; import { IntegrationTestConnectionError } from "../base/test-connection-error"; import type { HealthMonitoring } from "../types"; import { cpuTempSchema, fileSystemSchema, smartSchema, systemInformationSchema } from "./openmediavault-types"; +const localLogger = logger.child({ module: "OpenMediaVaultIntegration" }); + +type SessionStoreValue = + | { type: "header"; sessionId: string } + | { type: "cookie"; loginToken: string; sessionId: string }; + export class OpenMediaVaultIntegration extends Integration { - static extractSessionIdFromCookies(headers: Headers): string { - const cookies = headers.get("set-cookie") ?? ""; - const sessionId = cookies - .split(";") - .find((cookie) => cookie.includes("X-OPENMEDIAVAULT-SESSIONID") || cookie.includes("OPENMEDIAVAULT-SESSIONID")); + private readonly sessionStore: SessionStore; - if (sessionId) { - return sessionId; - } else { - throw new Error("Session ID not found in cookies"); - } - } - - static extractLoginTokenFromCookies(headers: Headers): string { - const cookies = headers.get("set-cookie") ?? ""; - const loginToken = cookies - .split(";") - .find((cookie) => cookie.includes("X-OPENMEDIAVAULT-LOGIN") || cookie.includes("OPENMEDIAVAULT-LOGIN")); - - if (loginToken) { - return loginToken; - } else { - throw new Error("Login token not found in cookies"); - } + constructor(integration: IntegrationInput) { + super(integration); + this.sessionStore = createSessionStore(integration); } public async getSystemInfoAsync(): Promise { - if (!this.headers) { - await this.authenticateAndConstructSessionInHeaderAsync(); - } - - const systemResponses = await this.makeOpenMediaVaultRPCCallAsync("system", "getInformation", {}, this.headers); - const fileSystemResponse = await this.makeOpenMediaVaultRPCCallAsync( + const systemResponses = await this.makeAuthenticatedRpcCallAsync("system", "getInformation"); + const fileSystemResponse = await this.makeAuthenticatedRpcCallAsync( "filesystemmgmt", "enumerateMountedFilesystems", { includeroot: true }, - this.headers, ); - const smartResponse = await this.makeOpenMediaVaultRPCCallAsync("smart", "enumerateDevices", {}, this.headers); - const cpuTempResponse = await this.makeOpenMediaVaultRPCCallAsync("cputemp", "get", {}, this.headers); + const smartResponse = await this.makeAuthenticatedRpcCallAsync("smart", "enumerateDevices"); + const cpuTempResponse = await this.makeAuthenticatedRpcCallAsync("cputemp", "get"); const systemResult = systemInformationSchema.safeParse(await systemResponses.json()); const fileSystemResult = fileSystemSchema.safeParse(await fileSystemResponse.json()); @@ -98,30 +85,43 @@ export class OpenMediaVaultIntegration extends Integration { } public async testConnectionAsync(): Promise { - const response = await this.makeOpenMediaVaultRPCCallAsync("session", "login", { - username: this.getSecretValue("username"), - password: this.getSecretValue("password"), + await this.getSessionAsync().catch((error) => { + if (error instanceof ResponseError) { + throw new IntegrationTestConnectionError("invalidCredentials"); + } }); - - if (!response.ok) { - throw new IntegrationTestConnectionError("invalidCredentials"); - } - const result = await response.json(); - if (typeof result !== "object" || result === null || !("response" in result)) { - throw new IntegrationTestConnectionError("invalidJson"); - } } - private async makeOpenMediaVaultRPCCallAsync( + private async makeAuthenticatedRpcCallAsync( serviceName: string, method: string, - params: Record, - headers: Record = {}, - ): Promise { + params: Record = {}, + ): Promise { + return await this.withAuthAsync(async (session) => { + const headers: HeadersInit = + session.type === "cookie" + ? { + Cookie: `${session.loginToken};${session.sessionId}`, + } + : { + "X-OPENMEDIAVAULT-SESSIONID": session.sessionId, + }; + + return await this.makeRpcCallAsync(serviceName, method, params, headers); + }); + } + + private async makeRpcCallAsync( + serviceName: string, + method: string, + params: Record = {}, + headers: HeadersInit = {}, + ): Promise { return await fetchWithTrustedCertificatesAsync(this.url("/rpc.php"), { method: "POST", headers: { "Content-Type": "application/json", + "User-Agent": "Homarr", ...headers, }, body: JSON.stringify({ @@ -132,25 +132,79 @@ export class OpenMediaVaultIntegration extends Integration { }); } - private headers: Record | undefined = undefined; + /** + * Run the callback with the current session id + * @param callback + * @returns + */ + private async withAuthAsync(callback: (session: SessionStoreValue) => Promise) { + const storedSession = await this.sessionStore.getAsync(); - private async authenticateAndConstructSessionInHeaderAsync() { - const authResponse = await this.makeOpenMediaVaultRPCCallAsync("session", "login", { + if (storedSession) { + localLogger.debug("Using stored session for request", { integrationId: this.integration.id }); + const response = await callback(storedSession); + if (response.status !== 401) { + return response; + } + + localLogger.info("Session expired, getting new session", { integrationId: this.integration.id }); + } + + const session = await this.getSessionAsync(); + await this.sessionStore.setAsync(session); + return await callback(session); + } + + /** + * Get a session id from the openmediavault server + * @returns The session details + */ + private async getSessionAsync(): Promise { + const response = await this.makeRpcCallAsync("session", "login", { username: this.getSecretValue("username"), password: this.getSecretValue("password"), }); - const authResult = (await authResponse.json()) as Response; - const response = (authResult as { response?: { sessionid?: string } }).response; - let sessionId; - const headers: Record = {}; - if (response?.sessionid) { - sessionId = response.sessionid; - headers["X-OPENMEDIAVAULT-SESSIONID"] = sessionId; + + const data = (await response.json()) as { response?: { sessionid?: string } }; + if (data.response?.sessionid) { + return { + type: "header", + sessionId: data.response.sessionid, + }; } else { - sessionId = OpenMediaVaultIntegration.extractSessionIdFromCookies(authResponse.headers); - const loginToken = OpenMediaVaultIntegration.extractLoginTokenFromCookies(authResponse.headers); - headers.Cookie = `${loginToken};${sessionId}`; + const sessionId = OpenMediaVaultIntegration.extractSessionIdFromCookies(response.headers); + const loginToken = OpenMediaVaultIntegration.extractLoginTokenFromCookies(response.headers); + + if (!sessionId || !loginToken) { + throw new ResponseError( + response, + `${JSON.stringify(data)} - sessionId=${"*".repeat(sessionId?.length ?? 0)} loginToken=${"*".repeat(loginToken?.length ?? 0)}`, + ); + } + + return { + type: "cookie", + loginToken, + sessionId, + }; } - this.headers = headers; + } + + private static extractSessionIdFromCookies(headers: Headers): string | null { + const cookies = headers.getSetCookie(); + const sessionId = cookies.find( + (cookie) => cookie.includes("X-OPENMEDIAVAULT-SESSIONID") || cookie.includes("OPENMEDIAVAULT-SESSIONID"), + ); + + return sessionId ?? null; + } + + private static extractLoginTokenFromCookies(headers: Headers): string | null { + const cookies = headers.getSetCookie(); + const loginToken = cookies.find( + (cookie) => cookie.includes("X-OPENMEDIAVAULT-LOGIN") || cookie.includes("OPENMEDIAVAULT-LOGIN"), + ); + + return loginToken ?? null; } } diff --git a/packages/integrations/src/pi-hole/pi-hole-integration-factory.ts b/packages/integrations/src/pi-hole/pi-hole-integration-factory.ts new file mode 100644 index 000000000..7970d7069 --- /dev/null +++ b/packages/integrations/src/pi-hole/pi-hole-integration-factory.ts @@ -0,0 +1,22 @@ +import { removeTrailingSlash } from "@homarr/common"; + +import type { IntegrationInput } from "../base/integration"; +import { PiHoleIntegrationV5 } from "./v5/pi-hole-integration-v5"; +import { PiHoleIntegrationV6 } from "./v6/pi-hole-integration-v6"; + +export const createPiHoleIntegrationAsync = async (input: IntegrationInput) => { + const baseUrl = removeTrailingSlash(input.url); + const url = new URL(`${baseUrl}/api/info/version`); + const response = await fetch(url); + + /** + * In pi-hole 5 the api was at /admin/api.php, in pi-hole 6 it was moved to /api + * For the /api/info/version endpoint, the response is 404 in pi-hole 5 + * and 401 in pi-hole 6 + */ + if (response.status === 404) { + return new PiHoleIntegrationV5(input); + } + + return new PiHoleIntegrationV6(input); +}; diff --git a/packages/integrations/src/pi-hole/pi-hole-integration.ts b/packages/integrations/src/pi-hole/v5/pi-hole-integration-v5.ts similarity index 84% rename from packages/integrations/src/pi-hole/pi-hole-integration.ts rename to packages/integrations/src/pi-hole/v5/pi-hole-integration-v5.ts index 45090a977..3f3e263d8 100644 --- a/packages/integrations/src/pi-hole/pi-hole-integration.ts +++ b/packages/integrations/src/pi-hole/v5/pi-hole-integration-v5.ts @@ -1,12 +1,12 @@ import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; -import { Integration } from "../base/integration"; -import { IntegrationTestConnectionError } from "../base/test-connection-error"; -import type { DnsHoleSummaryIntegration } from "../interfaces/dns-hole-summary/dns-hole-summary-integration"; -import type { DnsHoleSummary } from "../interfaces/dns-hole-summary/dns-hole-summary-types"; -import { summaryResponseSchema } from "./pi-hole-types"; +import { Integration } from "../../base/integration"; +import { IntegrationTestConnectionError } from "../../base/test-connection-error"; +import type { DnsHoleSummaryIntegration } from "../../interfaces/dns-hole-summary/dns-hole-summary-integration"; +import type { DnsHoleSummary } from "../../interfaces/dns-hole-summary/dns-hole-summary-types"; +import { summaryResponseSchema } from "./pi-hole-schemas-v5"; -export class PiHoleIntegration extends Integration implements DnsHoleSummaryIntegration { +export class PiHoleIntegrationV5 extends Integration implements DnsHoleSummaryIntegration { public async getSummaryAsync(): Promise { const apiKey = super.getSecretValue("apiKey"); const response = await fetchWithTrustedCertificatesAsync(this.url("/admin/api.php?summaryRaw", { auth: apiKey })); diff --git a/packages/integrations/src/pi-hole/pi-hole-types.ts b/packages/integrations/src/pi-hole/v5/pi-hole-schemas-v5.ts similarity index 75% rename from packages/integrations/src/pi-hole/pi-hole-types.ts rename to packages/integrations/src/pi-hole/v5/pi-hole-schemas-v5.ts index 7c9785d1b..35c204f9e 100644 --- a/packages/integrations/src/pi-hole/pi-hole-types.ts +++ b/packages/integrations/src/pi-hole/v5/pi-hole-schemas-v5.ts @@ -7,7 +7,3 @@ export const summaryResponseSchema = z.object({ dns_queries_today: z.number(), ads_percentage_today: z.number(), }); - -export const controlsInputSchema = z.object({ - duration: z.number().optional(), -}); diff --git a/packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts b/packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts new file mode 100644 index 000000000..8b3913ed0 --- /dev/null +++ b/packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts @@ -0,0 +1,204 @@ +import type { Response as UndiciResponse } from "undici"; +import type { z } from "zod"; + +import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { extractErrorMessage } from "@homarr/common"; +import { logger } from "@homarr/log"; + +import { IntegrationResponseError, ParseError, ResponseError } from "../../base/error"; +import type { IntegrationInput } from "../../base/integration"; +import { Integration } from "../../base/integration"; +import type { SessionStore } from "../../base/session-store"; +import { createSessionStore } from "../../base/session-store"; +import { IntegrationTestConnectionError } from "../../base/test-connection-error"; +import type { DnsHoleSummaryIntegration } from "../../interfaces/dns-hole-summary/dns-hole-summary-integration"; +import type { DnsHoleSummary } from "../../types"; +import { dnsBlockingGetSchema, sessionResponseSchema, statsSummaryGetSchema } from "./pi-hole-schemas-v6"; + +const localLogger = logger.child({ module: "PiHoleIntegrationV6" }); + +export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIntegration { + private readonly sessionStore: SessionStore; + + constructor(integration: IntegrationInput) { + super(integration); + this.sessionStore = createSessionStore(integration); + } + + public async getDnsBlockingStatusAsync(): Promise> { + const response = await this.withAuthAsync(async (sessionId) => { + return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), { + headers: { + sid: sessionId, + }, + }); + }); + + if (!response.ok) { + throw new IntegrationResponseError(this.integration, response, await response.json()); + } + + const result = dnsBlockingGetSchema.safeParse(await response.json()); + + if (!result.success) { + throw new ParseError("DNS blocking status", result.error, await response.json()); + } + + return result.data; + } + + private async getStatsSummaryAsync(): Promise> { + const response = await this.withAuthAsync(async (sessionId) => { + return await fetchWithTrustedCertificatesAsync(this.url("/api/stats/summary"), { + headers: { + sid: sessionId, + }, + }); + }); + + if (!response.ok) { + throw new IntegrationResponseError(this.integration, response, await response.json()); + } + + const data = await response.json(); + const result = statsSummaryGetSchema.safeParse(data); + + if (!result.success) { + throw new ParseError("stats summary", result.error, data); + } + + return result.data; + } + + public async getSummaryAsync(): Promise { + const dnsStatsSummary = await this.getStatsSummaryAsync(); + const dnsBlockingStatus = await this.getDnsBlockingStatusAsync(); + + return { + status: dnsBlockingStatus.blocking, + adsBlockedToday: dnsStatsSummary.queries.blocked, + adsBlockedTodayPercentage: dnsStatsSummary.queries.percent_blocked, + domainsBeingBlocked: dnsStatsSummary.gravity.domains_being_blocked, + dnsQueriesToday: dnsStatsSummary.queries.total, + }; + } + + public async testConnectionAsync(): Promise { + try { + const sessionId = await this.getSessionAsync(); + await this.clearSessionAsync(sessionId); + } catch (error: unknown) { + if (error instanceof ParseError) { + throw new IntegrationTestConnectionError("invalidJson"); + } + + if (error instanceof ResponseError && error.statusCode === 401) { + throw new IntegrationTestConnectionError("invalidCredentials"); + } + + throw new IntegrationTestConnectionError("commonError", extractErrorMessage(error)); + } + } + + public async enableAsync(): Promise { + const response = await this.withAuthAsync(async (sessionId) => { + return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), { + headers: { + sid: sessionId, + }, + body: JSON.stringify({ blocking: true }), + method: "POST", + }); + }); + + if (!response.ok) { + throw new IntegrationResponseError(this.integration, response, await response.json()); + } + } + + public async disableAsync(duration?: number): Promise { + const response = await this.withAuthAsync(async (sessionId) => { + return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), { + headers: { + sid: sessionId, + }, + body: JSON.stringify({ blocking: false, timer: duration }), + method: "POST", + }); + }); + + if (!response.ok) { + throw new IntegrationResponseError(this.integration, response, await response.json()); + } + } + + /** + * Run the callback with the current session id + * @param callback + * @returns + */ + private async withAuthAsync(callback: (sessionId: string) => Promise) { + const storedSession = await this.sessionStore.getAsync(); + + if (storedSession) { + localLogger.debug("Using stored session for request", { integrationId: this.integration.id }); + const response = await callback(storedSession); + if (response.status !== 401) { + return response; + } + + localLogger.info("Session expired, getting new session", { integrationId: this.integration.id }); + } + + const sessionId = await this.getSessionAsync(); + await this.sessionStore.setAsync(sessionId); + const response = await callback(sessionId); + return response; + } + + /** + * Get a session id from the Pi-hole server + * @returns The session id + */ + private async getSessionAsync(): Promise { + const apiKey = super.getSecretValue("apiKey"); + const response = await fetchWithTrustedCertificatesAsync(this.url("/api/auth"), { + method: "POST", + body: JSON.stringify({ password: apiKey }), + headers: { + "User-Agent": "Homarr", + }, + }); + const data = await response.json(); + const result = sessionResponseSchema.safeParse(data); + if (!result.success) { + throw new ParseError("session response", result.error, data); + } + if (!result.data.session.sid) { + throw new IntegrationResponseError(this.integration, response, data); + } + + localLogger.info("Received session id successfully", { integrationId: this.integration.id }); + + return result.data.session.sid; + } + + /** + * Remove the session from the Pi-hole server + * @param sessionId The session id to remove + */ + private async clearSessionAsync(sessionId: string) { + const response = await fetchWithTrustedCertificatesAsync(this.url("/api/auth"), { + method: "DELETE", + headers: { + sid: sessionId, + }, + }); + + if (!response.ok) { + localLogger.warn("Failed to clear session", { statusCode: response.status, content: await response.text() }); + } + + logger.debug("Cleared session successfully"); + } +} diff --git a/packages/integrations/src/pi-hole/v6/pi-hole-schemas-v6.ts b/packages/integrations/src/pi-hole/v6/pi-hole-schemas-v6.ts new file mode 100644 index 000000000..283a8a71f --- /dev/null +++ b/packages/integrations/src/pi-hole/v6/pi-hole-schemas-v6.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; + +export const sessionResponseSchema = z.object({ + session: z.object({ + sid: z.string().nullable(), + message: z.string().nullable(), + }), +}); + +export const dnsBlockingGetSchema = z.object({ + blocking: z.enum(["enabled", "disabled", "failed", "unknown"]).transform((value) => { + if (value === "failed") return undefined; + if (value === "unknown") return undefined; + return value; + }), + timer: z.number().nullable(), +}); + +export const statsSummaryGetSchema = z.object({ + queries: z.object({ + total: z.number(), + blocked: z.number(), + percent_blocked: z.number(), + }), + gravity: z.object({ + domains_being_blocked: z.number(), + }), +}); diff --git a/packages/integrations/src/types.ts b/packages/integrations/src/types.ts index d3760cadb..66a3d0de2 100644 --- a/packages/integrations/src/types.ts +++ b/packages/integrations/src/types.ts @@ -3,7 +3,6 @@ export * from "./interfaces/dns-hole-summary/dns-hole-summary-types"; export * from "./interfaces/health-monitoring/healt-monitoring"; export * from "./interfaces/indexer-manager/indexer"; export * from "./interfaces/media-requests/media-request"; -export * from "./pi-hole/pi-hole-types"; export * from "./base/searchable-integration"; export * from "./homeassistant/homeassistant-types"; export * from "./proxmox/proxmox-types"; diff --git a/packages/integrations/test/pi-hole.spec.ts b/packages/integrations/test/pi-hole.spec.ts index 558406ab7..aaa25d03c 100644 --- a/packages/integrations/test/pi-hole.spec.ts +++ b/packages/integrations/test/pi-hole.spec.ts @@ -1,17 +1,18 @@ import type { StartedTestContainer } from "testcontainers"; import { GenericContainer, Wait } from "testcontainers"; -import { describe, expect, test } from "vitest"; +import { describe, expect, test, vi } from "vitest"; -import { PiHoleIntegration } from "../src"; +import { PiHoleIntegrationV5, PiHoleIntegrationV6 } from "../src"; +import type { SessionStore } from "../src/base/session-store"; const DEFAULT_PASSWORD = "12341234"; const DEFAULT_API_KEY = "3b1434980677dcf53fa8c4a611db3b1f0f88478790097515c0abb539102778b9"; // Some hash generated from password -describe("Pi-hole integration", () => { +describe("Pi-hole v5 integration", () => { test("getSummaryAsync should return summary from pi-hole", async () => { // Arrange - const piholeContainer = await createPiHoleContainer(DEFAULT_PASSWORD).start(); - const piHoleIntegration = createPiHoleIntegration(piholeContainer, DEFAULT_API_KEY); + const piholeContainer = await createPiHoleV5Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV5(piholeContainer, DEFAULT_API_KEY); // Act const result = await piHoleIntegration.getSummaryAsync(); @@ -28,8 +29,8 @@ describe("Pi-hole integration", () => { test("testConnectionAsync should not throw", async () => { // Arrange - const piholeContainer = await createPiHoleContainer(DEFAULT_PASSWORD).start(); - const piHoleIntegration = createPiHoleIntegration(piholeContainer, DEFAULT_API_KEY); + const piholeContainer = await createPiHoleV5Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV5(piholeContainer, DEFAULT_API_KEY); // Act const actAsync = async () => await piHoleIntegration.testConnectionAsync(); @@ -43,8 +44,8 @@ describe("Pi-hole integration", () => { test("testConnectionAsync should throw with wrong credentials", async () => { // Arrange - const piholeContainer = await createPiHoleContainer(DEFAULT_PASSWORD).start(); - const piHoleIntegration = createPiHoleIntegration(piholeContainer, "wrong-api-key"); + const piholeContainer = await createPiHoleV5Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV5(piholeContainer, "wrong-api-key"); // Act const actAsync = async () => await piHoleIntegration.testConnectionAsync(); @@ -57,7 +58,118 @@ describe("Pi-hole integration", () => { }, 20_000); // Timeout of 20 seconds }); -const createPiHoleContainer = (password: string) => { +vi.mock("../src/base/session-store", () => ({ + createSessionStore: () => + ({ + async getAsync() { + return await Promise.resolve(null); + }, + async setAsync() { + return await Promise.resolve(); + }, + async clearAsync() { + return await Promise.resolve(); + }, + }) satisfies SessionStore, +})); + +describe("Pi-hole v6 integration", () => { + test("getSummaryAsync should return summary from pi-hole", async () => { + // Arrange + const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, DEFAULT_PASSWORD); + + // Act + const result = await piHoleIntegration.getSummaryAsync(); + + // Assert + expect(result.status).toBe("enabled"); + expect(result.adsBlockedToday).toBe(0); + expect(result.adsBlockedTodayPercentage).toBe(0); + expect(result.dnsQueriesToday).toBe(0); + expect(result.domainsBeingBlocked).toBeGreaterThanOrEqual(0); + + // Cleanup + await piholeContainer.stop(); + }, 20_000); // Timeout of 20 seconds + + test("enableAsync should enable pi-hole", async () => { + // Arrange + const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, DEFAULT_PASSWORD); + + // Disable pi-hole + await piholeContainer.exec(["pihole", "disable"]); + + // Act + await piHoleIntegration.enableAsync(); + + // Assert + const status = await piHoleIntegration.getDnsBlockingStatusAsync(); + expect(status.blocking).toContain("enabled"); + }, 20_000); // Timeout of 20 seconds + + test("disableAsync should disable pi-hole", async () => { + // Arrange + const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, DEFAULT_PASSWORD); + + // Act + await piHoleIntegration.disableAsync(); + + // Assert + const status = await piHoleIntegration.getDnsBlockingStatusAsync(); + expect(status.blocking).toBe("disabled"); + expect(status.timer).toBe(null); + }, 20_000); // Timeout of 20 seconds + + test("disableAsync should disable pi-hole with timer", async () => { + // Arrange + const timer = 10 * 60; // 10 minutes + const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, DEFAULT_PASSWORD); + + // Act + await piHoleIntegration.disableAsync(timer); + + // Assert + const status = await piHoleIntegration.getDnsBlockingStatusAsync(); + expect(status.blocking).toBe("disabled"); + expect(status.timer).toBeGreaterThan(timer - 10); + }, 20_000); // Timeout of 20 seconds + + test("testConnectionAsync should not throw", async () => { + // Arrange + const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, DEFAULT_PASSWORD); + + // Act + const actAsync = async () => await piHoleIntegration.testConnectionAsync(); + + // Assert + await expect(actAsync()).resolves.not.toThrow(); + + // Cleanup + await piholeContainer.stop(); + }, 20_000); // Timeout of 20 seconds + + test("testConnectionAsync should throw with wrong credentials", async () => { + // Arrange + const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, "wrong-api-key"); + + // Act + const actAsync = async () => await piHoleIntegration.testConnectionAsync(); + + // Assert + await expect(actAsync()).rejects.toThrow(); + + // Cleanup + await piholeContainer.stop(); + }, 20_000); // Timeout of 20 seconds +}); + +const createPiHoleV5Container = (password: string) => { return new GenericContainer("pihole/pihole:2024.07.0") // v5 .withEnvironment({ WEBPASSWORD: password, @@ -66,8 +178,31 @@ const createPiHoleContainer = (password: string) => { .withWaitStrategy(Wait.forLogMessage("Pi-hole Enabled")); }; -const createPiHoleIntegration = (container: StartedTestContainer, apiKey: string) => { - return new PiHoleIntegration({ +const createPiHoleIntegrationV5 = (container: StartedTestContainer, apiKey: string) => { + return new PiHoleIntegrationV5({ + id: "1", + decryptedSecrets: [ + { + kind: "apiKey", + value: apiKey, + }, + ], + name: "Pi hole", + url: `http://${container.getHost()}:${container.getMappedPort(80)}`, + }); +}; + +const createPiHoleV6Container = (password: string) => { + return new GenericContainer("pihole/pihole:latest") + .withEnvironment({ + FTLCONF_webserver_api_password: password, + }) + .withExposedPorts(80) + .withWaitStrategy(Wait.forHttp("/admin", 80)); +}; + +const createPiHoleIntegrationV6 = (container: StartedTestContainer, apiKey: string) => { + return new PiHoleIntegrationV6({ id: "1", decryptedSecrets: [ { diff --git a/packages/log/package.json b/packages/log/package.json index b1e0cfb6f..73bef5d32 100644 --- a/packages/log/package.json +++ b/packages/log/package.json @@ -2,7 +2,7 @@ "name": "@homarr/log", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./src/index.ts", diff --git a/packages/modals-collection/package.json b/packages/modals-collection/package.json index a527e507d..efd920c1f 100644 --- a/packages/modals-collection/package.json +++ b/packages/modals-collection/package.json @@ -2,7 +2,7 @@ "name": "@homarr/modals-collection", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts" @@ -33,8 +33,8 @@ "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/core": "^7.17.0", - "@tabler/icons-react": "^3.30.0", + "@mantine/core": "^7.17.1", + "@tabler/icons-react": "^3.31.0", "dayjs": "^1.11.13", "next": "15.1.7", "react": "19.0.0", diff --git a/packages/modals-collection/src/docker/add-docker-app-to-homarr.tsx b/packages/modals-collection/src/docker/add-docker-app-to-homarr.tsx index 2301c0dd5..6748f7979 100644 --- a/packages/modals-collection/src/docker/add-docker-app-to-homarr.tsx +++ b/packages/modals-collection/src/docker/add-docker-app-to-homarr.tsx @@ -1,4 +1,4 @@ -import { Button, Group, Image, List, LoadingOverlay, Stack, Text, TextInput } from "@mantine/core"; +import { Avatar, Button, Group, List, LoadingOverlay, Stack, Text, TextInput } from "@mantine/core"; import { z } from "zod"; import type { RouterOutputs } from "@homarr/api"; @@ -60,25 +60,35 @@ export const AddDockerAppToHomarrModal = createModal(
- + {innerProps.selectedContainers.map((container, index) => ( } + icon={ + + {container.name.at(0)?.toUpperCase()} + + } key={container.id} > - - {container.name} + + {container.name} ))} - - @@ -89,4 +99,5 @@ export const AddDockerAppToHomarrModal = createModal( defaultTitle(t) { return t("docker.action.addToHomarr.modal.title"); }, + size: "lg", }); diff --git a/packages/modals/package.json b/packages/modals/package.json index 3311226ee..e19d89f50 100644 --- a/packages/modals/package.json +++ b/packages/modals/package.json @@ -2,7 +2,7 @@ "name": "@homarr/modals", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts" @@ -24,8 +24,8 @@ "dependencies": { "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", - "@mantine/core": "^7.17.0", - "@mantine/hooks": "^7.17.0", + "@mantine/core": "^7.17.1", + "@mantine/hooks": "^7.17.1", "react": "19.0.0" }, "devDependencies": { diff --git a/packages/notifications/package.json b/packages/notifications/package.json index b84915a53..0fed421d5 100644 --- a/packages/notifications/package.json +++ b/packages/notifications/package.json @@ -2,7 +2,7 @@ "name": "@homarr/notifications", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", @@ -24,8 +24,8 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/ui": "workspace:^0.1.0", - "@mantine/notifications": "^7.17.0", - "@tabler/icons-react": "^3.30.0" + "@mantine/notifications": "^7.17.1", + "@tabler/icons-react": "^3.31.0" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/old-import/package.json b/packages/old-import/package.json index afb6090d1..90ec49ec1 100644 --- a/packages/old-import/package.json +++ b/packages/old-import/package.json @@ -2,7 +2,7 @@ "name": "@homarr/old-import", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", @@ -37,8 +37,8 @@ "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/core": "^7.17.0", - "@mantine/hooks": "^7.17.0", + "@mantine/core": "^7.17.1", + "@mantine/hooks": "^7.17.1", "adm-zip": "0.5.16", "next": "15.1.7", "react": "19.0.0", diff --git a/packages/old-import/src/mappers/map-board.ts b/packages/old-import/src/mappers/map-board.ts index 1e2153bef..7b5450057 100644 --- a/packages/old-import/src/mappers/map-board.ts +++ b/packages/old-import/src/mappers/map-board.ts @@ -14,12 +14,40 @@ export const mapBoard = (preparedBoard: PreparedBoard): InferInsertModel { + if (!logo) { + return null; + } + + if (logo.trim() === defaultOldmarrLogoPath) { + return null; // We fallback to default logo when null + } + + return logo; +}; + +const defaultOldmarrFaviconPath = "/imgs/favicon/favicon-squared.png"; + +const mapFavicon = (favicon: string | null | undefined) => { + if (!favicon) { + return null; + } + + if (favicon.trim() === defaultOldmarrFaviconPath) { + return null; // We fallback to default favicon when null + } + + return favicon; +}; diff --git a/packages/old-schema/package.json b/packages/old-schema/package.json index 21a934e5c..1f6732ef0 100644 --- a/packages/old-schema/package.json +++ b/packages/old-schema/package.json @@ -2,7 +2,7 @@ "name": "@homarr/old-schema", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts" diff --git a/packages/ping/package.json b/packages/ping/package.json index 974436f63..2e6f4e64a 100644 --- a/packages/ping/package.json +++ b/packages/ping/package.json @@ -2,7 +2,7 @@ "name": "@homarr/ping", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts" diff --git a/packages/redis/package.json b/packages/redis/package.json index f42c57c60..260aa1449 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -2,7 +2,7 @@ "name": "@homarr/redis", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts" diff --git a/packages/redis/src/index.ts b/packages/redis/src/index.ts index b752177ac..c61393e51 100644 --- a/packages/redis/src/index.ts +++ b/packages/redis/src/index.ts @@ -9,6 +9,7 @@ export { createChannelWithLatestAndEvents, handshakeAsync, createSubPubChannel, + createGetSetChannel, } from "./lib/channel"; export const exampleChannel = createSubPubChannel<{ message: string }>("example"); diff --git a/packages/redis/src/lib/channel.ts b/packages/redis/src/lib/channel.ts index 42eb2ef13..686fd4f50 100644 --- a/packages/redis/src/lib/channel.ts +++ b/packages/redis/src/lib/channel.ts @@ -94,6 +94,36 @@ export const createListChannel = (name: string) => { }; }; +/** + * Creates a new redis channel for getting and setting data + * @param name name of channel + */ +export const createGetSetChannel = (name: string) => { + return { + /** + * Get data from the channel + * @returns data or null if not found + */ + getAsync: async () => { + const data = await getSetClient.get(name); + return data ? superjson.parse(data) : null; + }, + /** + * Set data in the channel + * @param data data to be stored in the channel + */ + setAsync: async (data: TData) => { + await getSetClient.set(name, superjson.stringify(data)); + }, + /** + * Remove data from the channel + */ + removeAsync: async () => { + await getSetClient.del(name); + }, + }; +}; + /** * Creates a new cache channel. * @param name name of the channel diff --git a/packages/request-handler/package.json b/packages/request-handler/package.json index 0d46e2bc5..6599b177d 100644 --- a/packages/request-handler/package.json +++ b/packages/request-handler/package.json @@ -2,7 +2,7 @@ "name": "@homarr/request-handler", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { "./*": "./src/*.ts" diff --git a/packages/request-handler/src/calendar.ts b/packages/request-handler/src/calendar.ts index 3987e0497..f047d74df 100644 --- a/packages/request-handler/src/calendar.ts +++ b/packages/request-handler/src/calendar.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { CalendarEvent, RadarrReleaseType } from "@homarr/integrations/types"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const calendarMonthRequestHandler = createCachedIntegrationRequestHandler { year: number; month: number; releaseType: RadarrReleaseType[] } >({ async requestAsync(integration, input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); const startDate = dayjs().year(input.year).month(input.month).startOf("month"); const endDate = startDate.clone().endOf("month"); return await integrationInstance.getCalendarEventsAsync(startDate.toDate(), endDate.toDate()); diff --git a/packages/request-handler/src/dns-hole.ts b/packages/request-handler/src/dns-hole.ts index c05ee575d..a8c920801 100644 --- a/packages/request-handler/src/dns-hole.ts +++ b/packages/request-handler/src/dns-hole.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { DnsHoleSummary } from "@homarr/integrations/types"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const dnsHoleRequestHandler = createCachedIntegrationRequestHandler< Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getSummaryAsync(); }, cacheDuration: dayjs.duration(5, "seconds"), diff --git a/packages/request-handler/src/downloads.ts b/packages/request-handler/src/downloads.ts index 71fc0d4a4..567b88324 100644 --- a/packages/request-handler/src/downloads.ts +++ b/packages/request-handler/src/downloads.ts @@ -2,7 +2,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { DownloadClientJobsAndStatus } from "@homarr/integrations"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const downloadClientRequestHandler = createCachedIntegrationRequestHandle Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getClientJobsAndStatusAsync(); }, cacheDuration: dayjs.duration(5, "seconds"), diff --git a/packages/request-handler/src/health-monitoring.ts b/packages/request-handler/src/health-monitoring.ts index f57b9ee30..48d95cd20 100644 --- a/packages/request-handler/src/health-monitoring.ts +++ b/packages/request-handler/src/health-monitoring.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { HealthMonitoring, ProxmoxClusterInfo } from "@homarr/integrations/types"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const systemInfoRequestHandler = createCachedIntegrationRequestHandler< Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getSystemInfoAsync(); }, cacheDuration: dayjs.duration(5, "seconds"), @@ -25,7 +25,7 @@ export const clusterInfoRequestHandler = createCachedIntegrationRequestHandler< Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getClusterInfoAsync(); }, cacheDuration: dayjs.duration(5, "seconds"), diff --git a/packages/request-handler/src/indexer-manager.ts b/packages/request-handler/src/indexer-manager.ts index 5262fa8f0..a957b32ad 100644 --- a/packages/request-handler/src/indexer-manager.ts +++ b/packages/request-handler/src/indexer-manager.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { Indexer } from "@homarr/integrations/types"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const indexerManagerRequestHandler = createCachedIntegrationRequestHandle Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getIndexersAsync(); }, cacheDuration: dayjs.duration(5, "minutes"), diff --git a/packages/request-handler/src/media-request-list.ts b/packages/request-handler/src/media-request-list.ts index 53c31a7b8..fd4e02234 100644 --- a/packages/request-handler/src/media-request-list.ts +++ b/packages/request-handler/src/media-request-list.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { MediaRequest } from "@homarr/integrations/types"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const mediaRequestListRequestHandler = createCachedIntegrationRequestHand Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getRequestsAsync(); }, cacheDuration: dayjs.duration(1, "minute"), diff --git a/packages/request-handler/src/media-request-stats.ts b/packages/request-handler/src/media-request-stats.ts index c4829167a..a6568aecd 100644 --- a/packages/request-handler/src/media-request-stats.ts +++ b/packages/request-handler/src/media-request-stats.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { MediaRequestStats } from "@homarr/integrations/types"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const mediaRequestStatsRequestHandler = createCachedIntegrationRequestHan Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return { stats: await integrationInstance.getStatsAsync(), users: await integrationInstance.getUsersAsync(), diff --git a/packages/request-handler/src/media-server.ts b/packages/request-handler/src/media-server.ts index e2f52773f..2fadad23f 100644 --- a/packages/request-handler/src/media-server.ts +++ b/packages/request-handler/src/media-server.ts @@ -2,7 +2,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { StreamSession } from "@homarr/integrations"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const mediaServerRequestHandler = createCachedIntegrationRequestHandler< Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getCurrentSessionsAsync(); }, cacheDuration: dayjs.duration(5, "seconds"), diff --git a/packages/request-handler/src/media-transcoding.ts b/packages/request-handler/src/media-transcoding.ts index 05446bdd6..c40d5b058 100644 --- a/packages/request-handler/src/media-transcoding.ts +++ b/packages/request-handler/src/media-transcoding.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { TdarrQueue, TdarrStatistics, TdarrWorker } from "@homarr/integrations"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -14,7 +14,7 @@ export const mediaTranscodingRequestHandler = createCachedIntegrationRequestHand queryKey: "mediaTranscoding", cacheDuration: dayjs.duration(5, "minutes"), async requestAsync(integration, input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return { queue: await integrationInstance.getQueueAsync(input.pageOffset, input.pageSize), workers: await integrationInstance.getWorkersAsync(), diff --git a/packages/request-handler/src/smart-home-entity-state.ts b/packages/request-handler/src/smart-home-entity-state.ts index 69641949c..5fe02b2ac 100644 --- a/packages/request-handler/src/smart-home-entity-state.ts +++ b/packages/request-handler/src/smart-home-entity-state.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -11,7 +11,7 @@ export const smartHomeEntityStateRequestHandler = createCachedIntegrationRequest { entityId: string } >({ async requestAsync(integration, input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); const result = await integrationInstance.getEntityStateAsync(input.entityId); if (!result.success) { diff --git a/packages/server-settings/package.json b/packages/server-settings/package.json index 67bb10bf4..a7599cb52 100644 --- a/packages/server-settings/package.json +++ b/packages/server-settings/package.json @@ -2,7 +2,7 @@ "name": "@homarr/server-settings", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts" diff --git a/packages/settings/package.json b/packages/settings/package.json index 0f650de78..502c42c0f 100644 --- a/packages/settings/package.json +++ b/packages/settings/package.json @@ -2,7 +2,7 @@ "name": "@homarr/settings", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts" @@ -25,7 +25,7 @@ "@homarr/api": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", - "@mantine/dates": "^7.17.0", + "@mantine/dates": "^7.17.1", "next": "15.1.7", "react": "19.0.0", "react-dom": "19.0.0" diff --git a/packages/spotlight/package.json b/packages/spotlight/package.json index c8c6a527f..e789f24e4 100644 --- a/packages/spotlight/package.json +++ b/packages/spotlight/package.json @@ -2,7 +2,7 @@ "name": "@homarr/spotlight", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", @@ -33,10 +33,10 @@ "@homarr/settings": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", - "@mantine/core": "^7.17.0", - "@mantine/hooks": "^7.17.0", - "@mantine/spotlight": "^7.17.0", - "@tabler/icons-react": "^3.30.0", + "@mantine/core": "^7.17.1", + "@mantine/hooks": "^7.17.1", + "@mantine/spotlight": "^7.17.1", + "@tabler/icons-react": "^3.31.0", "jotai": "^2.12.1", "next": "15.1.7", "react": "19.0.0", diff --git a/packages/translation/package.json b/packages/translation/package.json index 4a522a9d6..4038fd24e 100644 --- a/packages/translation/package.json +++ b/packages/translation/package.json @@ -2,7 +2,7 @@ "name": "@homarr/translation", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", diff --git a/packages/translation/src/lang/ca.json b/packages/translation/src/lang/ca.json index f7e34f2e7..0bb0c2942 100644 --- a/packages/translation/src/lang/ca.json +++ b/packages/translation/src/lang/ca.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "", "available": "" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/cn.json b/packages/translation/src/lang/cn.json index 4f84e3bca..73b99b8cd 100644 --- a/packages/translation/src/lang/cn.json +++ b/packages/translation/src/lang/cn.json @@ -972,6 +972,11 @@ "create": "新建动态部分", "remove": "删除动态部分" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "删除动态部分", "message": "你确定你要删除这个动态部分?项目将被移动到父节的相同位置。" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "部分", "available": "可用" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "待定" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/cs.json b/packages/translation/src/lang/cs.json index 45d0e0458..e0a4aa538 100644 --- a/packages/translation/src/lang/cs.json +++ b/packages/translation/src/lang/cs.json @@ -972,6 +972,11 @@ "create": "Nová dynamická sekce", "remove": "Odstranit dynamickou sekci" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "Odstranit dynamickou sekci", "message": "Jste si jisti, že chcete odstranit tuto dynamickou sekci? Položky budou přesunuty na stejné místo v nadřazené sekci." @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Částečně dostupné", "available": "K dispozici" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/da.json b/packages/translation/src/lang/da.json index 01136c542..3507dea79 100644 --- a/packages/translation/src/lang/da.json +++ b/packages/translation/src/lang/da.json @@ -972,6 +972,11 @@ "create": "Ny dynamisk sektion", "remove": "Fjern dynamisk sektion" }, + "option": { + "borderColor": { + "label": "Kantfarve" + } + }, "remove": { "title": "Fjern dynamisk sektion", "message": "Er du sikker på at du vil fjerne dette dynamiske afsnit? Elementer vil blive flyttet på samme sted i det overordnede afsnit." @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Delvis", "available": "Tilgængelig" }, + "status": { + "pending": "Afventende", + "approved": "Godkendt", + "declined": "Afvist", + "failed": "Mislykket" + }, "toBeDetermined": "TBD" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/de.json b/packages/translation/src/lang/de.json index 837983390..9c75cd287 100644 --- a/packages/translation/src/lang/de.json +++ b/packages/translation/src/lang/de.json @@ -972,6 +972,11 @@ "create": "Neuer dynamischer Abschnitt", "remove": "Dynamischen Abschnitt entfernen" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "Dynamischen Abschnitt entfernen", "message": "Möchten Sie diesen dynamischen Abschnitt wirklich entfernen? Die Elemente werden an die gleiche Position im übergeordneten Abschnitt verschoben." @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Teilweise", "available": "Verfügbar" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "Noch Festzulegen" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/el.json b/packages/translation/src/lang/el.json index 47aed435e..6c3484425 100644 --- a/packages/translation/src/lang/el.json +++ b/packages/translation/src/lang/el.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Μερικώς", "available": "Διαθέσιμο" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 5a4adf486..72ed28ddc 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -972,6 +972,11 @@ "create": "New dynamic section", "remove": "Remove dynamic section" }, + "option": { + "borderColor": { + "label": "Border color" + } + }, "remove": { "title": "Remove dynamic section", "message": "Are you sure you want to remove this dynamic section? Items will be moved at the same location in the parent section." @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Partial", "available": "Available" }, + "status": { + "pending": "Pending", + "approved": "Approved", + "declined": "Declined", + "failed": "Failed" + }, "toBeDetermined": "TBD" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/es.json b/packages/translation/src/lang/es.json index 4e480b038..2c870225b 100644 --- a/packages/translation/src/lang/es.json +++ b/packages/translation/src/lang/es.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Parcial", "available": "Disponible" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/et.json b/packages/translation/src/lang/et.json index d2ea3d825..d90768f8e 100644 --- a/packages/translation/src/lang/et.json +++ b/packages/translation/src/lang/et.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "", "available": "" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/fr.json b/packages/translation/src/lang/fr.json index 1f8c000c3..793e01169 100644 --- a/packages/translation/src/lang/fr.json +++ b/packages/translation/src/lang/fr.json @@ -44,7 +44,7 @@ "apps": "Applications", "boards": "Tableaux de bord", "integrations": "Intégrations", - "credentialUsers": "" + "credentialUsers": "Utilisateurs identifiés" } }, "tokenModal": { @@ -307,12 +307,12 @@ "name": "Nom", "members": "Membres", "homeBoard": { - "label": "", - "description": "" + "label": "Tableau d'accueil", + "description": "Seuls les tableaux accessibles au groupe peuvent être sélectionnés" }, "mobileBoard": { - "label": "", - "description": "" + "label": "Tableau mobile", + "description": "Seuls les tableaux accessibles au groupe peuvent être sélectionnés" } }, "permission": { @@ -383,8 +383,8 @@ "description": "Autoriser les membres à interagir avec n'importe quelle intégration" }, "full-all": { - "label": "", - "description": "" + "label": "Accès complet à l'intégration", + "description": "Autoriser les membres à gérer, utiliser et interagir avec n'importe quelle intégration" } } }, @@ -422,19 +422,19 @@ "description": "Autoriser les membres à créer des moteurs de recherche" }, "modify-all": { - "label": "", - "description": "" + "label": "Modifier tous les moteurs de recherche", + "description": "Autoriser les membres à modifier tous les moteurs de recherche" }, "full-all": { - "label": "", - "description": "" + "label": "Accès complet au moteur de recherche", + "description": "Autoriser les membres à gérer et supprimer n'importe quel moteur de recherche" } } } }, "memberNotice": { - "mixed": "", - "external": "" + "mixed": "Certains membres proviennent de fournisseurs externes et ne peuvent pas être gérés ici", + "external": "Tous les membres proviennent de fournisseurs externes et ne peuvent pas être gérés ici" }, "reservedNotice": { "message": "Ce groupe est réservé pour l'utilisation du système et restreint certaines actions. " @@ -499,10 +499,10 @@ "update": { "notification": { "success": { - "message": "" + "message": "Le groupe {name} a été enregistré avec succès" }, "error": { - "message": "" + "message": "Impossible d'enregistrer le groupe {name}" } } }, @@ -514,12 +514,12 @@ "board": { "notification": { "success": { - "title": "", - "message": "" + "title": "Paramètres enregistrés", + "message": "Paramètres du tableau enregistrés avec succès" }, "error": { - "title": "", - "message": "" + "title": "Échec de l'enregistrement des paramètres", + "message": "Impossible d'enregistrer les paramètres du tableau" } } } @@ -527,21 +527,21 @@ "changePosition": { "notification": { "success": { - "message": "" + "message": "Position modifiée avec succès" }, "error": { - "message": "" + "message": "Impossible de modifier la position" } } } }, "defaultGroup": { - "name": "", - "description": "" + "name": "Groupe par défaut", + "description": "{name} - Tous les utilisateurs connectés" } }, "app": { - "search": "", + "search": "Trouver une application", "page": { "list": { "title": "Applications", @@ -558,21 +558,21 @@ "message": "L'application a bien été créée" }, "error": { - "title": "", - "message": "" + "title": "Échec de la création", + "message": "L'application n'a pas pu être créée" } } }, "edit": { - "title": "", + "title": "Éditer l'application", "notification": { "success": { - "title": "", - "message": "" + "title": "Modifications appliquées avec succès", + "message": "L'application a été enregistrée avec succès" }, "error": { "title": "Impossible d'appliquer les changements", - "message": "" + "message": "L'application n'a pas pu être enregistrée" } } }, @@ -582,11 +582,11 @@ "notification": { "success": { "title": "Suppression effectuée avec succès", - "message": "" + "message": "L'application a été supprimée avec succès" }, "error": { - "title": "", - "message": "" + "title": "Échec de la suppression", + "message": "Impossible de supprimer l'application" } } } @@ -603,8 +603,8 @@ }, "useDifferentUrlForPing": { "checkbox": { - "label": "", - "description": "" + "label": "Utiliser une URL différente pour le ping", + "description": "Utile si Homarr peut accéder directement à l'aide d'un nom d'hôte ou d'un réseau internes pour éviter l'utilisation de bande passante du FAI" } } }, @@ -619,48 +619,48 @@ "page": { "list": { "title": "Intégrations", - "search": "", + "search": "Rechercher des intégrations", "noResults": { - "title": "" + "title": "Il n'y a pas encore d'intégrations" } }, "create": { - "title": "", + "title": "Nouvelle intégration {name}", "notification": { "success": { "title": "Créé avec succès", - "message": "" + "message": "L'intégration a été créée avec succès" }, "error": { - "title": "", - "message": "" + "title": "Échec de la création", + "message": "L'intégration n'a pas pu être créée" } } }, "edit": { - "title": "", + "title": "Modifier l'intégration {name}", "notification": { "success": { - "title": "", - "message": "" + "title": "Modifications appliquées avec succès", + "message": "L'intégration a été enregistrée avec succès" }, "error": { - "title": "", - "message": "" + "title": "Impossible d'appliquer les modifications", + "message": "L'intégration n'a pas pu être enregistrée" } } }, "delete": { - "title": "", - "message": "", + "title": "Supprimer l'intégration", + "message": "Êtes-vous sûr(e) de vouloir supprimer l'intégration {name}?", "notification": { "success": { - "title": "", - "message": "" + "title": "Suppression réussie", + "message": "L'intégration a été supprimée avec succès" }, "error": { "title": "", - "message": "" + "message": "Impossible de supprimer l'intégration" } } } @@ -673,8 +673,8 @@ "label": "Url" }, "attemptSearchEngineCreation": { - "label": "", - "description": "" + "label": "Créer un moteur de recherche", + "description": "L'intégration \"{kind}\" peut être utilisée avec les moteurs de recherche. Cochez ceci pour configurer automatiquement le moteur de recherche." } }, "action": { @@ -705,57 +705,57 @@ }, "commonError": { "title": "Échec de la connexion", - "message": "" + "message": "La connexion n'a pas pu être établie" }, "badRequest": { - "title": "", - "message": "" + "title": "Requête incorrecte", + "message": "La requête a été mal formée" }, "unauthorized": { - "title": "", - "message": "" + "title": "Non autorisé", + "message": "Identifiants probablement erronés" }, "forbidden": { - "title": "", - "message": "" + "title": "Interdit", + "message": "Permissions probablement manquantes" }, "notFound": { - "title": "", - "message": "" + "title": "Pas trouvé", + "message": "URL ou chemin d'accès probablement incorrect" }, "internalServerError": { - "title": "", - "message": "" + "title": "Erreur interne du serveur", + "message": "Le serveur a rencontré une erreur" }, "serviceUnavailable": { - "title": "", - "message": "" + "title": "Service indisponible", + "message": "Le serveur est actuellement indisponible" }, "connectionAborted": { - "title": "", - "message": "" + "title": "Connexion interrompue", + "message": "La connexion a été interrompue" }, "domainNotFound": { - "title": "", - "message": "" + "title": "Domaine introuvable", + "message": "Le domaine n'a pas pu être trouvé" }, "connectionRefused": { - "title": "", - "message": "" + "title": "Connexion refusée", + "message": "La connexion a été refusée" }, "invalidJson": { - "title": "", - "message": "" + "title": "JSON invalide", + "message": "La réponse n'était pas un JSON valide" }, "wrongPath": { - "title": "", - "message": "" + "title": "Mauvais chemin d'accès", + "message": "Le chemin d'accès n'est probablement pas correct" } } }, "secrets": { - "title": "", - "lastUpdated": "", + "title": "Secrets", + "lastUpdated": "Dernière mise à jour {date}", "notSet": { "label": "Aucune valeur définie", "tooltip": "Le secret requis n'a pas encore été défini" @@ -783,12 +783,12 @@ "newLabel": "Nouveau mot de passe" }, "tokenId": { - "label": "", - "newLabel": "" + "label": "ID du jeton", + "newLabel": "Nouvel ID de jeton" }, "realm": { - "label": "", - "newLabel": "" + "label": "Domaine", + "newLabel": "Nouveau domaine" } } }, @@ -930,7 +930,7 @@ }, "dangerZone": "Zone de danger", "noResults": "Aucun résultat trouvé", - "unsavedChanges": "", + "unsavedChanges": "Vous avez des modifications non enregistrées !", "preview": { "show": "Afficher l’aperçu", "hide": "Masquer l'aperçu" @@ -956,12 +956,12 @@ "custom": { "passwordsDoNotMatch": "Les mots de passe ne correspondent pas.", "passwordRequirements": "Le mot de passe ne respecte pas les exigences requises.", - "boardAlreadyExists": "", - "invalidFileType": "", - "invalidFileName": "", - "fileTooLarge": "", - "invalidConfiguration": "", - "groupNameTaken": "" + "boardAlreadyExists": "Un tableau avec ce nom existe déjà", + "invalidFileType": "Type de fichier invalide, {expected} attendu", + "invalidFileName": "Nom de fichier invalide", + "fileTooLarge": "Le fichier est trop volumineux, la taille maximale est {maxSize}", + "invalidConfiguration": "Configuration invalide", + "groupNameTaken": "Nom de groupe déjà utilisé" } } } @@ -969,12 +969,17 @@ "section": { "dynamic": { "action": { - "create": "", - "remove": "" + "create": "Nouvelle section dynamique", + "remove": "Supprimer la section dynamique" + }, + "option": { + "borderColor": { + "label": "Couleur de la bordure" + } }, "remove": { - "title": "", - "message": "" + "title": "Supprimer la section dynamique", + "message": "Êtes-vous sûr de vouloir supprimer cette section dynamique ? Les éléments seront déplacés au même emplacement dans la section parente." } }, "category": { @@ -984,14 +989,14 @@ } }, "action": { - "create": "", - "edit": "", - "remove": "", + "create": "Nouvelle catégorie", + "edit": "Renommer la catégorie", + "remove": "Supprimer la catégorie", "moveUp": "Monter", "moveDown": "Descendre", - "createAbove": "", - "createBelow": "", - "openAllInNewTabs": "" + "createAbove": "Nouvelle catégorie au-dessus", + "createBelow": "Nouvelle catégorie en dessous", + "openAllInNewTabs": "Tout ouvrir dans des onglets" }, "create": { "title": "", @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Partiel", "available": "Disponible" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/he.json b/packages/translation/src/lang/he.json index 7df75773e..a7bcf29ff 100644 --- a/packages/translation/src/lang/he.json +++ b/packages/translation/src/lang/he.json @@ -603,8 +603,8 @@ }, "useDifferentUrlForPing": { "checkbox": { - "label": "", - "description": "" + "label": "השתמש בקישור שונה עבור פינג", + "description": "שימושי אם Homarr יכול לגשת ישירות באמצעות שם מארח פנימי או רשת כדי למנוע שימוש ברוחב פס של ספק שירותי האינטרנט" } } }, @@ -972,6 +972,11 @@ "create": "מדור דינמי חדש", "remove": "הסר קטע דינמי" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "הסר קטע דינמי", "message": "האם אתה בטוח שברצונך להסיר את הקטע הדינמי הזה? פריטים יועברו לאותו מיקום במדור האב." @@ -1615,7 +1620,7 @@ "app": { "noData": "לא נמצאה אפליקציה", "description": "לחץ על כדי ליצור אפליקציה חדשה", - "quickCreate": "" + "quickCreate": "צור אפליקציה תוך כדי תנועה" }, "error": { "noIntegration": "לא נבחרה אינטגרציה", @@ -1816,6 +1821,12 @@ "partiallyAvailable": "חלקי", "available": "זמין" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "ייקבע בהמשך" }, "mediaRequests-requestStats": { @@ -2017,8 +2028,8 @@ }, "quickCreateApp": { "modal": { - "title": "", - "createAndUse": "" + "title": "צור אפליקציה תוך כדי תנועה", + "createAndUse": "צור והשתמש" } } }, @@ -2095,7 +2106,7 @@ "label": "אטימות" }, "iconColor": { - "label": "" + "label": "צבע סמל" }, "customCss": { "label": "עיצוב מותאם אישית עבור הלוח הזה", @@ -2113,14 +2124,14 @@ "label": "ספירת עמודות" }, "itemRadius": { - "label": "", - "description": "", + "label": "רדיוס פריט", + "description": "משנה את העגלגלות של האריחים על הלוח שלך", "option": { - "xs": "", - "sm": "", - "md": "", - "lg": "", - "xl": "" + "xs": "קטן מאוד", + "sm": "קטן", + "md": "בינוני", + "lg": "גדול", + "xl": "גדול מאוד" } }, "name": { @@ -2144,9 +2155,9 @@ "layout": { "title": "פריסה", "responsive": { - "title": "", + "title": "פריסות רספונסיביות", "action": { - "add": "" + "add": "הוסף פריסה" } } }, @@ -2154,7 +2165,7 @@ "title": "רקע" }, "appearance": { - "title": "" + "title": "מראה" }, "customCss": { "title": "עיצוב מותאם אישית" @@ -2259,14 +2270,14 @@ "layout": { "field": { "name": { - "label": "" + "label": "שם" }, "columnCount": { - "label": "" + "label": "ספירת עמודות" }, "breakpoint": { - "label": "", - "description": "" + "label": "נקודת עצירה", + "description": "הפריסה תשמש בכל המסכים הגדולים מנקודת העצירה הזו עד לנקודת העצירה הגדולה הבאה." } } }, diff --git a/packages/translation/src/lang/hr.json b/packages/translation/src/lang/hr.json index 75779c02d..53c2cc407 100644 --- a/packages/translation/src/lang/hr.json +++ b/packages/translation/src/lang/hr.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "", "available": "" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/hu.json b/packages/translation/src/lang/hu.json index d836e181d..b44502daa 100644 --- a/packages/translation/src/lang/hu.json +++ b/packages/translation/src/lang/hu.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Részleges", "available": "Elérhető" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/it.json b/packages/translation/src/lang/it.json index 71553839d..b0d2a6cad 100644 --- a/packages/translation/src/lang/it.json +++ b/packages/translation/src/lang/it.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Parziale", "available": "Disponibile" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/ja.json b/packages/translation/src/lang/ja.json index 543a7c58d..e1430e52a 100644 --- a/packages/translation/src/lang/ja.json +++ b/packages/translation/src/lang/ja.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "一部", "available": "利用可能" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/ko.json b/packages/translation/src/lang/ko.json index 9be540671..ab3e881a8 100644 --- a/packages/translation/src/lang/ko.json +++ b/packages/translation/src/lang/ko.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "", "available": "" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/lt.json b/packages/translation/src/lang/lt.json index 1013e8b8a..ebcb179c4 100644 --- a/packages/translation/src/lang/lt.json +++ b/packages/translation/src/lang/lt.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Dalis", "available": "Galima" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/lv.json b/packages/translation/src/lang/lv.json index f16d74463..4cd1ca5a3 100644 --- a/packages/translation/src/lang/lv.json +++ b/packages/translation/src/lang/lv.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Daļējs", "available": "Pieejams" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/nl.json b/packages/translation/src/lang/nl.json index dead99aa3..0ead03684 100644 --- a/packages/translation/src/lang/nl.json +++ b/packages/translation/src/lang/nl.json @@ -972,6 +972,11 @@ "create": "Nieuwe dynamische sectie", "remove": "Dynamische sectie verwijderen" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "Dynamische sectie verwijderen", "message": "Weet je zeker dat je deze dynamische sectie wilt verwijderen? Items worden verplaatst naar dezelfde locatie in de bovenliggende sectie." @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Gedeeltelijk", "available": "Beschikbaar" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "TBD" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/no.json b/packages/translation/src/lang/no.json index 909239d2c..1e5af8ff9 100644 --- a/packages/translation/src/lang/no.json +++ b/packages/translation/src/lang/no.json @@ -972,6 +972,11 @@ "create": "Ny dynamisk seksjon", "remove": "Fjern dynamisk seksjon" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "Fjern dynamisk seksjon", "message": "Er du sikker på at du vil fjerne denne dynamiske delen? Elementer vil bli flyttet til samme sted i overordnet seksjon." @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Delvis", "available": "Tilgjengelig" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "Uavklart" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/pl.json b/packages/translation/src/lang/pl.json index ac4196b27..71f6953be 100644 --- a/packages/translation/src/lang/pl.json +++ b/packages/translation/src/lang/pl.json @@ -972,6 +972,11 @@ "create": "Nowa sekcja dynamiczna", "remove": "Usuń sekcję dynamiczną" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "Usuń sekcję dynamiczną", "message": "Czy na pewno chcesz usunąć tę sekcję dynamiczną? Elementy zostaną przeniesione w tej samej lokalizacji w sekcji nadrzędnej." @@ -1816,6 +1821,12 @@ "partiallyAvailable": "", "available": "Dostępne" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/pt.json b/packages/translation/src/lang/pt.json index c61b359e0..7aaea8fd5 100644 --- a/packages/translation/src/lang/pt.json +++ b/packages/translation/src/lang/pt.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Parcial", "available": "Disponível" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/ro.json b/packages/translation/src/lang/ro.json index fb3a80086..22ecc7e14 100644 --- a/packages/translation/src/lang/ro.json +++ b/packages/translation/src/lang/ro.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Parțial", "available": "Disponibil" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/ru.json b/packages/translation/src/lang/ru.json index 8ef51cf99..28fda401f 100644 --- a/packages/translation/src/lang/ru.json +++ b/packages/translation/src/lang/ru.json @@ -307,12 +307,12 @@ "name": "Название", "members": "Участники", "homeBoard": { - "label": "", - "description": "" + "label": "Домашняя панель", + "description": "Можно выбрать только панели, доступные для группы" }, "mobileBoard": { - "label": "", - "description": "" + "label": "Мобильная панель", + "description": "Можно выбрать только панели, доступные для группы" } }, "permission": { @@ -514,12 +514,12 @@ "board": { "notification": { "success": { - "title": "", - "message": "" + "title": "Настройки сохранены", + "message": "Настройки панели успешно сохранены" }, "error": { - "title": "", - "message": "" + "title": "Не удалось сохранить настройки", + "message": "Невозможно сохранить настройки панели" } } } @@ -527,17 +527,17 @@ "changePosition": { "notification": { "success": { - "message": "" + "message": "Позиция успешно изменена" }, "error": { - "message": "" + "message": "Невозможно изменить позицию" } } } }, "defaultGroup": { - "name": "", - "description": "" + "name": "Группа по умолчанию", + "description": "{name} - Все авторизованные пользователи" } }, "app": { @@ -603,8 +603,8 @@ }, "useDifferentUrlForPing": { "checkbox": { - "label": "", - "description": "" + "label": "Использовать другой URL для проверки доступности", + "description": "Полезно, если Homarr имеет прямой доступ через внутреннее имя хоста или локальную сеть, что позволит избежать расхода трафика провайдера" } } }, @@ -930,7 +930,7 @@ }, "dangerZone": "Опасная зона", "noResults": "Ничего не найдено", - "unsavedChanges": "", + "unsavedChanges": "У вас есть несохранённые изменения!", "preview": { "show": "Показать предпросмотр", "hide": "Скрыть предпросмотр" @@ -972,6 +972,11 @@ "create": "Новый динамический элемент", "remove": "Удалить динамический элемент" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "Удалить динамический элемент", "message": "Вы уверены, что хотите удалить этот динамический элемент? Содержимое будет перемещено в то же место в родительском элементе." @@ -991,7 +996,7 @@ "moveDown": "Переместить вниз", "createAbove": "Создать категорию выше", "createBelow": "Создать категорию ниже", - "openAllInNewTabs": "Открыть все во вкладках" + "openAllInNewTabs": "Открыть всё в новых вкладках" }, "create": { "title": "Новая категория", @@ -1012,8 +1017,8 @@ } }, "openAllInNewTabs": { - "title": "Открыть все во вкладках", - "text": "Некоторые браузеры могут блокировать широкое открытие вкладок по соображениям безопасности. Homarr не смог открыть все окна, потому что ваш браузер заблокировал это действие. Пожалуйста, разрешите \"Открыть всплывающие окна\" и повторите попытку." + "title": "Открыть всё в новых вкладках", + "text": "Некоторые браузеры могут блокировать массовое открытие вкладок из соображений безопасности. Homarr не смог открыть все окна, поскольку ваш браузер заблокировал это действие. Пожалуйста, разрешите \"Открывать всплывающие окна\" и повторите попытку." } } }, @@ -1244,11 +1249,11 @@ }, "customTimeFormat": { "label": "Пользовательский формат времени", - "description": "Использовать ISO 8601 для форматирования времени (это переопределит другие параметры)" + "description": "Используйте формат ISO 8601 для отображения времени (это переопределит другие настройки)" }, "customDateFormat": { "label": "Пользовательский формат даты", - "description": "Использовать ISO 8601 для форматирования даты (это переопределит другие параметры)" + "description": "Используйте формат ISO 8601 для отображения даты (это переопределит другие настройки)" } } }, @@ -1431,11 +1436,11 @@ "label": "Температура в градусах Фаренгейта" }, "disableTemperatureDecimals": { - "label": "Отключить десятичные дроби у температур" + "label": "Отключить десятичные значения температуры" }, "showCurrentWindSpeed": { "label": "Показать текущую скорость ветра", - "description": "Только при текущей погоде" + "description": "Только для текущей погоды" }, "location": { "label": "Местоположение" @@ -1459,8 +1464,8 @@ "dailyForecast": { "sunrise": "Восход", "sunset": "Закат", - "maxWindSpeed": "Максимальная скорость ветра: {maxWindSpeed} км/ч", - "maxWindGusts": "Максимальные порывы ветра: {maxWindGusts} км/ч" + "maxWindSpeed": "Макс. скорость ветра: {maxWindSpeed} км/ч", + "maxWindGusts": "Макс. порывы ветра: {maxWindGusts} км/ч" }, "kind": { "clear": "Ясно", @@ -1615,7 +1620,7 @@ "app": { "noData": "Приложения не найдены", "description": "Нажмите , чтобы создать новое приложение", - "quickCreate": "" + "quickCreate": "Быстрое создание приложения" }, "error": { "noIntegration": "Интеграция не выбрана", @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Частично доступно", "available": "Доступно" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "Будет определено позже" }, "mediaRequests-requestStats": { @@ -2017,8 +2028,8 @@ }, "quickCreateApp": { "modal": { - "title": "", - "createAndUse": "" + "title": "Быстрое создание приложения", + "createAndUse": "Создать и использовать" } } }, @@ -2095,7 +2106,7 @@ "label": "Прозрачность" }, "iconColor": { - "label": "" + "label": "Цвет значка" }, "customCss": { "label": "Пользовательский CSS для этой панели", @@ -2106,21 +2117,21 @@ } }, "disableStatus": { - "label": "", - "description": "" + "label": "Отключить проверку состояния приложений", + "description": "Отключает проверку состояния для всех приложений на этой панели" }, "columnCount": { "label": "Количество столбцов" }, "itemRadius": { - "label": "", - "description": "", + "label": "Закругление элементов", + "description": "Изменяет скругление углов плиток на вашей панели", "option": { - "xs": "", - "sm": "", - "md": "", - "lg": "", - "xl": "" + "xs": "Очень маленькое", + "sm": "Маленькое", + "md": "Среднее", + "lg": "Большое", + "xl": "Очень большое" } }, "name": { @@ -2144,9 +2155,9 @@ "layout": { "title": "Компоновка", "responsive": { - "title": "", + "title": "Адаптивные макеты", "action": { - "add": "" + "add": "Добавить макет" } } }, @@ -2154,13 +2165,13 @@ "title": "Фон" }, "appearance": { - "title": "" + "title": "Внешний вид" }, "customCss": { "title": "Пользовательский CSS" }, "behavior": { - "title": "" + "title": "Поведение" }, "access": { "title": "Управление доступом", @@ -2259,14 +2270,14 @@ "layout": { "field": { "name": { - "label": "" + "label": "Название" }, "columnCount": { - "label": "" + "label": "Количество столбцов" }, "breakpoint": { - "label": "", - "description": "" + "label": "Точка перехода", + "description": "Этот макет будет применяться для экранов, ширина которых больше указанного здесь значения, вплоть до следующей заданной точки перехода с большим значением." } } }, @@ -2499,10 +2510,10 @@ "ownerOfGroupDeleted": "Владелец этой группы был удалён. В данный момент у группы нет владельца." }, "setting": { - "title": "", - "alert": "", + "title": "Настройки", + "alert": "Настройки групп имеют приоритет согласно порядку групп в списке. Верхние настройки перезаписывают нижние.", "board": { - "title": "" + "title": "Панели" } }, "members": { @@ -2576,14 +2587,14 @@ "description": "Для выбора доступны только публичные панели" }, "status": { - "title": "", + "title": "Состояние приложения", "enableStatusByDefault": { - "label": "", - "description": "" + "label": "Включать состояние по умолчанию", + "description": "При добавлении элемента приложения проверка состояния будет включена по умолчанию" }, "forceDisableStatus": { - "label": "", - "description": "" + "label": "Принудительно отключить проверку состояния", + "description": "Проверка состояния приложений будет отключена для всех пользователей и не может быть включена" } } }, @@ -2696,7 +2707,7 @@ "modal": { "delete": { "title": "Удалить API-токен", - "text": "Это навсегда удалит API токен. API клиенты с помощью этого токена больше не могут аутентифицировать и выполнить API запросы. Это действие нельзя отменить." + "text": "Это действие навсегда удалит токен API. Клиенты API, использующие этот токен, больше не смогут авторизоваться и выполнять запросы API. Это действие нельзя отменить." } }, "table": { diff --git a/packages/translation/src/lang/sk.json b/packages/translation/src/lang/sk.json index 401845246..d75620f5e 100644 --- a/packages/translation/src/lang/sk.json +++ b/packages/translation/src/lang/sk.json @@ -972,6 +972,11 @@ "create": "Nová dynamická sekcia", "remove": "Nová dynamická sekcia" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "Nová dynamická sekcia", "message": "Naozaj chcete odstrániť túto dynamickú sekciu? Položky sa presunú na rovnaké miesto v nadradenej sekcii." @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Čiastočný", "available": "K dispozícii" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "TBD" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/sl.json b/packages/translation/src/lang/sl.json index d54cabce8..c5c4de2d6 100644 --- a/packages/translation/src/lang/sl.json +++ b/packages/translation/src/lang/sl.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "", "available": "" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/sv.json b/packages/translation/src/lang/sv.json index a11827f08..3a0bbed45 100644 --- a/packages/translation/src/lang/sv.json +++ b/packages/translation/src/lang/sv.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Delvis", "available": "Tillgänglig" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/tr.json b/packages/translation/src/lang/tr.json index 5df5d4c99..f82d8348f 100644 --- a/packages/translation/src/lang/tr.json +++ b/packages/translation/src/lang/tr.json @@ -12,7 +12,7 @@ }, "import": { "title": "Verileri İçe Aktar", - "subtitle": "Mevcut Homarr Örneğinden Alınan Verileri İçe Aktarabilirsiniz.", + "subtitle": "Mevcut bir Homarr örneğinden veri içe aktarabilirsiniz.", "dropzone": { "title": "Zip dosyasını buraya sürükleyin veya göz atmak için tıklayın", "description": "Seçtiğiniz zip işlenecek ve içe aktarmak istediğiniz verileri seçebileceksiniz" @@ -24,7 +24,7 @@ }, "importSettings": { "title": "İçe Aktarma Ayarları", - "description": "İçe Aktarma İşlevlerini Yapılandırın" + "description": "İçe Aktarma İşlevlerini Yapılandır" }, "boardSelection": { "title": "{count} Panel Bulundu", @@ -48,17 +48,17 @@ } }, "tokenModal": { - "title": "İçe Aktarma Token'ını Girin", + "title": "İçe Aktarma Anahtarını Girin", "field": { "token": { - "label": "Token", - "description": "Önceki Homarr Örneğinizden Aldığınız İçe Aktarma Token'ını girin" + "label": "Token Anahtarı", + "description": "Önceki Homarr Örneğinizden Aldığınız İçe Aktarma Anahtarı girin" } }, "notification": { "error": { "title": "Token", - "message": "Girdiğiniz Token Geçersiz" + "message": "Girdiğiniz Token Anahtarı Geçersiz" } } } @@ -72,12 +72,12 @@ "message": "Kullanıcı başarıyla oluşturuldu" }, "error": { - "title": "Kullanıcı oluşturma başarısız oldu" + "title": "Kullanıcı oluşturma başarısız" } } }, "group": { - "title": "Dış grup", + "title": "Harici grup", "subtitle": "Harici kullanıcılar için kullanılacak grubu belirtin.", "form": { "name": { @@ -119,7 +119,7 @@ }, "init": { "title": "Yeni Homarr Kurulumu", - "subtitle": "Lütfen yönetici kullanıcısını oluşturun" + "subtitle": "Lütfen yönetici için kullanıcı oluşturun" } }, "field": { @@ -150,7 +150,7 @@ "label": "Ana panel" }, "pingIconsEnabled": { - "label": "Pingler İçin Simgeler Kullanın" + "label": "Pingler İçin Simge Kullanın" }, "defaultSearchEngine": { "label": "Öntanımlı arama motoru" @@ -291,7 +291,7 @@ "confirm": "{username} kullanıcısını tercihleriyle birlikte silmek istediğinizden emin misiniz?" }, "select": { - "label": "Kullanıcıyı seç", + "label": "Kullanıcı seç", "notFound": "Kullanıcı bulunmadı" }, "transfer": { @@ -363,7 +363,7 @@ }, "full-all": { "label": "Tam panel erişimi", - "description": "Üyelerin tüm panelleri görüntülemesine, değiştirmesine ve silmesine izin verin (Erişim kontrolü ve tehlikeli alan dahil)" + "description": "Üyelerin tüm panelleri görüntülemesine, değiştirmesine ve silmesine izin ver (Erişim kontrolü ve Tehlikeli alan dahil)" } } }, @@ -371,16 +371,16 @@ "title": "Entegrasyonlar", "item": { "create": { - "label": "Entegrasyonlar oluştur", - "description": "Üyelerin entegrasyonlar oluşturmasına izin ver" + "label": "Entegrasyon oluştur", + "description": "Üyelerin entegrasyon oluşturmasına izin ver" }, "use-all": { "label": "Tüm entegrasyonları kullan", - "description": "Üyelerin panellere herhangi bir entegrasyonu eklemelerine olanak tanır" + "description": "Üyelerin panellere herhangi bir entegrasyon eklemelerine olanak tanır" }, "interact-all": { "label": "Herhangi bir entegrasyonla etkileşim kurun", - "description": "Üyelerin herhangi bir entegrasyonla etkileşime girmesine izin verin" + "description": "Üyelerin herhangi bir entegrasyonla etkileşime girmesine izin ver" }, "full-all": { "label": "Tam entegrasyon erişimi", @@ -418,8 +418,8 @@ "title": "Arama Motorları", "item": { "create": { - "label": "Arama motorları oluşturun", - "description": "Üyelerin arama motorları oluşturmasına izin ver" + "label": "Arama motoru oluştur", + "description": "Üyelerin arama motoru oluşturmasına izin ver" }, "modify-all": { "label": "Tüm arama motorlarını değiştir", @@ -433,8 +433,8 @@ } }, "memberNotice": { - "mixed": "Bazı üyeler harici sağlayıcılardandır ve burada yönetilemezler", - "external": "Tüm üyeler harici sağlayıcılardandır ve burada yönetilemezler" + "mixed": "Bazı üyeler harici sağlayıcılardandır ve buradan yönetilemezler", + "external": "Tüm üyeler harici sağlayıcılardandır ve buradan yönetilemezler" }, "reservedNotice": { "message": "Bu grup sistem kullanımı için ayrılmıştır ve bazı eylemleri kısıtlar. " @@ -621,7 +621,7 @@ "title": "Entegrasyonlar", "search": "Entegrasyon ara", "noResults": { - "title": "Henüz entegrasyon yok" + "title": "Henüz entegrasyon oluşturulmadı" } }, "create": { @@ -685,7 +685,7 @@ "create": "Bağlantıyı test et ve oluştur", "edit": "Bağlantıyı test et ve kaydet" }, - "alertNotice": "Bağlantı kurulumu başaralı olduğunda Kaydet düğmesi etkinleştirilir", + "alertNotice": "Bağlantı başarılı olduğunda Kaydet butonu etkinleştirilir", "notification": { "success": { "title": "Bağlantı başarılı", @@ -716,7 +716,7 @@ "message": "Muhtemelen yanlış kimlik bilgileri" }, "forbidden": { - "title": "Yasaklı", + "title": "Yetkisiz Erişim", "message": "Muhtemelen izinler eksik" }, "notFound": { @@ -748,7 +748,7 @@ "message": "Yanıt geçerli bir JSON değildi" }, "wrongPath": { - "title": "Yanlış yol", + "title": " Yol hatalı", "message": "Yol muhtemelen doğru değil" } } @@ -783,12 +783,12 @@ "newLabel": "Yeni Şifre" }, "tokenId": { - "label": "Token Kimliği", - "newLabel": "Yeni Token Kimliği" + "label": "Token Anahtar Kimliği", + "newLabel": "Yeni Anahtar Kimliği" }, "realm": { - "label": "Realm", - "newLabel": "Yeni realm" + "label": "Erişim Alanı", + "newLabel": "Yeni erişim alanı" } } }, @@ -861,7 +861,7 @@ "continue": "Devam Et", "previous": "Önceki", "next": "İleri", - "checkoutDocs": "Dökümanlara göz atın", + "checkoutDocs": "Dökümanlara göz at", "checkLogs": "Daha fazla ayrıntı için günlükleri kontrol edin", "tryAgain": "Tekrar Deneyin", "loading": "Yükleniyor" @@ -972,6 +972,11 @@ "create": "Yeni Dinamik Bölüm", "remove": "Dinamik bölümü kaldır" }, + "option": { + "borderColor": { + "label": "Kenarlık rengi" + } + }, "remove": { "title": "Dinamik bölümü kaldır", "message": "Bu dinamik bölümü kaldırmak istediğinizden emin misiniz? Öğeler üst bölümdeki aynı konuma taşınacaktır." @@ -1700,8 +1705,8 @@ "columnTitle": "Kontroller" }, "added": { - "columnTitle": "Ekleme", - "detailsTitle": "Ekleme Tarihi" + "columnTitle": "Eklenme", + "detailsTitle": "Eklenme Tarihi" }, "category": { "columnTitle": "Ekstralar", @@ -1816,11 +1821,17 @@ "partiallyAvailable": "Kısmi", "available": "Mevcut" }, + "status": { + "pending": "Bekleyen", + "approved": "Onaylandı", + "declined": "Reddedildi", + "failed": "Başarısız" + }, "toBeDetermined": "-Yapım Aşamasında-" }, "mediaRequests-requestStats": { "name": "Medya Talep İstatistikleri", - "description": "Medya taleplerinizle ilgili istatistikler", + "description": "Medya talepleriyle ilgili istatistikler", "option": {}, "titles": { "stats": { @@ -1852,7 +1863,7 @@ } }, "tab": { - "workers": "İşçiler", + "workers": "İşler", "queue": "Kuyruk", "statistics": "İstatistik" }, @@ -2063,11 +2074,11 @@ "description": "Görsel tekrarlanmaz ve tüm alanı doldurmayabilir." }, "repeat-x": { - "label": "Y tekrarla", + "label": "Y eksende tekrarla", "description": "'Tekrarla' ile aynı, ancak yalnızca yatay eksende." }, "repeat-y": { - "label": "D tekrarla", + "label": "D eksende tekrarla", "description": "'Tekrarla' ile aynı ancak sadece dikey eksende." } } @@ -2244,14 +2255,14 @@ "notice": "Bu sayfayı tüm kullanıcılardan gizlemek için sunucuya öntanımlı panel atayın" }, "user": { - "description": "Henüz bir ev paneli belirlemediniz.", + "description": "Henüz öntanımlı panel belirlemediniz.", "link": "Öntanımlı paneli yapılandır", "notice": "Bu sayfanın kaybolmasını sağlamak için tercihlerden öntanımlı panel seçin" }, "anonymous": { - "description": "Sunucu yöneticiniz henüz bir öntanımlı panel belirlemedi.", + "description": "Sunucu yöneticiniz henüz öntanımlı panel belirlemedi.", "link": "Herkese açık panelleri görüntüle", - "notice": "Bu sayfanın kaybolmasını sağlamak için sunucu yöneticisinden öntanımlı panel ayarlamasını isteyin" + "notice": "Bu sayfanın kaybolmasını sağlamak için sunucu yöneticisinden öntanımlı panel atamasını isteyin" } } } @@ -2484,7 +2495,7 @@ "label": "Son geçerlilik tarihi" }, "token": { - "label": "Token" + "label": "Token Anahtarı" } } } @@ -2533,11 +2544,11 @@ "title": "Analiz", "general": { "title": "Anonim analizler gönder", - "text": "Homarr, açık kaynaklı yazılım Umami'yi kullanarak anonimleştirilmiş analizler gönderecektir. Hiçbir zaman kişisel bilgi toplamaz ve bu nedenle tamamen GDPR ve CCPA uyumludur. Analizleri etkinleştirmenizi öneririz çünkü, açık kaynak yazılım ekibimizin sorunları daha hızlı belirlemesine ve işlerimizi önceliklendirmesine yardımcı olmaktadır." + "text": "Homarr, açık kaynaklı yazılım Umami'yi kullanarak anonimleştirilmiş analizler gönderecektir. Hiçbir zaman kişisel bilgi toplamaz ve bu nedenle tamamen GDPR ve CCPA uyumludur. Analizleri etkinleştirmenizi öneririz çünkü, açık kaynak yazılım ekibimizin sorunları daha hızlı belirlemesine ve birikmiş işlerimizi önceliklendirmesine yardımcı olmaktadır." }, "widgetData": { - "title": "Widget verileri", - "text": "Araç (ve sayılarını) yapılandırma verilerini gönder. URL'leri, alan adlarını veya başka herhangi bir veriyi içermez." + "title": "Araç verileri", + "text": "Hangi araçları (ve miktarlarını) yapılandırdığınızı gönderin. URL'leri, adları veya başka herhangi bir veri içermez." }, "integrationData": { "title": "Entegrasyon verileri", @@ -2550,7 +2561,7 @@ }, "crawlingAndIndexing": { "title": "Arama ve İndeksleme", - "warning": "Buradaki ayarların etkinleştirilmesi veya devre dışı bırakılması, arama motorlarının sayfanızı arama ve indekleme şeklini ciddi şekilde etkileyecektir. Her ayar bir istek başlatır ve bu ayarları almak tarayıcının görevidir. Değişiklikler birkaç gün veya hafta sürebilir. Bazı ayarlar arama motoruna özel olabilir.", + "warning": "Buradaki ayarların etkinleştirilmesi veya devre dışı bırakılması, arama motorlarının sayfanızı arama ve indekleme şeklini ciddi şekilde etkileyecektir. Her ayar bir istek başlatır ve bu ayarları uygulamak tarayıcının görevidir. Değişiklikler birkaç gün veya hafta sürebilir. Bazı ayarlar arama motoruna özel olabilir.", "noIndex": { "title": "İndeksleme Yapma", "text": "Web sitesini arama motorlarında indekslemeyin veya herhangi bir arama sonucunda görüntülemeyin" @@ -2678,8 +2689,8 @@ "title": "API", "modal": { "createApiToken": { - "title": "API token'ı oluşturuldu", - "description": "API anahtarı oluşturuldu. Dikkatli olun, bu anahtar veritabanında şifrelenmiştir ve size bir daha asla aktarılmayacaktır. Bu anaharı kaybederseniz, bir daha erişmeniz mümkün değildir.", + "title": "API anahtarı oluşturuldu", + "description": "API anahtarı oluşturuldu. Dikkatli olun, bu anahtar veritabanında şifrelenmiştir ve size bir daha asla aktarılmayacaktır. Bu anahtarı kaybederseniz, bir daha erişmeniz mümkün değildir.", "button": "Kopyala ve kapat" } }, @@ -2691,12 +2702,12 @@ "label": "Kimlik doğrulama", "title": "API Anahtarları", "button": { - "createApiToken": "API token'ı oluştur" + "createApiToken": "API anahtarı oluştur" }, "modal": { "delete": { - "title": "API belirtecini sil", - "text": "Bu, API belirtecini kalıcı olarak silecektir. Bu belirteci kullanan API istemcileri artık kimlik doğrulaması yapamaz ve API istekleri gerçekleştiremez. Bu eylem geri alınamaz." + "title": "API anahtarını sil", + "text": "Bu, API anahtarı kalıcı olarak silecektir. Bu anahtarı kullanan API istemcileri artık kimlik doğrulaması yapamaz ve API istekleri gerçekleştiremez. Bu eylem geri alınamaz." } }, "table": { @@ -2765,7 +2776,7 @@ "label": "Başlat", "notification": { "success": { - "title": "Konteynerler başladı", + "title": "Konteynerler başlatıldı", "message": "Konteynerler başarıyla başlatıldı" }, "error": { diff --git a/packages/translation/src/lang/uk.json b/packages/translation/src/lang/uk.json index 1f60c0856..9c7b88f9d 100644 --- a/packages/translation/src/lang/uk.json +++ b/packages/translation/src/lang/uk.json @@ -307,11 +307,11 @@ "name": "Ім’я", "members": "Учасники", "homeBoard": { - "label": "", + "label": "Домашня дошка", "description": "" }, "mobileBoard": { - "label": "", + "label": "Мобільна дошка", "description": "" } }, @@ -518,8 +518,8 @@ "message": "" }, "error": { - "title": "", - "message": "" + "title": "Не вдалося зберегти налаштування", + "message": "Не вдалося зберегти налаштування дошки" } } } @@ -536,7 +536,7 @@ } }, "defaultGroup": { - "name": "", + "name": "Група за замовчуванням", "description": "" } }, @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Частково доступно", "available": "Доступно" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/vi.json b/packages/translation/src/lang/vi.json index cdf16c3a0..a57377fc7 100644 --- a/packages/translation/src/lang/vi.json +++ b/packages/translation/src/lang/vi.json @@ -972,6 +972,11 @@ "create": "", "remove": "" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "", "message": "" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "Một phần", "available": "Khả dụng" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "" }, "mediaRequests-requestStats": { diff --git a/packages/translation/src/lang/zh.json b/packages/translation/src/lang/zh.json index 6ad11faeb..6bbf39eab 100644 --- a/packages/translation/src/lang/zh.json +++ b/packages/translation/src/lang/zh.json @@ -972,6 +972,11 @@ "create": "創建動態區段", "remove": "移除動態區段" }, + "option": { + "borderColor": { + "label": "" + } + }, "remove": { "title": "移除動態區段", "message": "確認要移除此動態區段嗎?項目將移動到父區段的相同位置" @@ -1816,6 +1821,12 @@ "partiallyAvailable": "部分", "available": "待定" }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, "toBeDetermined": "多媒體請求狀態" }, "mediaRequests-requestStats": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 0d2e8880e..1b90534e4 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -2,7 +2,7 @@ "name": "@homarr/ui", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", @@ -29,10 +29,10 @@ "@homarr/log": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/core": "^7.17.0", - "@mantine/dates": "^7.17.0", - "@mantine/hooks": "^7.17.0", - "@tabler/icons-react": "^3.30.0", + "@mantine/core": "^7.17.1", + "@mantine/dates": "^7.17.1", + "@mantine/hooks": "^7.17.1", + "@tabler/icons-react": "^3.31.0", "mantine-react-table": "2.0.0-beta.9", "next": "15.1.7", "react": "19.0.0", diff --git a/packages/validation/package.json b/packages/validation/package.json index f3838c597..737a8a6fa 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -2,7 +2,7 @@ "name": "@homarr/validation", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index d57bc47e9..caf2ccee4 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -32,6 +32,7 @@ export { sectionSchema, itemAdvancedOptionsSchema, sharedItemSchema, + dynamicSectionOptionsSchema, type BoardItemAdvancedOptions, type BoardItemIntegration, } from "./shared"; diff --git a/packages/validation/src/shared.ts b/packages/validation/src/shared.ts index a0eda19be..0080254c0 100644 --- a/packages/validation/src/shared.ts +++ b/packages/validation/src/shared.ts @@ -58,9 +58,14 @@ const emptySectionSchema = z.object({ xOffset: z.number(), }); +export const dynamicSectionOptionsSchema = z.object({ + borderColor: z.string().default(""), +}); + const dynamicSectionSchema = z.object({ id: z.string(), kind: z.literal("dynamic"), + options: dynamicSectionOptionsSchema, layouts: z.array( z.object({ layoutId: z.string(), diff --git a/packages/widgets/package.json b/packages/widgets/package.json index 38d462945..872827e17 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -2,7 +2,7 @@ "name": "@homarr/widgets", "version": "0.1.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { ".": "./index.ts", @@ -44,9 +44,9 @@ "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/core": "^7.17.0", - "@mantine/hooks": "^7.17.0", - "@tabler/icons-react": "^3.30.0", + "@mantine/core": "^7.17.1", + "@mantine/hooks": "^7.17.1", + "@tabler/icons-react": "^3.31.0", "@tiptap/extension-color": "2.11.5", "@tiptap/extension-highlight": "2.11.5", "@tiptap/extension-image": "2.11.5", diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx index 106ca07c4..1e4111fb0 100644 --- a/packages/widgets/src/media-requests/list/component.tsx +++ b/packages/widgets/src/media-requests/list/component.tsx @@ -41,14 +41,12 @@ export default function MediaServerWidget({ const newData = filteredData.concat( data.requests.map((request) => ({ ...request, integrationId: data.integrationId })), ); - return newData.sort(({ status: statusA }, { status: statusB }) => { - if (statusA === MediaRequestStatus.PendingApproval) { - return -1; + return newData.sort((dataA, dataB) => { + if (dataA.status === dataB.status) { + return dataB.createdAt.getTime() - dataA.createdAt.getTime(); } - if (statusB === MediaRequestStatus.PendingApproval) { - return 1; - } - return 0; + + return dataA.status - dataB.status; }); }); }, @@ -69,7 +67,7 @@ export default function MediaServerWidget({ {mediaRequests.map((mediaRequest) => ( - {mediaRequest.status === MediaRequestStatus.PendingApproval && ( + {mediaRequest.status === MediaRequestStatus.PendingApproval ? ( + ) : ( + )} @@ -199,6 +199,34 @@ export default function MediaServerWidget({ ); } +const statusMapping = { + [MediaRequestStatus.PendingApproval]: { color: "blue", label: (t) => t("pending") }, + [MediaRequestStatus.Approved]: { color: "green", label: (t) => t("approved") }, + [MediaRequestStatus.Declined]: { color: "red", label: (t) => t("declined") }, + [MediaRequestStatus.Failed]: { color: "red", label: (t) => t("failed") }, +} satisfies Record< + MediaRequestStatus, + { + color: string; + label: (t: ScopedTranslationFunction<"widget.mediaRequests-requestList.status">) => string; + } +>; + +interface StatusBadgeProps { + status: MediaRequestStatus; +} + +const StatusBadge = ({ status }: StatusBadgeProps) => { + const { color, label } = statusMapping[status]; + const tStatus = useScopedI18n("widget.mediaRequests-requestList.status"); + + return ( + + {label(tStatus)} + + ); +}; + function getAvailabilityProperties( mediaRequestAvailability: MediaAvailability, t: ScopedTranslationFunction<"widget.mediaRequests-requestList">, @@ -208,10 +236,10 @@ function getAvailabilityProperties( return { color: "green", label: t("availability.available") }; case MediaAvailability.PartiallyAvailable: return { color: "yellow", label: t("availability.partiallyAvailable") }; - case MediaAvailability.Pending: - return { color: "violet", label: t("availability.pending") }; case MediaAvailability.Processing: return { color: "blue", label: t("availability.processing") }; + case MediaAvailability.Pending: + return { color: "violet", label: t("availability.pending") }; default: return { color: "red", label: t("availability.unknown") }; } diff --git a/packages/widgets/src/media-server/index.ts b/packages/widgets/src/media-server/index.ts index 5e9fe1f3a..c7f4aeea5 100644 --- a/packages/widgets/src/media-server/index.ts +++ b/packages/widgets/src/media-server/index.ts @@ -7,5 +7,5 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaServ createOptions() { return {}; }, - supportedIntegrations: ["jellyfin", "plex"], + supportedIntegrations: ["jellyfin", "plex", "emby"], }).withDynamicImport(() => import("./component")); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5a4e02ce..3281da641 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,10 +39,10 @@ importers: version: 14.0.3(semantic-release@24.2.3(typescript@5.8.2)) '@turbo/gen': specifier: ^2.4.4 - version: 2.4.4(@types/node@22.13.5)(typescript@5.8.2) + version: 2.4.4(@types/node@22.13.9)(typescript@5.8.2) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@5.4.5(@types/node@22.13.5)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) + version: 4.3.4(vite@5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) '@vitest/coverage-v8': specifier: ^3.0.7 version: 3.0.7(vitest@3.0.7) @@ -59,8 +59,8 @@ importers: specifier: ^26.0.0 version: 26.0.0 prettier: - specifier: ^3.5.2 - version: 3.5.2 + specifier: ^3.5.3 + version: 3.5.3 semantic-release: specifier: ^24.2.3 version: 24.2.3(typescript@5.8.2) @@ -75,10 +75,10 @@ importers: version: 5.8.2 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.8.2)(vite@5.4.5(@types/node@22.13.5)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) + version: 5.1.4(typescript@5.8.2)(vite@5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) vitest: specifier: ^3.0.7 - version: 3.0.7(@types/node@22.13.5)(@vitest/ui@3.0.7)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) + version: 3.0.7(@types/node@22.13.9)(@vitest/ui@3.0.7)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) apps/nextjs: dependencies: @@ -182,50 +182,50 @@ importers: specifier: workspace:^0.1.0 version: link:../../packages/widgets '@mantine/colors-generator': - specifier: ^7.17.0 - version: 7.17.0(chroma-js@3.1.2) + specifier: ^7.17.1 + version: 7.17.1(chroma-js@3.1.2) '@mantine/core': - specifier: ^7.17.0 - version: 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mantine/dropzone': - specifier: ^7.17.0 - version: 7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mantine/hooks': - specifier: ^7.17.0 - version: 7.17.0(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(react@19.0.0) '@mantine/modals': - specifier: ^7.17.0 - version: 7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mantine/tiptap': - specifier: ^7.17.0 - version: 7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(@tiptap/extension-link@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5))(@tiptap/react@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(@tiptap/extension-link@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5))(@tiptap/react@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@million/lint': specifier: 1.0.14 version: 1.0.14(rollup@4.21.3)(webpack-sources@3.2.3) '@tabler/icons-react': - specifier: ^3.30.0 - version: 3.30.0(react@19.0.0) + specifier: ^3.31.0 + version: 3.31.0(react@19.0.0) '@tanstack/react-query': - specifier: ^5.66.11 - version: 5.66.11(react@19.0.0) + specifier: ^5.67.1 + version: 5.67.1(react@19.0.0) '@tanstack/react-query-devtools': - specifier: ^5.66.11 - version: 5.66.11(@tanstack/react-query@5.66.11(react@19.0.0))(react@19.0.0) + specifier: ^5.67.1 + version: 5.67.1(@tanstack/react-query@5.67.1(react@19.0.0))(react@19.0.0) '@tanstack/react-query-next-experimental': - specifier: ^5.66.11 - version: 5.66.11(@tanstack/react-query@5.66.11(react@19.0.0))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react@19.0.0) + specifier: ^5.67.1 + version: 5.67.1(@tanstack/react-query@5.67.1(react@19.0.0))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react@19.0.0) '@trpc/client': specifier: next - version: 11.0.0-rc.819(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(typescript@5.8.2) + version: 11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2) '@trpc/next': specifier: next - version: 11.0.0-rc.819(@tanstack/react-query@5.66.11(react@19.0.0))(@trpc/client@11.0.0-rc.819(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0-rc.819(@tanstack/react-query@5.66.11(react@19.0.0))(@trpc/client@11.0.0-rc.819(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2))(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) + version: 11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) '@trpc/react-query': specifier: next - version: 11.0.0-rc.819(@tanstack/react-query@5.66.11(react@19.0.0))(@trpc/client@11.0.0-rc.819(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) + version: 11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) '@trpc/server': specifier: next - version: 11.0.0-rc.819(typescript@5.8.2) + version: 11.0.0-rc.824(typescript@5.8.2) '@xterm/addon-canvas': specifier: ^0.7.0 version: 0.7.0(@xterm/xterm@5.5.0) @@ -258,7 +258,7 @@ importers: version: 2.12.1(@types/react@19.0.10)(react@19.0.0) mantine-react-table: specifier: 2.0.0-beta.9 - version: 2.0.0-beta.9(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(@tabler/icons-react@3.30.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 2.0.0-beta.9(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(@tabler/icons-react@3.31.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: 15.1.7 version: 15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1) @@ -309,8 +309,8 @@ importers: specifier: 3.1.1 version: 3.1.1 '@types/node': - specifier: ^22.13.5 - version: 22.13.5 + specifier: ^22.13.9 + version: 22.13.9 '@types/prismjs': specifier: ^1.26.5 version: 1.26.5 @@ -333,8 +333,8 @@ importers: specifier: ^2.1.0 version: 2.1.0(webpack@5.94.0) prettier: - specifier: ^3.5.2 - version: 3.5.2 + specifier: ^3.5.3 + version: 3.5.3 typescript: specifier: ^5.8.2 version: 5.8.2 @@ -409,8 +409,8 @@ importers: specifier: workspace:^0.1.0 version: link:../../tooling/typescript '@types/node': - specifier: ^22.13.5 - version: 22.13.5 + specifier: ^22.13.9 + version: 22.13.9 dotenv-cli: specifier: ^8.0.0 version: 8.0.0 @@ -418,8 +418,8 @@ importers: specifier: ^9.21.0 version: 9.21.0 prettier: - specifier: ^3.5.2 - version: 3.5.2 + specifier: ^3.5.3 + version: 3.5.3 tsx: specifier: 4.19.3 version: 4.19.3 @@ -473,14 +473,14 @@ importers: specifier: workspace:^0.1.0 version: link:../../tooling/typescript '@types/ws': - specifier: ^8.5.14 - version: 8.5.14 + specifier: ^8.18.0 + version: 8.18.0 eslint: specifier: ^9.21.0 version: 9.21.0 prettier: - specifier: ^3.5.2 - version: 3.5.2 + specifier: ^3.5.3 + version: 3.5.3 typescript: specifier: ^5.8.2 version: 5.8.2 @@ -580,13 +580,13 @@ importers: version: link:../validation '@trpc/client': specifier: next - version: 11.0.0-rc.819(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(typescript@5.8.2) + version: 11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2) '@trpc/react-query': specifier: next - version: 11.0.0-rc.819(@tanstack/react-query@5.66.11(react@19.0.0))(@trpc/client@11.0.0-rc.819(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) + version: 11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) '@trpc/server': specifier: next - version: 11.0.0-rc.819(typescript@5.8.2) + version: 11.0.0-rc.824(typescript@5.8.2) lodash.clonedeep: specifier: ^4.5.0 version: 4.5.0 @@ -607,7 +607,7 @@ importers: version: 2.2.2 trpc-to-openapi: specifier: ^2.1.3 - version: 2.1.3(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(zod-openapi@2.19.0(zod@3.24.2))(zod@3.24.2) + version: 2.1.3(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(zod-openapi@2.19.0(zod@3.24.2))(zod@3.24.2) zod: specifier: ^3.24.2 version: 3.24.2 @@ -625,8 +625,8 @@ importers: specifier: ^9.21.0 version: 9.21.0 prettier: - specifier: ^3.5.2 - version: 3.5.2 + specifier: ^3.5.3 + version: 3.5.3 typescript: specifier: ^5.8.2 version: 5.8.2 @@ -639,6 +639,9 @@ importers: '@auth/drizzle-adapter': specifier: ^1.8.0 version: 1.8.0 + '@homarr/certificates': + specifier: workspace:^0.1.0 + version: link:../certificates '@homarr/common': specifier: workspace:^0.1.0 version: link:../common @@ -704,8 +707,8 @@ importers: specifier: ^9.21.0 version: 9.21.0 prettier: - specifier: ^3.5.2 - version: 3.5.2 + specifier: ^3.5.3 + version: 3.5.3 typescript: specifier: ^5.8.2 version: 5.8.2 @@ -1009,8 +1012,8 @@ importers: specifier: workspace:^0.1.0 version: link:../server-settings '@mantine/core': - specifier: ^7.17.0 - version: 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@paralleldrive/cuid2': specifier: ^2.2.2 version: 2.2.2 @@ -1055,8 +1058,8 @@ importers: specifier: ^9.21.0 version: 9.21.0 prettier: - specifier: ^3.5.2 - version: 3.5.2 + specifier: ^3.5.3 + version: 3.5.3 tsx: specifier: 4.19.3 version: 4.19.3 @@ -1154,8 +1157,8 @@ importers: specifier: workspace:^0.1.0 version: link:../validation '@mantine/form': - specifier: ^7.17.0 - version: 7.17.0(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(react@19.0.0) zod: specifier: ^3.24.2 version: 3.24.2 @@ -1200,8 +1203,8 @@ importers: specifier: workspace:^0.1.0 version: link:../validation '@mantine/core': - specifier: ^7.17.0 - version: 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: 19.0.0 version: 19.0.0 @@ -1366,11 +1369,11 @@ importers: specifier: workspace:^0.1.0 version: link:../ui '@mantine/core': - specifier: ^7.17.0 - version: 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mantine/hooks': - specifier: ^7.17.0 - version: 7.17.0(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(react@19.0.0) react: specifier: 19.0.0 version: 19.0.0 @@ -1427,11 +1430,11 @@ importers: specifier: workspace:^0.1.0 version: link:../validation '@mantine/core': - specifier: ^7.17.0 - version: 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@tabler/icons-react': - specifier: ^3.30.0 - version: 3.30.0(react@19.0.0) + specifier: ^3.31.0 + version: 3.31.0(react@19.0.0) dayjs: specifier: ^1.11.13 version: 1.11.13 @@ -1470,11 +1473,11 @@ importers: specifier: workspace:^0.1.0 version: link:../ui '@mantine/notifications': - specifier: ^7.17.0 - version: 7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@tabler/icons-react': - specifier: ^3.30.0 - version: 3.30.0(react@19.0.0) + specifier: ^3.31.0 + version: 3.31.0(react@19.0.0) devDependencies: '@homarr/eslint-config': specifier: workspace:^0.2.0 @@ -1528,11 +1531,11 @@ importers: specifier: workspace:^0.1.0 version: link:../validation '@mantine/core': - specifier: ^7.17.0 - version: 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mantine/hooks': - specifier: ^7.17.0 - version: 7.17.0(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(react@19.0.0) adm-zip: specifier: 0.5.16 version: 0.5.16 @@ -1756,8 +1759,8 @@ importers: specifier: workspace:^0.1.0 version: link:../server-settings '@mantine/dates': - specifier: ^7.17.0 - version: 7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: 15.1.7 version: 15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1) @@ -1817,17 +1820,17 @@ importers: specifier: workspace:^0.1.0 version: link:../ui '@mantine/core': - specifier: ^7.17.0 - version: 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mantine/hooks': - specifier: ^7.17.0 - version: 7.17.0(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(react@19.0.0) '@mantine/spotlight': - specifier: ^7.17.0 - version: 7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@tabler/icons-react': - specifier: ^3.30.0 - version: 3.30.0(react@19.0.0) + specifier: ^3.31.0 + version: 3.31.0(react@19.0.0) jotai: specifier: ^2.12.1 version: 2.12.1(@types/react@19.0.10)(react@19.0.0) @@ -1876,7 +1879,7 @@ importers: version: 4.3.1 mantine-react-table: specifier: 2.0.0-beta.9 - version: 2.0.0-beta.9(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(@tabler/icons-react@3.30.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 2.0.0-beta.9(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(@tabler/icons-react@3.31.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: 15.1.7 version: 15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1) @@ -1924,20 +1927,20 @@ importers: specifier: workspace:^0.1.0 version: link:../validation '@mantine/core': - specifier: ^7.17.0 - version: 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mantine/dates': - specifier: ^7.17.0 - version: 7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mantine/hooks': - specifier: ^7.17.0 - version: 7.17.0(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(react@19.0.0) '@tabler/icons-react': - specifier: ^3.30.0 - version: 3.30.0(react@19.0.0) + specifier: ^3.31.0 + version: 3.31.0(react@19.0.0) mantine-react-table: specifier: 2.0.0-beta.9 - version: 2.0.0-beta.9(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(@tabler/icons-react@3.30.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 2.0.0-beta.9(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(@tabler/icons-react@3.31.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: 15.1.7 version: 15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1) @@ -2061,14 +2064,14 @@ importers: specifier: workspace:^0.1.0 version: link:../validation '@mantine/core': - specifier: ^7.17.0 - version: 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mantine/hooks': - specifier: ^7.17.0 - version: 7.17.0(react@19.0.0) + specifier: ^7.17.1 + version: 7.17.1(react@19.0.0) '@tabler/icons-react': - specifier: ^3.30.0 - version: 3.30.0(react@19.0.0) + specifier: ^3.31.0 + version: 3.31.0(react@19.0.0) '@tiptap/extension-color': specifier: 2.11.5 version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-text-style@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))) @@ -2122,7 +2125,7 @@ importers: version: 1.11.13 mantine-react-table: specifier: 2.0.0-beta.9 - version: 2.0.0-beta.9(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(@tabler/icons-react@3.30.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 2.0.0-beta.9(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(@tabler/icons-react@3.31.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: 15.1.7 version: 15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1) @@ -2171,7 +2174,7 @@ importers: version: 2.4.4(eslint@9.21.0)(turbo@2.4.4) eslint-plugin-import: specifier: ^2.31.0 - version: 2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0) + version: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0) eslint-plugin-jsx-a11y: specifier: ^6.10.2 version: 6.10.2(eslint@9.21.0) @@ -2182,8 +2185,8 @@ importers: specifier: ^5.2.0 version: 5.2.0(eslint@9.21.0) typescript-eslint: - specifier: ^8.25.0 - version: 8.25.0(eslint@9.21.0)(typescript@5.8.2) + specifier: ^8.26.0 + version: 8.26.0(eslint@9.21.0)(typescript@5.8.2) devDependencies: '@homarr/prettier-config': specifier: workspace:^0.1.0 @@ -2204,17 +2207,17 @@ importers: dependencies: '@ianvs/prettier-plugin-sort-imports': specifier: ^4.4.1 - version: 4.4.1(prettier@3.5.2) + version: 4.4.1(prettier@3.5.3) prettier: - specifier: ^3.5.2 - version: 3.5.2 + specifier: ^3.5.3 + version: 3.5.3 devDependencies: '@homarr/tsconfig': specifier: workspace:^0.1.0 version: link:../typescript prettier-plugin-packagejson: - specifier: ^2.5.9 - version: 2.5.9(prettier@3.5.2) + specifier: ^2.5.10 + version: 2.5.10(prettier@3.5.3) typescript: specifier: ^5.8.2 version: 5.8.2 @@ -3466,79 +3469,79 @@ packages: '@libsql/core@0.14.0': resolution: {integrity: sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==} - '@mantine/colors-generator@7.17.0': - resolution: {integrity: sha512-3kpjaOhjxCYlrhSCSkQPPGZnntqFF5GdhghjkVujCFEFj1JUnn+1TY/jQI+VLz0kioJdFarPiaGHZHlAM7ps1A==} + '@mantine/colors-generator@7.17.1': + resolution: {integrity: sha512-gtrLfaNKT4B8vLj0IznI1E/W9xTkRs2rxpp3YXxwOZYkqtFEaEWXjGlmzCJtQDqZgrMB6xB4apr4NxT6DmhGtA==} peerDependencies: chroma-js: '>=2.4.2' - '@mantine/core@7.17.0': - resolution: {integrity: sha512-AU5UFewUNzBCUXIq5Jk6q402TEri7atZW61qHW6P0GufJ2W/JxGHRvgmHOVHTVIcuWQRCt9SBSqZoZ/vHs9LhA==} + '@mantine/core@7.17.1': + resolution: {integrity: sha512-V8O3Ftq4la4I4wNDkTfH4Slkt/pCEU32pTE/DkO46zua0VFxfOAJeLjaol0s11//T+bXx82DtjMsd9APWPuFhA==} peerDependencies: - '@mantine/hooks': 7.17.0 + '@mantine/hooks': 7.17.1 react: ^18.x || ^19.x react-dom: ^18.x || ^19.x - '@mantine/dates@7.17.0': - resolution: {integrity: sha512-I+WVqkT8jxRQV+gi0MLiG0JYRQf+aL0aQN5Kp6xknAiYVp01D52Vg19QC6urkxQZ9Khwu0GHlFDeAkx5APVO+w==} + '@mantine/dates@7.17.1': + resolution: {integrity: sha512-L9MlIDb528RpznUeeW71xS4q3lYGolElz/f7xGRXEu9gHLaNJufbxroTw2N8RC6p/+RN1ZrSXEsjlr2euiofAw==} peerDependencies: - '@mantine/core': 7.17.0 - '@mantine/hooks': 7.17.0 + '@mantine/core': 7.17.1 + '@mantine/hooks': 7.17.1 dayjs: '>=1.0.0' react: ^18.x || ^19.x react-dom: ^18.x || ^19.x - '@mantine/dropzone@7.17.0': - resolution: {integrity: sha512-1BGOH/Fs1xxsVl6JUxFAElwqdmtj1nrzc7QSV3vs3xh7zAIAH6wqeor8j8+yycxz4lCfehHSaVAyDDv3AFsX8w==} + '@mantine/dropzone@7.17.1': + resolution: {integrity: sha512-bMn2f64IUIntEqGde5aPIa9Lp7AB1ONfuxYQwTOC3Gbr6xl77ShuxOAdPJSatx0ncGefb8Hd40gnWGouRzajGA==} peerDependencies: - '@mantine/core': 7.17.0 - '@mantine/hooks': 7.17.0 + '@mantine/core': 7.17.1 + '@mantine/hooks': 7.17.1 react: ^18.x || ^19.x react-dom: ^18.x || ^19.x - '@mantine/form@7.17.0': - resolution: {integrity: sha512-LONdeb+wL8h9fvyQ339ZFLxqrvYff+b+H+kginZhnr45OBTZDLXNVAt/YoKVFEkynF9WDJjdBVrXKcOZvPgmrA==} + '@mantine/form@7.17.1': + resolution: {integrity: sha512-IGZEzf/UUawIFYpLDI/F0oIkDImleaRzppkFqpTih4iBanlMIrguHOozm399xUK7+cTlQqWypd7vGVgBPTBnwQ==} peerDependencies: react: ^18.x || ^19.x - '@mantine/hooks@7.17.0': - resolution: {integrity: sha512-vo3K49mLy1nJ8LQNb5KDbJgnX0xwt3Y8JOF3ythjB5LEFMptdLSSgulu64zj+QHtzvffFCsMb05DbTLLpVP/JQ==} + '@mantine/hooks@7.17.1': + resolution: {integrity: sha512-mkHLrXMPd5xdI5WD7UOLwNEpdh/i6A7HaRDTXvjDE2/S0N8VmAE+BlvdyvWRMi7ODp2zVqJdP8cF1tgUn+Z0fA==} peerDependencies: react: ^18.x || ^19.x - '@mantine/modals@7.17.0': - resolution: {integrity: sha512-4sfiFxIxMxfm2RH4jXMN+cr8tFS5AexXG4TY7TRN/ySdkiWtFVvDe5l2/KRWWeWwDUb7wQhht8Ompj5KtexlEA==} + '@mantine/modals@7.17.1': + resolution: {integrity: sha512-hTgV4bfITPRo33R4a6Hp+aLN9H2leQA/ei/9cUbolURVh+4syacNtR8AoDKbRhX0vFzNHne99LJS8hlLvAYdjA==} peerDependencies: - '@mantine/core': 7.17.0 - '@mantine/hooks': 7.17.0 + '@mantine/core': 7.17.1 + '@mantine/hooks': 7.17.1 react: ^18.x || ^19.x react-dom: ^18.x || ^19.x - '@mantine/notifications@7.17.0': - resolution: {integrity: sha512-xejr1WW02NrrrE4HPDoownILJubcjLLwCDeTk907ZeeHKBEPut7RukEq6gLzOZBhNhKdPM+vCM7GcbXdaLZq/Q==} + '@mantine/notifications@7.17.1': + resolution: {integrity: sha512-jsCNkkjgtsGYIMbCrzBY0UBckoXyeaSWbEoJdvMlfA+LaeOQrSLxa+ot+1+wPaoZxR+1Q1xOwC1X5bTxHKudBA==} peerDependencies: - '@mantine/core': 7.17.0 - '@mantine/hooks': 7.17.0 + '@mantine/core': 7.17.1 + '@mantine/hooks': 7.17.1 react: ^18.x || ^19.x react-dom: ^18.x || ^19.x - '@mantine/spotlight@7.17.0': - resolution: {integrity: sha512-T7xfXxyDg2fxf7qvKwBozQ8HBnTQ2GRCIIoeYdAoiHoFQUS7NbBAnqrjdr5iYZpJqyLRXn8uFI7DX1Zdzd6/PQ==} + '@mantine/spotlight@7.17.1': + resolution: {integrity: sha512-wbOFoIIQHnMJCI+lTPX+D9TF2KJY0/JmbMcQSrecHLIIzKYQLhetACMvnF+bWTN13cxFckUtXMHowQETIcGcgQ==} peerDependencies: - '@mantine/core': 7.17.0 - '@mantine/hooks': 7.17.0 + '@mantine/core': 7.17.1 + '@mantine/hooks': 7.17.1 react: ^18.x || ^19.x react-dom: ^18.x || ^19.x - '@mantine/store@7.17.0': - resolution: {integrity: sha512-nhWRYRLqvAjrD/ApKCXxuHyTWg2b5dC06Z5gmO8udj4pBgndNf9nmCl+Of90H6bgOa56moJA7UQyXoF1SfxqVg==} + '@mantine/store@7.17.1': + resolution: {integrity: sha512-is1c0FycakMsbTElKGWO59LarjMIk24JUXfjP/QIrB0XqpXreq3u7aN4hoNqr1IftTZSfVBii4W8pVFeWaV55g==} peerDependencies: react: ^18.x || ^19.x - '@mantine/tiptap@7.17.0': - resolution: {integrity: sha512-WgmtQ5xJ9fenEbgpfG/HFU8O5R9SWrI8NJTpc0VLQKA2t+I4S/j8dTXr58aLCEXqqo6MlX9hkIDZQSxzUNoPXQ==} + '@mantine/tiptap@7.17.1': + resolution: {integrity: sha512-GJunUNzant7zOYHL8QgpweR+jh2oR85GIo6Ra20C2qaMvaGmQVQAcCR1KRQdanUQuNKjl+9cwlbfURYZYWsOZA==} peerDependencies: - '@mantine/core': 7.17.0 - '@mantine/hooks': 7.17.0 + '@mantine/core': 7.17.1 + '@mantine/hooks': 7.17.1 '@tiptap/extension-link': '>=2.1.12' '@tiptap/react': '>=2.1.12' react: ^18.x || ^19.x @@ -4241,39 +4244,39 @@ packages: zod: optional: true - '@tabler/icons-react@3.30.0': - resolution: {integrity: sha512-9KZ9D1UNAyjlLkkYp2HBPHdf6lAJ2aelDqh8YYAnnmLF3xwprWKxxW8+zw5jlI0IwdfN4XFFuzqePkaw+DpIOg==} + '@tabler/icons-react@3.31.0': + resolution: {integrity: sha512-2rrCM5y/VnaVKnORpDdAua9SEGuJKVqPtWxeQ/vUVsgaUx30LDgBZph7/lterXxDY1IKR6NO//HDhWiifXTi3w==} peerDependencies: react: '>= 16' - '@tabler/icons@3.30.0': - resolution: {integrity: sha512-c8OKLM48l00u9TFbh2qhSODMONIzML8ajtCyq95rW8vzkWcBrKRPM61tdkThz2j4kd5u17srPGIjqdeRUZdfdw==} + '@tabler/icons@3.31.0': + resolution: {integrity: sha512-dblAdeKY3+GA1U+Q9eziZ0ooVlZMHsE8dqP0RkwvRtEsAULoKOYaCUOcJ4oW1DjWegdxk++UAt2SlQVnmeHv+g==} '@tanstack/match-sorter-utils@8.19.4': resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} engines: {node: '>=12'} - '@tanstack/query-core@5.66.11': - resolution: {integrity: sha512-ZEYxgHUcohj3sHkbRaw0gYwFxjY5O6M3IXOYXEun7E1rqNhsP8fOtqjJTKPZpVHcdIdrmX4lzZctT4+pts0OgA==} + '@tanstack/query-core@5.67.1': + resolution: {integrity: sha512-AkFmuukVejyqVIjEQoFhLb3q+xHl7JG8G9cANWTMe3s8iKzD9j1VBSYXgCjy6vm6xM8cUCR9zP2yqWxY9pTWOA==} '@tanstack/query-devtools@5.65.0': resolution: {integrity: sha512-g5y7zc07U9D3esMdqUfTEVu9kMHoIaVBsD0+M3LPdAdD710RpTcLiNvJY1JkYXqkq9+NV+CQoemVNpQPBXVsJg==} - '@tanstack/react-query-devtools@5.66.11': - resolution: {integrity: sha512-a+zr2TN4dKpxVlJ9YBOC5YmpGWp2Ez2ZfIzsorVbrs/u2R+bVkLrU1u5e8WHzLdf6tXYueATqgeXWLHrvi4Dig==} + '@tanstack/react-query-devtools@5.67.1': + resolution: {integrity: sha512-a/2I8ORNalh+ek6Nyb9mEiq2u7vydjVMvaQz5ZieGq7r7DxgIFcPiMs4Ay0qkQvHfptESgXR5nImGTHmmt19yQ==} peerDependencies: - '@tanstack/react-query': ^5.66.11 + '@tanstack/react-query': ^5.67.1 react: ^18 || ^19 - '@tanstack/react-query-next-experimental@5.66.11': - resolution: {integrity: sha512-0HUkKa0N3N6u5JhmIaGIgi6oMFPNHXgmfdpnEg1cMjCXiY9OsAzEtxzhYrTDC81jn3YZkd/zLslpxJCJFWI4rA==} + '@tanstack/react-query-next-experimental@5.67.1': + resolution: {integrity: sha512-gNkIksA/jy5lLuAEzkn4aLLmGb1tPlJdbAgnm6GF29uVI4on6D0LfefenYz9Qv/Y8OfACx7XAjpRwisPJBooVQ==} peerDependencies: - '@tanstack/react-query': ^5.66.11 + '@tanstack/react-query': ^5.67.1 next: ^13 || ^14 || ^15 react: ^18 || ^19 - '@tanstack/react-query@5.66.11': - resolution: {integrity: sha512-uPDiQbZScWkAeihmZ9gAm3wOBA1TmLB1KCB1fJ1hIiEKq3dTT+ja/aYM7wGUD+XiEsY4sDSE7p8VIz/21L2Dow==} + '@tanstack/react-query@5.67.1': + resolution: {integrity: sha512-fH5u4JLwB6A+wLFdi8wWBWAYoJV5deYif2OveJ26ktAWjU499uvVFS1wPWnyEyq5LvZX1MZInvv9QRaIZANRaQ==} peerDependencies: react: ^18 || ^19 @@ -4506,19 +4509,19 @@ packages: tree-sitter: optional: true - '@trpc/client@11.0.0-rc.819': - resolution: {integrity: sha512-okJqmsDmn9D+3T73FeE8nER3s9BEn7sg9jk6C2h2Gw+e6uhJCQ5wryIrrDaYgLXyVXf3oPG+jJihlFWlFXH9wg==} + '@trpc/client@11.0.0-rc.824': + resolution: {integrity: sha512-3JMsgiMrCEeblrzu7ScTjipYaeEmegbnM5ZPmqqkPIbXcnth/TwH5tRr+nQbLSU/NGkWJWsjgEq7bM0BV17sJw==} peerDependencies: - '@trpc/server': 11.0.0-rc.819+b550ee46a + '@trpc/server': 11.0.0-rc.824+b21849468 typescript: '>=5.7.2' - '@trpc/next@11.0.0-rc.819': - resolution: {integrity: sha512-6oYCRT0j9HXUa8QkKknRwJ/Sqinf95qfap4y2OpUqTUfGuWxdV1AABvce/gr8J7WhJMyMx0ULUKItWwdRGYfog==} + '@trpc/next@11.0.0-rc.824': + resolution: {integrity: sha512-DD7WbLgu5u1mocqF07U+9xG/3WMO7tdA3RLjDwcT5MwgM2lYmcDTc7z5zJw1UAtb3zNfY+4N7CgNvx+r/KKrBA==} peerDependencies: '@tanstack/react-query': ^5.59.15 - '@trpc/client': 11.0.0-rc.819+b550ee46a - '@trpc/react-query': 11.0.0-rc.819+b550ee46a - '@trpc/server': 11.0.0-rc.819+b550ee46a + '@trpc/client': 11.0.0-rc.824+b21849468 + '@trpc/react-query': 11.0.0-rc.824+b21849468 + '@trpc/server': 11.0.0-rc.824+b21849468 next: 15.2.0 react: '>=16.8.0' react-dom: '>=16.8.0' @@ -4529,18 +4532,18 @@ packages: '@trpc/react-query': optional: true - '@trpc/react-query@11.0.0-rc.819': - resolution: {integrity: sha512-cc6ANj156hE9iRIqwQBU8wmmG8TZNGYlP8thw6fjk8lpn+a65ky/08UwMoPn0z/mBvXM0TGO9apbPQ03Dm7Y/Q==} + '@trpc/react-query@11.0.0-rc.824': + resolution: {integrity: sha512-M1Ui946RBKDjQN/w9JRANJUXStoUDIRd+ti5IhggNdyzY21kx7AJkz5vujZ2GEUxUIQBG8QbUxXN7mj+5BL/mw==} peerDependencies: - '@tanstack/react-query': ^5.62.8 - '@trpc/client': 11.0.0-rc.819+b550ee46a - '@trpc/server': 11.0.0-rc.819+b550ee46a + '@tanstack/react-query': ^5.67.1 + '@trpc/client': 11.0.0-rc.824+b21849468 + '@trpc/server': 11.0.0-rc.824+b21849468 react: '>=18.2.0' react-dom: '>=18.2.0' typescript: '>=5.7.2' - '@trpc/server@11.0.0-rc.819': - resolution: {integrity: sha512-KPkFEdsgXQeJux3bHMA8ahYOnXZBZIDBUeqAtZrQtFoIein/JNQQIuaAlA4UowWYBjEl1+fpWQwLYWCRPhL0OQ==} + '@trpc/server@11.0.0-rc.824': + resolution: {integrity: sha512-6u6CZ/x+oe2bZDhOo5h0wonbMU3svPe8hs6Dr2ymrU+23NAHE6tHtROAX+dKrMzup9KJcqyAabuFRYHy2H0tjw==} peerDependencies: typescript: '>=5.7.2' @@ -4681,8 +4684,8 @@ packages: '@types/node@18.19.50': resolution: {integrity: sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==} - '@types/node@22.13.5': - resolution: {integrity: sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==} + '@types/node@22.13.9': + resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -4749,57 +4752,57 @@ packages: '@types/video.js@7.3.58': resolution: {integrity: sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==} - '@types/ws@8.5.14': - resolution: {integrity: sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==} + '@types/ws@8.18.0': + resolution: {integrity: sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==} '@types/xml2js@0.4.14': resolution: {integrity: sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==} - '@typescript-eslint/eslint-plugin@8.25.0': - resolution: {integrity: sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==} + '@typescript-eslint/eslint-plugin@8.26.0': + resolution: {integrity: sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.25.0': - resolution: {integrity: sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==} + '@typescript-eslint/parser@8.26.0': + resolution: {integrity: sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.25.0': - resolution: {integrity: sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==} + '@typescript-eslint/scope-manager@8.26.0': + resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.25.0': - resolution: {integrity: sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==} + '@typescript-eslint/type-utils@8.26.0': + resolution: {integrity: sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.25.0': - resolution: {integrity: sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==} + '@typescript-eslint/types@8.26.0': + resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.25.0': - resolution: {integrity: sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==} + '@typescript-eslint/typescript-estree@8.26.0': + resolution: {integrity: sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.25.0': - resolution: {integrity: sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==} + '@typescript-eslint/utils@8.26.0': + resolution: {integrity: sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.25.0': - resolution: {integrity: sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==} + '@typescript-eslint/visitor-keys@8.26.0': + resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@umami/node@0.4.0': @@ -8316,16 +8319,16 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-packagejson@2.5.9: - resolution: {integrity: sha512-lhVEpWyYPtO+g9/zQLk6CLkMqcpwR8YGO4mwbbHJaFXoXVeK0LtcgTTalp8vZVmMyHKbcv/zhA1Xp4uQKoeCww==} + prettier-plugin-packagejson@2.5.10: + resolution: {integrity: sha512-LUxATI5YsImIVSaaLJlJ3aE6wTD+nvots18U3GuQMJpUyClChaZlQrqx3dBnbhF20OnKWZyx8EgyZypQtBDtgQ==} peerDependencies: prettier: '>= 1.16.0' peerDependenciesMeta: prettier: optional: true - prettier@3.5.2: - resolution: {integrity: sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==} + prettier@3.5.3: + resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} engines: {node: '>=14'} hasBin: true @@ -9043,8 +9046,8 @@ packages: sort-object-keys@1.1.3: resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} - sort-package-json@2.15.0: - resolution: {integrity: sha512-wpKu3DvFuymcRvPqJR7VN5J6wnqR+SYZ4SZmnJa9ckpV+BuoE0XYHZYsoWaJbt6oz8OwOXb4eoMjlEBM6hwhBw==} + sort-package-json@2.15.1: + resolution: {integrity: sha512-9x9+o8krTT2saA9liI4BljNjwAbvUnWf11Wq+i/iZt8nl2UGYnf3TH5uBydE7VALmP7AGwlfszuEeL8BDyb0YA==} hasBin: true source-map-js@1.2.1: @@ -9641,12 +9644,12 @@ packages: types-ramda@0.30.1: resolution: {integrity: sha512-1HTsf5/QVRmLzcGfldPFvkVsAdi1db1BBKzi7iW3KBUlOICg/nKnFS+jGqDJS3YD8VsWbAh7JiHeBvbsw8RPxA==} - typescript-eslint@8.25.0: - resolution: {integrity: sha512-TxRdQQLH4g7JkoFlYG3caW5v1S6kEkz8rqt80iQJZUYPq1zD1Ra7HfQBJJ88ABRaMvHAXnwRvRB4V+6sQ9xN5Q==} + typescript-eslint@8.26.0: + resolution: {integrity: sha512-PtVz9nAnuNJuAVeUFvwztjuUgSnJInODAUx47VDwWPXzd5vismPOtPtt83tzNXyOjVQbPRp786D6WFW/M2koIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' typescript@5.8.2: resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} @@ -11019,13 +11022,13 @@ snapshots: '@humanwhocodes/retry@0.4.2': {} - '@ianvs/prettier-plugin-sort-imports@4.4.1(prettier@3.5.2)': + '@ianvs/prettier-plugin-sort-imports@4.4.1(prettier@3.5.3)': dependencies: '@babel/generator': 7.26.2 '@babel/parser': 7.26.2 '@babel/traverse': 7.25.9 '@babel/types': 7.26.0 - prettier: 3.5.2 + prettier: 3.5.3 semver: 7.6.3 transitivePeerDependencies: - supports-color @@ -11162,14 +11165,14 @@ snapshots: js-base64: 3.7.7 optional: true - '@mantine/colors-generator@7.17.0(chroma-js@3.1.2)': + '@mantine/colors-generator@7.17.1(chroma-js@3.1.2)': dependencies: chroma-js: 3.1.2 - '@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@floating-ui/react': 0.26.28(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mantine/hooks': 7.17.0(react@19.0.0) + '@mantine/hooks': 7.17.1(react@19.0.0) clsx: 2.1.1 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) @@ -11180,65 +11183,65 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@mantine/dates@7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@mantine/dates@7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@mantine/core': 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mantine/hooks': 7.17.0(react@19.0.0) + '@mantine/core': 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mantine/hooks': 7.17.1(react@19.0.0) clsx: 2.1.1 dayjs: 1.11.13 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - '@mantine/dropzone@7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@mantine/dropzone@7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@mantine/core': 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mantine/hooks': 7.17.0(react@19.0.0) + '@mantine/core': 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mantine/hooks': 7.17.1(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) react-dropzone-esm: 15.2.0(react@19.0.0) - '@mantine/form@7.17.0(react@19.0.0)': + '@mantine/form@7.17.1(react@19.0.0)': dependencies: fast-deep-equal: 3.1.3 klona: 2.0.6 react: 19.0.0 - '@mantine/hooks@7.17.0(react@19.0.0)': + '@mantine/hooks@7.17.1(react@19.0.0)': dependencies: react: 19.0.0 - '@mantine/modals@7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@mantine/modals@7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@mantine/core': 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mantine/hooks': 7.17.0(react@19.0.0) + '@mantine/core': 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mantine/hooks': 7.17.1(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - '@mantine/notifications@7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@mantine/notifications@7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@mantine/core': 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mantine/hooks': 7.17.0(react@19.0.0) - '@mantine/store': 7.17.0(react@19.0.0) + '@mantine/core': 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mantine/hooks': 7.17.1(react@19.0.0) + '@mantine/store': 7.17.1(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) react-transition-group: 4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mantine/spotlight@7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@mantine/spotlight@7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@mantine/core': 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mantine/hooks': 7.17.0(react@19.0.0) - '@mantine/store': 7.17.0(react@19.0.0) + '@mantine/core': 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mantine/hooks': 7.17.1(react@19.0.0) + '@mantine/store': 7.17.1(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - '@mantine/store@7.17.0(react@19.0.0)': + '@mantine/store@7.17.1(react@19.0.0)': dependencies: react: 19.0.0 - '@mantine/tiptap@7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(@tiptap/extension-link@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5))(@tiptap/react@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@mantine/tiptap@7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(@tiptap/extension-link@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5))(@tiptap/react@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@mantine/core': 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mantine/hooks': 7.17.0(react@19.0.0) + '@mantine/core': 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mantine/hooks': 7.17.1(react@19.0.0) '@tiptap/extension-link': 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5) '@tiptap/react': 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 @@ -12309,36 +12312,36 @@ snapshots: typescript: 5.8.2 zod: 3.24.2 - '@tabler/icons-react@3.30.0(react@19.0.0)': + '@tabler/icons-react@3.31.0(react@19.0.0)': dependencies: - '@tabler/icons': 3.30.0 + '@tabler/icons': 3.31.0 react: 19.0.0 - '@tabler/icons@3.30.0': {} + '@tabler/icons@3.31.0': {} '@tanstack/match-sorter-utils@8.19.4': dependencies: remove-accents: 0.5.0 - '@tanstack/query-core@5.66.11': {} + '@tanstack/query-core@5.67.1': {} '@tanstack/query-devtools@5.65.0': {} - '@tanstack/react-query-devtools@5.66.11(@tanstack/react-query@5.66.11(react@19.0.0))(react@19.0.0)': + '@tanstack/react-query-devtools@5.67.1(@tanstack/react-query@5.67.1(react@19.0.0))(react@19.0.0)': dependencies: '@tanstack/query-devtools': 5.65.0 - '@tanstack/react-query': 5.66.11(react@19.0.0) + '@tanstack/react-query': 5.67.1(react@19.0.0) react: 19.0.0 - '@tanstack/react-query-next-experimental@5.66.11(@tanstack/react-query@5.66.11(react@19.0.0))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react@19.0.0)': + '@tanstack/react-query-next-experimental@5.67.1(@tanstack/react-query@5.67.1(react@19.0.0))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react@19.0.0)': dependencies: - '@tanstack/react-query': 5.66.11(react@19.0.0) + '@tanstack/react-query': 5.67.1(react@19.0.0) next: 15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1) react: 19.0.0 - '@tanstack/react-query@5.66.11(react@19.0.0)': + '@tanstack/react-query@5.67.1(react@19.0.0)': dependencies: - '@tanstack/query-core': 5.66.11 + '@tanstack/query-core': 5.67.1 react: 19.0.0 '@tanstack/react-table@8.20.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': @@ -12580,33 +12583,33 @@ snapshots: tree-sitter: 0.22.1 optional: true - '@trpc/client@11.0.0-rc.819(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(typescript@5.8.2)': + '@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2)': dependencies: - '@trpc/server': 11.0.0-rc.819(typescript@5.8.2) + '@trpc/server': 11.0.0-rc.824(typescript@5.8.2) typescript: 5.8.2 - '@trpc/next@11.0.0-rc.819(@tanstack/react-query@5.66.11(react@19.0.0))(@trpc/client@11.0.0-rc.819(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0-rc.819(@tanstack/react-query@5.66.11(react@19.0.0))(@trpc/client@11.0.0-rc.819(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2))(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)': + '@trpc/next@11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(next@15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)': dependencies: - '@trpc/client': 11.0.0-rc.819(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(typescript@5.8.2) - '@trpc/server': 11.0.0-rc.819(typescript@5.8.2) + '@trpc/client': 11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2) + '@trpc/server': 11.0.0-rc.824(typescript@5.8.2) next: 15.1.7(@babel/core@7.26.0)(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.85.1) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) typescript: 5.8.2 optionalDependencies: - '@tanstack/react-query': 5.66.11(react@19.0.0) - '@trpc/react-query': 11.0.0-rc.819(@tanstack/react-query@5.66.11(react@19.0.0))(@trpc/client@11.0.0-rc.819(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) + '@tanstack/react-query': 5.67.1(react@19.0.0) + '@trpc/react-query': 11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) - '@trpc/react-query@11.0.0-rc.819(@tanstack/react-query@5.66.11(react@19.0.0))(@trpc/client@11.0.0-rc.819(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)': + '@trpc/react-query@11.0.0-rc.824(@tanstack/react-query@5.67.1(react@19.0.0))(@trpc/client@11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)': dependencies: - '@tanstack/react-query': 5.66.11(react@19.0.0) - '@trpc/client': 11.0.0-rc.819(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(typescript@5.8.2) - '@trpc/server': 11.0.0-rc.819(typescript@5.8.2) + '@tanstack/react-query': 5.67.1(react@19.0.0) + '@trpc/client': 11.0.0-rc.824(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(typescript@5.8.2) + '@trpc/server': 11.0.0-rc.824(typescript@5.8.2) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) typescript: 5.8.2 - '@trpc/server@11.0.0-rc.819(typescript@5.8.2)': + '@trpc/server@11.0.0-rc.824(typescript@5.8.2)': dependencies: typescript: 5.8.2 @@ -12620,7 +12623,7 @@ snapshots: '@tsconfig/svelte@1.0.13': {} - '@turbo/gen@2.4.4(@types/node@22.13.5)(typescript@5.8.2)': + '@turbo/gen@2.4.4(@types/node@22.13.9)(typescript@5.8.2)': dependencies: '@turbo/workspaces': 2.4.4 commander: 10.0.1 @@ -12630,7 +12633,7 @@ snapshots: node-plop: 0.26.3 picocolors: 1.0.1 proxy-agent: 6.4.0 - ts-node: 10.9.2(@types/node@22.13.5)(typescript@5.8.2) + ts-node: 10.9.2(@types/node@22.13.9)(typescript@5.8.2) update-check: 1.5.4 validate-npm-package-name: 5.0.1 transitivePeerDependencies: @@ -12656,11 +12659,11 @@ snapshots: '@types/adm-zip@0.5.7': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/asn1@0.2.4': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/aws-lambda@8.10.146': {} @@ -12687,22 +12690,22 @@ snapshots: '@types/bcrypt@5.0.2': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/better-sqlite3@7.6.12': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/chroma-js@3.1.1': {} '@types/connect@3.4.38': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/cookie@0.4.1': {} @@ -12713,11 +12716,11 @@ snapshots: '@types/connect': 3.4.38 '@types/express': 4.17.21 '@types/keygrip': 1.0.6 - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/cors@2.8.17': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/css-font-loading-module@0.0.7': {} @@ -12725,13 +12728,13 @@ snapshots: '@types/docker-modem@3.0.6': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/ssh2': 1.15.1 '@types/dockerode@3.3.35': dependencies: '@types/docker-modem': 3.0.6 - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/ssh2': 1.15.1 '@types/estree@1.0.5': {} @@ -12740,7 +12743,7 @@ snapshots: '@types/express-serve-static-core@4.19.5': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/qs': 6.9.16 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -12755,7 +12758,7 @@ snapshots: '@types/glob@7.2.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/hast@2.3.10': dependencies: @@ -12793,7 +12796,7 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@22.13.5': + '@types/node@22.13.9': dependencies: undici-types: 6.20.0 @@ -12822,21 +12825,21 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/send': 0.17.4 '@types/ssh2-streams@0.1.12': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/ssh2@0.5.52': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/ssh2-streams': 0.1.12 '@types/ssh2@1.15.1': @@ -12849,7 +12852,7 @@ snapshots: '@types/through@0.0.33': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/tinycolor2@1.4.6': {} @@ -12864,22 +12867,22 @@ snapshots: '@types/video.js@7.3.58': {} - '@types/ws@8.5.14': + '@types/ws@8.18.0': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@types/xml2js@0.4.14': dependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 - '@typescript-eslint/eslint-plugin@8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)': + '@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0)(typescript@5.8.2) - '@typescript-eslint/scope-manager': 8.25.0 - '@typescript-eslint/type-utils': 8.25.0(eslint@9.21.0)(typescript@5.8.2) - '@typescript-eslint/utils': 8.25.0(eslint@9.21.0)(typescript@5.8.2) - '@typescript-eslint/visitor-keys': 8.25.0 + '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/type-utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/visitor-keys': 8.26.0 eslint: 9.21.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -12889,27 +12892,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2)': + '@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2)': dependencies: - '@typescript-eslint/scope-manager': 8.25.0 - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2) - '@typescript-eslint/visitor-keys': 8.25.0 + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2) + '@typescript-eslint/visitor-keys': 8.26.0 debug: 4.4.0 eslint: 9.21.0 typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.25.0': + '@typescript-eslint/scope-manager@8.26.0': dependencies: - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/visitor-keys': 8.25.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/visitor-keys': 8.26.0 - '@typescript-eslint/type-utils@8.25.0(eslint@9.21.0)(typescript@5.8.2)': + '@typescript-eslint/type-utils@8.26.0(eslint@9.21.0)(typescript@5.8.2)': dependencies: - '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2) - '@typescript-eslint/utils': 8.25.0(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2) + '@typescript-eslint/utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2) debug: 4.4.0 eslint: 9.21.0 ts-api-utils: 2.0.1(typescript@5.8.2) @@ -12917,12 +12920,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.25.0': {} + '@typescript-eslint/types@8.26.0': {} - '@typescript-eslint/typescript-estree@8.25.0(typescript@5.8.2)': + '@typescript-eslint/typescript-estree@8.26.0(typescript@5.8.2)': dependencies: - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/visitor-keys': 8.25.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/visitor-keys': 8.26.0 debug: 4.4.0 fast-glob: 3.3.2 is-glob: 4.0.3 @@ -12933,20 +12936,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.25.0(eslint@9.21.0)(typescript@5.8.2)': + '@typescript-eslint/utils@8.26.0(eslint@9.21.0)(typescript@5.8.2)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.21.0) - '@typescript-eslint/scope-manager': 8.25.0 - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2) + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2) eslint: 9.21.0 typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.25.0': + '@typescript-eslint/visitor-keys@8.26.0': dependencies: - '@typescript-eslint/types': 8.25.0 + '@typescript-eslint/types': 8.26.0 eslint-visitor-keys: 4.2.0 '@umami/node@0.4.0': {} @@ -12979,14 +12982,14 @@ snapshots: global: 4.4.0 is-function: 1.0.2 - '@vitejs/plugin-react@4.3.4(vite@5.4.5(@types/node@22.13.5)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0))': + '@vitejs/plugin-react@4.3.4(vite@5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.5(@types/node@22.13.5)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) + vite: 5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) transitivePeerDependencies: - supports-color @@ -13004,7 +13007,7 @@ snapshots: std-env: 3.8.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.7(@types/node@22.13.5)(@vitest/ui@3.0.7)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) + vitest: 3.0.7(@types/node@22.13.9)(@vitest/ui@3.0.7)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) transitivePeerDependencies: - supports-color @@ -13015,13 +13018,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.7(vite@5.4.5(@types/node@22.13.5)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0))': + '@vitest/mocker@3.0.7(vite@5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0))': dependencies: '@vitest/spy': 3.0.7 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.5(@types/node@22.13.5)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) + vite: 5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) '@vitest/pretty-format@3.0.7': dependencies: @@ -13051,7 +13054,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.12 tinyrainbow: 2.0.0 - vitest: 3.0.7(@types/node@22.13.5)(@vitest/ui@3.0.7)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) + vitest: 3.0.7(@types/node@22.13.9)(@vitest/ui@3.0.7)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) '@vitest/utils@3.0.7': dependencies: @@ -14252,7 +14255,7 @@ snapshots: dependencies: '@types/cookie': 0.4.1 '@types/cors': 2.8.17 - '@types/node': 22.13.5 + '@types/node': 22.13.9 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 @@ -14615,17 +14618,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.21.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.21.0): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2) eslint: 9.21.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -14636,7 +14639,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.21.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.21.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.21.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -14648,7 +14651,7 @@ snapshots: string.prototype.trimend: 1.0.8 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -15786,7 +15789,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -16046,12 +16049,12 @@ snapshots: make-error@1.3.6: {} - mantine-react-table@2.0.0-beta.9(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(@tabler/icons-react@3.30.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + mantine-react-table@2.0.0-beta.9(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/dates@7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(@tabler/icons-react@3.31.0(react@19.0.0))(clsx@2.1.1)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - '@mantine/core': 7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mantine/dates': 7.17.0(@mantine/core@7.17.0(@mantine/hooks@7.17.0(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.0(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mantine/hooks': 7.17.0(react@19.0.0) - '@tabler/icons-react': 3.30.0(react@19.0.0) + '@mantine/core': 7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mantine/dates': 7.17.1(@mantine/core@7.17.1(@mantine/hooks@7.17.1(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.1(react@19.0.0))(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mantine/hooks': 7.17.1(react@19.0.0) + '@tabler/icons-react': 3.31.0(react@19.0.0) '@tanstack/match-sorter-utils': 8.19.4 '@tanstack/react-table': 8.20.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@tanstack/react-virtual': 3.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -16805,14 +16808,14 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-packagejson@2.5.9(prettier@3.5.2): + prettier-plugin-packagejson@2.5.10(prettier@3.5.3): dependencies: - sort-package-json: 2.15.0 + sort-package-json: 2.15.1 synckit: 0.9.2 optionalDependencies: - prettier: 3.5.2 + prettier: 3.5.3 - prettier@3.5.2: {} + prettier@3.5.3: {} pretty-format@3.8.0: {} @@ -16983,7 +16986,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 22.13.5 + '@types/node': 22.13.9 long: 5.2.3 proxmox-api@1.1.1: @@ -17193,14 +17196,14 @@ snapshots: dependencies: find-up-simple: 1.0.0 read-pkg: 9.0.1 - type-fest: 4.30.2 + type-fest: 4.34.1 read-pkg@9.0.1: dependencies: '@types/normalize-package-data': 2.4.4 normalize-package-data: 6.0.2 parse-json: 8.1.0 - type-fest: 4.30.2 + type-fest: 4.34.1 unicorn-magic: 0.1.0 readable-stream@2.3.8: @@ -17736,7 +17739,7 @@ snapshots: sort-object-keys@1.1.3: {} - sort-package-json@2.15.0: + sort-package-json@2.15.1: dependencies: detect-indent: 7.0.1 detect-newline: 4.0.1 @@ -18260,9 +18263,9 @@ snapshots: triple-beam@1.4.1: {} - trpc-to-openapi@2.1.3(@trpc/server@11.0.0-rc.819(typescript@5.8.2))(zod-openapi@2.19.0(zod@3.24.2))(zod@3.24.2): + trpc-to-openapi@2.1.3(@trpc/server@11.0.0-rc.824(typescript@5.8.2))(zod-openapi@2.19.0(zod@3.24.2))(zod@3.24.2): dependencies: - '@trpc/server': 11.0.0-rc.819(typescript@5.8.2) + '@trpc/server': 11.0.0-rc.824(typescript@5.8.2) co-body: 6.2.0 h3: 1.13.0 openapi3-ts: 4.4.0 @@ -18277,14 +18280,14 @@ snapshots: ts-mixer@6.0.4: {} - ts-node@10.9.2(@types/node@22.13.5)(typescript@5.8.2): + ts-node@10.9.2(@types/node@22.13.9)(typescript@5.8.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.13.5 + '@types/node': 22.13.9 acorn: 8.14.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -18450,11 +18453,11 @@ snapshots: dependencies: ts-toolbelt: 9.6.0 - typescript-eslint@8.25.0(eslint@9.21.0)(typescript@5.8.2): + typescript-eslint@8.26.0(eslint@9.21.0)(typescript@5.8.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2) - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0)(typescript@5.8.2) - '@typescript-eslint/utils': 8.25.0(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/eslint-plugin': 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2) eslint: 9.21.0 typescript: 5.8.2 transitivePeerDependencies: @@ -18684,13 +18687,13 @@ snapshots: dependencies: global: 4.4.0 - vite-node@3.0.7(@types/node@22.13.5)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0): + vite-node@3.0.7(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 5.4.5(@types/node@22.13.5)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) + vite: 5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) transitivePeerDependencies: - '@types/node' - less @@ -18702,33 +18705,33 @@ snapshots: - supports-color - terser - vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@5.4.5(@types/node@22.13.5)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)): + vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)): dependencies: debug: 4.3.7 globrex: 0.1.2 tsconfck: 3.1.3(typescript@5.8.2) optionalDependencies: - vite: 5.4.5(@types/node@22.13.5)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) + vite: 5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) transitivePeerDependencies: - supports-color - typescript - vite@5.4.5(@types/node@22.13.5)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0): + vite@5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0): dependencies: esbuild: 0.21.5 postcss: 8.4.47 rollup: 4.21.3 optionalDependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 fsevents: 2.3.3 sass: 1.85.1 sugarss: 4.0.1(postcss@8.4.47) terser: 5.32.0 - vitest@3.0.7(@types/node@22.13.5)(@vitest/ui@3.0.7)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0): + vitest@3.0.7(@types/node@22.13.9)(@vitest/ui@3.0.7)(jsdom@26.0.0)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0): dependencies: '@vitest/expect': 3.0.7 - '@vitest/mocker': 3.0.7(vite@5.4.5(@types/node@22.13.5)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) + '@vitest/mocker': 3.0.7(vite@5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) '@vitest/pretty-format': 3.0.7 '@vitest/runner': 3.0.7 '@vitest/snapshot': 3.0.7 @@ -18744,11 +18747,11 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 5.4.5(@types/node@22.13.5)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) - vite-node: 3.0.7(@types/node@22.13.5)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) + vite: 5.4.5(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) + vite-node: 3.0.7(@types/node@22.13.9)(sass@1.85.1)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.13.5 + '@types/node': 22.13.9 '@vitest/ui': 3.0.7(vitest@3.0.7) jsdom: 26.0.0 transitivePeerDependencies: diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index 54131a632..c362eac3a 100644 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -19,7 +19,7 @@ if [ "${PUID}" != "0" ] || [ "${PGID}" != "0" ]; then fi if [ "${PUID}" != "0" ]; then - su-exec $PUID:$PGID "$@" + exec su-exec $PUID:$PGID "$@" else exec "$@" fi diff --git a/scripts/run.sh b/scripts/run.sh index 83833d5cd..d1c4dee86 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -19,17 +19,37 @@ export AUTH_SECRET=$(openssl rand -base64 32) # 2. Create the nginx configuration file from the template # 3. Start the nginx server envsubst '${HOSTNAME}' < /etc/nginx/templates/nginx.conf > /etc/nginx/nginx.conf +# Start services in the background and store their PIDs nginx -g 'daemon off;' & +NGINX_PID=$! -# Start Redis redis-server /app/redis.conf & +REDIS_PID=$! -# Run the tasks backend node apps/tasks/tasks.cjs & +TASKS_PID=$! node apps/websocket/wssServer.cjs & +WSS_PID=$! -# Run the nextjs server -node apps/nextjs/server.js & PID=$! +node apps/nextjs/server.js & +NEXTJS_PID=$! -wait $PID +# Function to handle SIGTERM and shut down services +terminate() { + echo "Received SIGTERM. Shutting down..." + kill -TERM $NGINX_PID $TASKS_PID $WSS_PID $NEXTJS_PID 2>/dev/null + wait + # kill redis-server last because of logging of other services + kill -TERM $REDIS_PID 2>/dev/null + wait + echo "Shutdown complete." + exit 0 +} + +# When SIGTERM (docker stop ) / SIGINT (ctrl+c) is received, run the terminate function +trap terminate TERM INT + +# Wait for all processes +wait $NEXTJS_PID +terminate \ No newline at end of file diff --git a/tooling/eslint/package.json b/tooling/eslint/package.json index 2109df827..ae078dd24 100644 --- a/tooling/eslint/package.json +++ b/tooling/eslint/package.json @@ -2,7 +2,7 @@ "name": "@homarr/eslint-config", "version": "0.2.0", "private": true, - "license": "MIT", + "license": "Apache-2.0", "type": "module", "exports": { "./base": "./base.js", @@ -24,7 +24,7 @@ "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^5.2.0", - "typescript-eslint": "^8.25.0" + "typescript-eslint": "^8.26.0" }, "devDependencies": { "@homarr/prettier-config": "workspace:^0.1.0", diff --git a/tooling/prettier/package.json b/tooling/prettier/package.json index 5a08dec47..e5d4b97aa 100644 --- a/tooling/prettier/package.json +++ b/tooling/prettier/package.json @@ -11,11 +11,11 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.4.1", - "prettier": "^3.5.2" + "prettier": "^3.5.3" }, "devDependencies": { "@homarr/tsconfig": "workspace:^0.1.0", - "prettier-plugin-packagejson": "^2.5.9", + "prettier-plugin-packagejson": "^2.5.10", "typescript": "^5.8.2" } }