diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bd3444e2d..ed077718b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -33,6 +33,7 @@ body: options: # The below comment is used to insert a new version with on-release.yml #NEXT_VERSION# + - 1.57.1 - 1.57.0 - 1.56.1 - 1.56.0 diff --git a/.github/actions/prebuilt-debian/action.yaml b/.github/actions/prebuilt-debian/action.yaml index 9770a7eb1..af1b7fdad 100644 --- a/.github/actions/prebuilt-debian/action.yaml +++ b/.github/actions/prebuilt-debian/action.yaml @@ -15,11 +15,11 @@ runs: with: persist-credentials: false - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build docker image for ${{ inputs.architecture }} id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: push: false load: true diff --git a/.github/renovate.json5 b/.github/renovate.json5 index e3c3e433f..b34d1dcab 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -31,6 +31,12 @@ automerge: false, }, ], + // Updates lockfile for prebuilt-debian after if package.json is updated, otherwise only root lockfile is updated + postUpgradeTasks: { + commands: ["cd deployments/prebuilt-debian && pnpm install --ignore-workspace --lockfile-only"], + fileFilters: ["deployments/prebuilt-debian/pnpm-lock.yaml"], + executionMode: "update", + }, updateInternalDeps: true, rangeStrategy: "bump", automerge: false, diff --git a/.github/workflows/automatic-approval.yml b/.github/workflows/automatic-approval.yml index a8a7b6feb..daee34a24 100644 --- a/.github/workflows/automatic-approval.yml +++ b/.github/workflows/automatic-approval.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v6 - name: Obtain token id: obtainToken - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }} app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }} diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index eeaec94f2..a61ef630a 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -89,7 +89,7 @@ jobs: uses: ./tooling/github/setup - name: Build docker image id: build-docker-image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: platforms: linux/amd64 context: . diff --git a/.github/workflows/crowdin-schedule-download.yml b/.github/workflows/crowdin-schedule-download.yml index fb0e1d7ba..4a176ca0f 100644 --- a/.github/workflows/crowdin-schedule-download.yml +++ b/.github/workflows/crowdin-schedule-download.yml @@ -19,7 +19,7 @@ jobs: - name: Obtain token id: obtainToken - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: private-key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }} app-id: ${{ secrets.CROWDIN_APP_ID }} diff --git a/.github/workflows/deployment-docker-image.yml b/.github/workflows/deployment-docker-image.yml index b13b1d378..03babd093 100644 --- a/.github/workflows/deployment-docker-image.yml +++ b/.github/workflows/deployment-docker-image.yml @@ -50,7 +50,7 @@ jobs: - name: Obtain token if: env.SKIP_RELEASE == 'false' id: obtainToken - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }} app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }} @@ -60,12 +60,12 @@ jobs: if: env.SKIP_RELEASE == 'false' with: persist-credentials: false - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 if: env.SKIP_RELEASE == 'false' - uses: actions/setup-node@v6 if: env.SKIP_RELEASE == 'false' with: - node-version: 24.14.0 + node-version: 24.14.1 cache: "pnpm" - run: npm i -g pnpm if: env.SKIP_RELEASE == 'false' @@ -123,7 +123,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: "${{ env.GHCR_REPO }}" @@ -135,11 +135,11 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build and push by digest id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . network: host @@ -162,7 +162,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: "${{ env.GHCR_REPO }}" @@ -174,11 +174,11 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build and push by digest id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . network: host @@ -231,7 +231,7 @@ jobs: - name: Obtain token if: needs.release.outputs.skipped == 'false' id: obtainToken - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }} app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }} diff --git a/.github/workflows/deployment-weekly-release.yml b/.github/workflows/deployment-weekly-release.yml index 869894a15..37d0c4364 100644 --- a/.github/workflows/deployment-weekly-release.yml +++ b/.github/workflows/deployment-weekly-release.yml @@ -50,7 +50,7 @@ jobs: args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }} (new behaviour: ${{ steps.semver.outputs.bump }})" - name: Obtain token id: obtainApprovalToken - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }} app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }} @@ -62,7 +62,7 @@ jobs: gh pr review --approve --body "Automatically approved by GitHub Action" - name: Obtain token id: obtainMergeToken - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }} app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }} diff --git a/.github/workflows/on-release.yml b/.github/workflows/on-release.yml index f5042d42f..f667d2669 100644 --- a/.github/workflows/on-release.yml +++ b/.github/workflows/on-release.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Obtain token id: obtainToken - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: private-key: ${{ secrets.HOMARR_DOCS_RELEASE_APP_PRIVATE_KEY }} app-id: ${{ vars.HOMARR_DOCS_RELEASE_APP_ID }} @@ -45,7 +45,7 @@ jobs: steps: - name: Obtain token id: obtainToken - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }} app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }} diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml index c3b7e53f0..feb22c52f 100644 --- a/.github/workflows/update-contributors.yml +++ b/.github/workflows/update-contributors.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Obtain token id: obtainToken - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }} app-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }} @@ -33,7 +33,7 @@ jobs: env: GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }} - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: diff --git a/.github/workflows/update-integration-list.yml b/.github/workflows/update-integration-list.yml index 35e012c16..204246d7b 100644 --- a/.github/workflows/update-integration-list.yml +++ b/.github/workflows/update-integration-list.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Obtain token id: obtainToken - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }} app-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }} diff --git a/.nvmrc b/.nvmrc index d845d9d88..8e3503489 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.14.0 +24.14.1 diff --git a/Dockerfile b/Dockerfile index a36fbffdb..3db197429 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.14.0-alpine AS base +FROM node:24.14.1-alpine AS base FROM base AS builder RUN apk add --no-cache libc6-compat diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index bf5b0375f..5845f2ac7 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -55,14 +55,14 @@ "@mantine/modals": "^8.3.18", "@mantine/tiptap": "^8.3.18", "@million/lint": "1.0.14", - "@tabler/icons-react": "^3.40.0", - "@tanstack/react-query": "^5.91.2", - "@tanstack/react-query-devtools": "^5.91.3", - "@tanstack/react-query-next-experimental": "^5.91.0", - "@trpc/client": "^11.14.1", - "@trpc/next": "^11.14.1", - "@trpc/react-query": "^11.14.1", - "@trpc/server": "^11.14.1", + "@tabler/icons-react": "^3.41.0", + "@tanstack/react-query": "^5.95.2", + "@tanstack/react-query-devtools": "^5.95.2", + "@tanstack/react-query-next-experimental": "^5.95.2", + "@trpc/client": "^11.16.0", + "@trpc/next": "^11.16.0", + "@trpc/react-query": "^11.16.0", + "@trpc/server": "^11.16.0", "@xterm/addon-canvas": "^0.7.0", "@xterm/addon-fit": "0.11.0", "@xterm/xterm": "^6.0.0", @@ -73,8 +73,8 @@ "dotenv": "^17.3.1", "flag-icons": "^7.5.0", "glob": "^13.0.6", - "isomorphic-dompurify": "^3.6.0", - "jotai": "^2.18.1", + "isomorphic-dompurify": "^3.7.1", + "jotai": "^2.19.0", "mantine-react-table": "2.0.0-beta.9", "next": "16.2.1", "postcss-preset-mantine": "^1.18.0", @@ -87,7 +87,7 @@ "superjson": "2.2.6", "swagger-ui-react": "^5.32.1", "use-deep-compare-effect": "^1.8.1", - "zod": "^4.2.1" + "zod": "^4.3.6" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx index 51be38157..0bfc79173 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef } from "react"; import { Button, Grid, Group, Loader, Stack, TextInput } from "@mantine/core"; -import { useDebouncedValue, useDocumentTitle, useFavicon } from "@mantine/hooks"; +import { useDebouncedValue, useDocumentTitle } from "@mantine/hooks"; import { useUpdateBoard } from "@homarr/boards/updater"; import { useZodForm } from "@homarr/form"; @@ -10,6 +10,7 @@ import { IconPicker } from "@homarr/forms-collection"; import { useI18n } from "@homarr/translation/client"; import { boardSavePartialSettingsSchema } from "@homarr/validation/board"; +import { homarrLogoPath } from "~/components/layout/logo/homarr-logo"; import { createMetaTitle } from "~/metadata"; import type { Board } from "../../_types"; import { useSavePartialSettingsMutation } from "./_shared"; @@ -53,7 +54,6 @@ export const GeneralSettingsContent = ({ board }: Props) => { ); useLogoPreview(form.values.logoImageUrl); - useFaviconPreview(form.values.faviconImageUrl); const metaTitleStatus = useMetaTitlePreview(form.values.metaTitle); // Cleanup for not applied changes of the page title and logo image URL @@ -75,6 +75,7 @@ export const GeneralSettingsContent = ({ board }: Props) => { pageTitle: values.pageTitle, logoImageUrl: values.logoImageUrl, }; + updateFavicon(values.faviconImageUrl ?? homarrLogoPath); savePartialSettings({ id: board.id, ...values, @@ -146,7 +147,13 @@ const useMetaTitlePreview = (title: string | null) => { }; }; -const useFaviconPreview = (url: string | null) => { - const [faviconDebounced] = useDebouncedValue(url ?? "", 500); - useFavicon(faviconDebounced); +// Previously we used the useFavicon hook from Mantine +// However if the user tried to click on a link to a different page +// it caused an error "Cannot read properties of null (reading 'removeChild')" +// Probably because the head element was null (https://github.com/mantinedev/mantine/blob/b90d9b81031f45f5d15e75f25138ed6477f65bce/packages/%40mantine/hooks/src/use-favicon/use-favicon.ts#L21) +// See https://github.com/homarr-labs/homarr/issues/4905 +const updateFavicon = (url: string) => { + const link: HTMLLinkElement | null = document.querySelector('link[rel="icon"]'); + if (!link) return; + link.href = url; }; diff --git a/apps/tasks/package.json b/apps/tasks/package.json index b8ebf997a..1d58843dc 100644 --- a/apps/tasks/package.json +++ b/apps/tasks/package.json @@ -39,9 +39,9 @@ "@homarr/widgets": "workspace:^0.1.0", "dayjs": "^1.11.20", "dotenv": "^17.3.1", - "fastify": "^5.8.2", + "fastify": "^5.8.4", "superjson": "2.2.6", - "undici": "7.24.5" + "undici": "7.24.6" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/deployments/prebuilt-debian/Dockerfile b/deployments/prebuilt-debian/Dockerfile index ced8d7bdf..8e0319dc5 100644 --- a/deployments/prebuilt-debian/Dockerfile +++ b/deployments/prebuilt-debian/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.14.0-trixie AS base +FROM node:24.14.1-trixie AS base WORKDIR /app COPY package.json . COPY pnpm-lock.yaml . diff --git a/deployments/prebuilt-debian/package.json b/deployments/prebuilt-debian/package.json index bad01df98..a03422d93 100644 --- a/deployments/prebuilt-debian/package.json +++ b/deployments/prebuilt-debian/package.json @@ -2,12 +2,12 @@ "name": "homarr-prebuilt-debian", "private": true, "dependencies": { - "better-sqlite3": "^12.6.2" + "better-sqlite3": "^12.8.0" }, - "packageManager": "pnpm@10.32.1", + "packageManager": "pnpm@10.33.0", "engines": { - "node": ">=24.14.0", - "pnpm": ">=10.32.1" + "node": ">=24.14.1", + "pnpm": ">=10.33.0" }, "pnpm": { "onlyBuiltDependencies": [ diff --git a/deployments/prebuilt-debian/pnpm-lock.yaml b/deployments/prebuilt-debian/pnpm-lock.yaml index f18d00d42..2536b3481 100644 --- a/deployments/prebuilt-debian/pnpm-lock.yaml +++ b/deployments/prebuilt-debian/pnpm-lock.yaml @@ -9,16 +9,16 @@ importers: .: dependencies: better-sqlite3: - specifier: ^12.6.2 - version: 12.6.2 + specifier: ^12.8.0 + version: 12.8.0 packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - better-sqlite3@12.6.2: - resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==} + better-sqlite3@12.8.0: + resolution: {integrity: sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==} engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} bindings@1.5.0: @@ -93,6 +93,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true pump@3.0.3: @@ -147,7 +148,7 @@ snapshots: base64-js@1.5.1: {} - better-sqlite3@12.6.2: + better-sqlite3@12.8.0: dependencies: bindings: 1.5.0 prebuild-install: 7.1.3 diff --git a/docs/README.md b/docs/README.md index f159c8765..f3a85f715 100644 --- a/docs/README.md +++ b/docs/README.md @@ -302,6 +302,13 @@ + + Search.ch +
+

Search.ch

+
+ + Seerr
@@ -342,15 +349,15 @@

Transmission

- - + + TrueNAS

TrueNAS

- - + + Unifi Controller
diff --git a/package.json b/package.json index ea050f2d1..4e3412253 100644 --- a/package.json +++ b/package.json @@ -43,26 +43,26 @@ "@semantic-release/npm": "^13.1.5", "@semantic-release/release-notes-generator": "^14.1.0", "@testcontainers/redis": "^11.13.0", - "@turbo/gen": "^2.8.20", + "@turbo/gen": "^2.8.21", "@vitejs/plugin-react": "^5.2.0", - "@vitest/coverage-v8": "^4.1.0", - "@vitest/ui": "^4.1.0", - "conventional-changelog-conventionalcommits": "^9.3.0", + "@vitest/coverage-v8": "^4.1.2", + "@vitest/ui": "^4.1.2", + "conventional-changelog-conventionalcommits": "^9.3.1", "cross-env": "^10.1.0", - "jsdom": "^28.1.0", + "jsdom": "^29.0.1", "json5": "^2.2.3", "prettier": "^3.8.1", "semantic-release": "^25.0.3", "testcontainers": "^11.13.0", - "turbo": "^2.8.20", + "turbo": "^2.8.21", "typescript": "^5.9.3", "vite-tsconfig-paths": "^6.1.1", - "vitest": "^4.1.0" + "vitest": "^4.1.2" }, - "packageManager": "pnpm@10.32.1", + "packageManager": "pnpm@10.33.0", "engines": { - "node": ">=24.14.0", - "pnpm": ">=10.32.1" + "node": ">=24.14.1", + "pnpm": ">=10.33.0" }, "pnpm": { "onlyBuiltDependencies": [ @@ -79,23 +79,23 @@ "overrides": { "@babel/helpers@<7.26.10": ">=7.29.2", "@babel/runtime@<7.26.10": ">=7.29.2", - "axios@>=1.0.0 <1.8.2": ">=1.13.6", - "brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.4", - "brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.4", + "axios@>=1.0.0 <1.8.2": ">=1.14.0", + "brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.5", + "brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.5", "esbuild@<=0.24.2": ">=0.27.4", "form-data@>=4.0.0 <4.0.4": ">=4.0.5", - "hono@<4.6.5": ">=4.12.8", + "hono@<4.6.5": ">=4.12.9", "linkifyjs@<4.3.2": ">=4.3.2", "nanoid@>=4.0.0 <5.0.9": ">=5.1.7", "prismjs@<1.30.0": ">=1.30.0", - "proxmox-api>undici": "7.24.5", + "proxmox-api>undici": "7.24.6", "react-is": "^19.2.4", "rollup@>=4.0.0 <4.22.4": ">=4.60.0", "sha.js@<=2.4.11": ">=2.4.12", "tar-fs@>=3.0.0 <3.0.9": ">=3.1.2", "tar-fs@>=2.0.0 <2.1.3": ">=3.1.2", "tmp@<=0.2.3": ">=0.2.5", - "vite@>=5.0.0 <=5.4.18": ">=8.0.1" + "vite@>=5.0.0 <=5.4.18": ">=8.0.3" }, "patchedDependencies": { "@types/node-unifi": "patches/@types__node-unifi.patch", diff --git a/packages/api/package.json b/packages/api/package.json index 74527649e..0de085d2c 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -41,18 +41,18 @@ "@homarr/translation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "@kubernetes/client-node": "^1.4.0", - "@tanstack/react-query": "^5.91.2", - "@trpc/client": "^11.14.1", - "@trpc/react-query": "^11.14.1", - "@trpc/server": "^11.14.1", - "@trpc/tanstack-react-query": "^11.14.1", + "@tanstack/react-query": "^5.95.2", + "@trpc/client": "^11.16.0", + "@trpc/react-query": "^11.16.0", + "@trpc/server": "^11.16.0", + "@trpc/tanstack-react-query": "^11.16.0", "lodash.clonedeep": "^4.5.0", "next": "16.2.1", "react": "19.2.4", "react-dom": "19.2.4", "superjson": "2.2.6", "trpc-to-openapi": "^3.2.0", - "zod": "^4.2.1" + "zod": "^4.3.6" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts index 8dd182cfd..954a79815 100644 --- a/packages/api/src/router/user.ts +++ b/packages/api/src/router/user.ts @@ -145,7 +145,7 @@ export const userRouter = createTRPCRouter({ create: permissionRequiredProcedure .requiresPermission("admin") .meta({ openapi: { method: "POST", path: "/api/users", tags: ["users"], protect: true } }) - .input(userCreateSchema) + .input(convertIntersectionToZodObject(userCreateSchema)) .output(z.void()) .mutation(async ({ ctx, input }) => { throwIfCredentialsDisabled(); @@ -376,7 +376,7 @@ export const userRouter = createTRPCRouter({ await ctx.db.delete(users).where(eq(users.id, input.userId)); }), changePassword: protectedProcedure - .input(userChangePasswordApiSchema) + .input(convertIntersectionToZodObject(userChangePasswordApiSchema)) .output(z.void()) .meta({ openapi: { method: "PATCH", path: "/api/users/{userId}/changePassword", tags: ["users"], protect: true } }) .mutation(async ({ ctx, input }) => { diff --git a/packages/api/src/router/widgets/index.ts b/packages/api/src/router/widgets/index.ts index 126ec4ad2..e58c62e6a 100644 --- a/packages/api/src/router/widgets/index.ts +++ b/packages/api/src/router/widgets/index.ts @@ -26,5 +26,6 @@ export const widgetRouter = createTRPCRouter({ networkController: lazy(() => import("./network-controller").then((mod) => mod.networkControllerRouter)), firewall: lazy(() => import("./firewall").then((mod) => mod.firewallRouter)), notifications: lazy(() => import("./notifications").then((mod) => mod.notificationsRouter)), + timetable: lazy(() => import("./timetable").then((mod) => mod.timetableRouter)), tracearr: lazy(() => import("./tracearr").then((mod) => mod.tracearrRouter)), }); diff --git a/packages/api/src/router/widgets/timetable.ts b/packages/api/src/router/widgets/timetable.ts new file mode 100644 index 000000000..787be4c37 --- /dev/null +++ b/packages/api/src/router/widgets/timetable.ts @@ -0,0 +1,43 @@ +import z from "zod"; + +import { getIntegrationKindsByCategory } from "@homarr/definitions"; +import { timetableOptionsSchema } from "@homarr/integrations/types"; +import { + timetableGetTimetableRequestHandler, + timetableSearchStationsRequestHandler, +} from "@homarr/request-handler/timetable"; + +import { createOneIntegrationMiddleware } from "../../middlewares/integration"; +import { createTRPCRouter, publicProcedure } from "../../trpc"; + +export const timetableRouter = createTRPCRouter({ + getTimetable: publicProcedure + .concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("timetable"))) + .input(timetableOptionsSchema) + .query(async ({ input, ctx }) => { + const innerHandler = timetableGetTimetableRequestHandler.handler(ctx.integration, { + stationId: input.stationId, + limit: input.limit, + }); + + const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + + return data; + }), + searchStations: publicProcedure + .concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("timetable"))) + .input( + z.object({ + query: z.string(), + }), + ) + .query(async ({ input, ctx }) => { + const innerHandler = timetableSearchStationsRequestHandler.handler(ctx.integration, { + query: input.query, + }); + + const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + + return data; + }), +}); diff --git a/packages/auth/package.json b/packages/auth/package.json index b53e67f55..46f903e75 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -38,7 +38,7 @@ "next-auth": "5.0.0-beta.30", "react": "19.2.4", "react-dom": "19.2.4", - "zod": "^4.2.1" + "zod": "^4.3.6" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/common/package.json b/packages/common/package.json index 04af57a08..5e014c671 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -34,8 +34,8 @@ "octokit": "^5.0.5", "react": "19.2.4", "react-dom": "19.2.4", - "undici": "7.24.5", - "zod": "^4.2.1", + "undici": "7.24.6", + "zod": "^4.3.6", "zod-validation-error": "^5.0.0" }, "devDependencies": { diff --git a/packages/core/package.json b/packages/core/package.json index af0d31a56..92ca35f88 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -38,15 +38,15 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@t3-oss/env-nextjs": "^0.13.11", - "better-sqlite3": "^12.6.2", + "better-sqlite3": "^12.8.0", "dns-caching": "^0.2.9", - "drizzle-orm": "^0.45.1", + "drizzle-orm": "^0.45.2", "ioredis": "5.10.1", "mysql2": "3.20.0", "pg": "^8.20.0", "superjson": "2.2.6", "winston": "3.19.0", - "zod": "^4.2.1" + "zod": "^4.3.6" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/cron-job-api/package.json b/packages/cron-job-api/package.json index e9d085e0e..7a715adb7 100644 --- a/packages/cron-job-api/package.json +++ b/packages/cron-job-api/package.json @@ -28,13 +28,13 @@ "@homarr/common": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0", - "@tanstack/react-query": "^5.91.2", - "@trpc/client": "^11.14.1", - "@trpc/server": "^11.14.1", - "@trpc/tanstack-react-query": "^11.14.1", + "@tanstack/react-query": "^5.95.2", + "@trpc/client": "^11.16.0", + "@trpc/server": "^11.16.0", + "@trpc/tanstack-react-query": "^11.16.0", "node-cron": "^4.2.1", "react": "19.2.4", - "zod": "^4.2.1" + "zod": "^4.3.6" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/cron-jobs/src/index.ts b/packages/cron-jobs/src/index.ts index 5ab802124..7f2b2349f 100644 --- a/packages/cron-jobs/src/index.ts +++ b/packages/cron-jobs/src/index.ts @@ -18,6 +18,7 @@ import { mediaServerJob } from "./jobs/integrations/media-server"; import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding"; import { networkControllerJob } from "./jobs/integrations/network-controller"; import { refreshNotificationsJob } from "./jobs/integrations/notifications"; +import { timetableJob } from "./jobs/integrations/timetable"; import { tracearrJob } from "./jobs/integrations/tracearr"; import { minecraftServerStatusJob } from "./jobs/minecraft-server-status"; import { pingJob } from "./jobs/ping"; @@ -51,6 +52,7 @@ export const jobGroup = createCronJobGroup({ firewallInterfaces: firewallInterfacesJob, refreshNotifications: refreshNotificationsJob, weather: weatherJob, + timetable: timetableJob, tracearr: tracearrJob, }); diff --git a/packages/cron-jobs/src/jobs/integrations/timetable.ts b/packages/cron-jobs/src/jobs/integrations/timetable.ts new file mode 100644 index 000000000..b3964dc38 --- /dev/null +++ b/packages/cron-jobs/src/jobs/integrations/timetable.ts @@ -0,0 +1,20 @@ +import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; +import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; +import { timetableGetTimetableRequestHandler } from "@homarr/request-handler/timetable"; + +import { createCronJob } from "../../lib"; + +export const timetableJob = createCronJob("timetable", EVERY_5_MINUTES).withCallback( + createRequestIntegrationJobHandler(timetableGetTimetableRequestHandler.handler, { + widgetKinds: ["timetable"], + getInput: { + timetable: (options) => + options.station + ? { + stationId: options.station.value, + limit: 10, + } + : [], + }, + }), +); diff --git a/packages/db/package.json b/packages/db/package.json index 51dfb5c21..e322c0374 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -51,10 +51,10 @@ "@paralleldrive/cuid2": "^3.3.0", "@testcontainers/mysql": "^11.13.0", "@testcontainers/postgresql": "^11.13.0", - "better-sqlite3": "^12.6.2", + "better-sqlite3": "^12.8.0", "dotenv": "^17.3.1", "drizzle-kit": "^0.31.10", - "drizzle-orm": "^0.45.1", + "drizzle-orm": "^0.45.2", "drizzle-zod": "^0.8.3", "mysql2": "3.20.0", "pg": "^8.20.0", diff --git a/packages/definitions/package.json b/packages/definitions/package.json index 33080232c..34b0e4b34 100644 --- a/packages/definitions/package.json +++ b/packages/definitions/package.json @@ -24,8 +24,8 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^0.1.0", - "fast-xml-parser": "^5.5.8", - "zod": "^4.2.1" + "fast-xml-parser": "^5.5.9", + "zod": "^4.3.6" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index 06f6aa73f..317fd1f7a 100644 --- a/packages/definitions/src/integration.ts +++ b/packages/definitions/src/integration.ts @@ -334,6 +334,15 @@ export const integrationDefs = { // @ts-expect-error - docs page will be created when integration is merged documentationUrl: createDocumentationLink("/docs/integrations/coolify"), }, + searchCh: { + name: "Search.ch", + secretKinds: [[]], + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/search-ch.png", + defaultUrl: "https://search.ch", + category: ["timetable"], + // @ts-expect-error - docs page will be created when integration is merged + documentationUrl: createDocumentationLink("/docs/integrations/search-ch"), + }, immich: { name: "Immich", secretKinds: [["apiKey"]], @@ -434,6 +443,7 @@ export const integrationCategories = [ "releasesProvider", "notifications", "firewall", + "timetable", "photoService", "mediaMonitoring", ] as const; diff --git a/packages/definitions/src/widget.ts b/packages/definitions/src/widget.ts index cf94bd3fa..732b3f1bc 100644 --- a/packages/definitions/src/widget.ts +++ b/packages/definitions/src/widget.ts @@ -31,6 +31,7 @@ export const widgetKinds = [ "systemResources", "coolify", "systemDisks", + "timetable", "immich-serverStats", "immich-albumCarousel", "tracearr", diff --git a/packages/form/package.json b/packages/form/package.json index 3e404dbe1..31ce91291 100644 --- a/packages/form/package.json +++ b/packages/form/package.json @@ -28,7 +28,7 @@ "@homarr/validation": "workspace:^0.1.0", "@mantine/form": "^8.3.18", "mantine-form-zod-resolver": "^1.3.0", - "zod": "^4.2.1" + "zod": "^4.3.6" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/forms-collection/package.json b/packages/forms-collection/package.json index 4fd35b4db..abc900cf0 100644 --- a/packages/forms-collection/package.json +++ b/packages/forms-collection/package.json @@ -32,7 +32,7 @@ "@homarr/validation": "workspace:^0.1.0", "@mantine/core": "^8.3.18", "react": "19.2.4", - "zod": "^4.2.1" + "zod": "^4.3.6" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 9e98502ed..406fedc1c 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -25,7 +25,7 @@ }, "prettier": "@homarr/prettier-config", "dependencies": { - "@ctrl/deluge": "^7.5.2", + "@ctrl/deluge": "^8.0.0", "@ctrl/qbittorrent": "^9.11.2", "@ctrl/transmission": "^7.4.0", "@gitbeaker/rest": "^43.8.0", @@ -38,18 +38,18 @@ "@homarr/redis": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@immich/sdk": "^2.6.1", + "@immich/sdk": "^2.6.3", "@jellyfin/sdk": "^0.13.0", "@octokit/auth-app": "^8.2.0", "ical.js": "^2.2.1", "maria2": "^0.4.1", - "node-ical": "^0.22.1", "octokit": "^5.0.5", "proxmox-api": "1.1.1", + "timezones-ical-library": "^2.1.3", "tsdav": "^2.1.8", - "undici": "7.24.5", + "undici": "7.24.6", "xml2js": "^0.6.2", - "zod": "^4.2.1" + "zod": "^4.3.6" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/integrations/src/base/creator.ts b/packages/integrations/src/base/creator.ts index b35fa5f63..c0743b5d4 100644 --- a/packages/integrations/src/base/creator.ts +++ b/packages/integrations/src/base/creator.ts @@ -40,6 +40,7 @@ import { PlexIntegration } from "../plex/plex-integration"; import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration"; import { ProxmoxIntegration } from "../proxmox/proxmox-integration"; import { QuayIntegration } from "../quay/quay-integration"; +import { SearchChIntegration } from "../search-ch/search-ch-integration"; import { SeerrIntegration } from "../seerr/seerr-integration"; import { TracearrIntegration } from "../tracearr/tracearr-integration"; import { TrueNasIntegration } from "../truenas/truenas-integration"; @@ -114,6 +115,7 @@ export const integrationCreators = { coolify: CoolifyIntegration, tracearr: TracearrIntegration, glances: GlancesIntegration, + searchCh: SearchChIntegration, immich: ImmichIntegration, } satisfies Record Promise]>; diff --git a/packages/integrations/src/immich/immich-integration.ts b/packages/integrations/src/immich/immich-integration.ts index 21019e4b7..9e13ff953 100644 --- a/packages/integrations/src/immich/immich-integration.ts +++ b/packages/integrations/src/immich/immich-integration.ts @@ -1,5 +1,6 @@ import { getAlbumInfo, getAllAlbums, getMyUser, getServerStatistics, init, searchUsers } from "@immich/sdk"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ImageProxy } from "@homarr/image-proxy"; @@ -41,7 +42,10 @@ export class ImmichIntegration extends Integration { public async getServerStatsAsync(): Promise { this.initClient(); - const [statistics, users] = await Promise.all([getServerStatistics(), searchUsers()]); + const [statistics, users] = await Promise.all([ + getServerStatistics(this.getRequestOptions()), + searchUsers(this.getRequestOptions()), + ]); return { photoCount: statistics.photos, userCount: users.length, @@ -52,7 +56,7 @@ export class ImmichIntegration extends Integration { public async getAlbumAsync(albumId: string): Promise { this.initClient(); - const album = await getAlbumInfo({ id: albumId }); + const album = await getAlbumInfo({ id: albumId }, this.getRequestOptions()); const imageProxy = new ImageProxy(); return { albumName: album.albumName, @@ -89,17 +93,24 @@ export class ImmichIntegration extends Integration { }[] > { this.initClient(); - const albums = await getAllAlbums({}); + const albums = await getAllAlbums({}, this.getRequestOptions()); return albums.map((album) => ({ id: album.id, albumName: album.albumName, assetCount: album.assetCount })); } - protected async testingAsync(_: IntegrationTestingInput): Promise { + protected async testingAsync(input: IntegrationTestingInput): Promise { this.initClient(); - const user = await getMyUser(); + const user = await getMyUser(this.getRequestOptions(input.fetchAsync)); logger.debug(`Logged in as ${user.name} (${user.id})`); return { success: true }; } + private getRequestOptions(fetchAsync = fetchWithTrustedCertificatesAsync) { + return { + // Undici and node types are not the same + fetch: fetchAsync as unknown as typeof fetch, + }; + } + /** * Sets the credentials and prepares the client for calls. * Must be called before any other functions. diff --git a/packages/integrations/src/interfaces/timetable/timetable-integration.ts b/packages/integrations/src/interfaces/timetable/timetable-integration.ts new file mode 100644 index 000000000..74e9d3c02 --- /dev/null +++ b/packages/integrations/src/interfaces/timetable/timetable-integration.ts @@ -0,0 +1,6 @@ +import type { Station, Timetable, TimetableOptions } from "./timetable-types"; + +export interface TimetableIntegration { + searchStationsAsync(query: string): Promise; + getTimetableAsync(options: TimetableOptions): Promise; +} diff --git a/packages/integrations/src/interfaces/timetable/timetable-types.ts b/packages/integrations/src/interfaces/timetable/timetable-types.ts new file mode 100644 index 000000000..72350b5a3 --- /dev/null +++ b/packages/integrations/src/interfaces/timetable/timetable-types.ts @@ -0,0 +1,33 @@ +import z from "zod"; + +export interface Station { + id: string; + name: string; +} + +export const timetableOptionsSchema = z.object({ + stationId: z.string(), + limit: z.number().default(10), +}); + +export type TimetableOptions = z.infer; + +export interface TimetableEntry { + timestamp: Date; + delay: number; + line: { + name: string; + color: string | null; + } | null; + location: string; + platform: { + name: string; + hasChanged: boolean; + } | null; +} + +export interface Timetable { + stationId: string; + timestamp: Date; + entries: TimetableEntry[]; +} diff --git a/packages/integrations/src/nextcloud/nextcloud.integration.ts b/packages/integrations/src/nextcloud/nextcloud.integration.ts index 531191cf6..ff41d8300 100644 --- a/packages/integrations/src/nextcloud/nextcloud.integration.ts +++ b/packages/integrations/src/nextcloud/nextcloud.integration.ts @@ -2,8 +2,9 @@ import type { Agent } from "https"; import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; +import ICAL from "ical.js"; import type { RequestInit as NodeFetchRequestInit } from "node-fetch"; -import * as ical from "node-ical"; +import { tzlib_get_ical_block, tzlib_get_timezones } from "timezones-ical-library"; import { DAVClient } from "tsdav"; import { z } from "zod"; @@ -21,6 +22,20 @@ import type { CalendarEvent } from "../interfaces/calendar/calendar-types"; import type { Notification } from "../interfaces/notifications/notification-types"; import type { INotificationsIntegration } from "../interfaces/notifications/notifications-integration"; +// Register all existing timezones +if (ICAL.TimezoneService.count === 0) { + const timezones = tzlib_get_timezones() as string[]; + for (const tzid of timezones) { + ICAL.TimezoneService.register( + new ICAL.Timezone({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-non-null-assertion + component: new ICAL.Component(ICAL.parse((tzlib_get_ical_block(tzid) as string[])[0]!)), + tzid, + }), + ); + } +} + const notificationSchema = z.object({ ocs: z.object({ data: z.array( @@ -68,42 +83,64 @@ export class NextcloudIntegration extends Integration implements ICalendarIntegr return calendarEvents .map((event) => { - // @ts-expect-error the typescript definitions for this package are wrong - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - const icalData = ical.default.parseICS(event.data) as ical.CalendarResponse; - const veventObject = Object.values(icalData).find((data) => data.type === "VEVENT"); - - if (!veventObject) { - throw new Error(`Invalid event data object: ${JSON.stringify(event.data)}. Unable to process the calendar.`); + if (typeof event.data !== "string") { + return []; } + const comp = ICAL.Component.fromString(event.data); + const veventComponent = comp.getFirstSubcomponent("vevent"); + + if (!veventComponent) { + return []; + } + + const veventObject = new ICAL.Event(veventComponent); logger.debug(`Converting VEVENT event to ${event.etag} from Nextcloud: ${JSON.stringify(veventObject)}`); const eventUrlWithoutHost = new URL(event.url).pathname; const eventSlug = Buffer.from(eventUrlWithoutHost).toString("base64url"); - const startDates = veventObject.rrule ? veventObject.rrule.between(start, end) : [veventObject.start]; + const durationMs = veventObject.duration.toSeconds() * 1000; - const durationMs = veventObject.end.getTime() - veventObject.start.getTime(); + // Handle recurring events + let startDates: Date[]; + if (veventObject.isRecurring()) { + const iterator = veventObject.iterator(); + startDates = []; + + // Iterate through occurrences within the time range + let next: ICAL.Time | undefined; + // next actually returns undefined when there are no more occurrences + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + while ((next = iterator.next())) { + const nextStartDate = next.convertToZone(ICAL.Timezone.utcTimezone).toJSDate(); + if (nextStartDate > end) break; + const nextEndDate = new Date(nextStartDate.getTime() + durationMs); + if (nextEndDate < start) continue; + + startDates.push(nextStartDate); + } + } else { + startDates = [veventObject.startDate.convertToZone(ICAL.Timezone.utcTimezone).toJSDate()]; + } return startDates.map((startDate) => { - const timezoneOffsetMinutes = veventObject.rrule?.origOptions.tzid - ? dayjs(startDate).tz(veventObject.rrule.origOptions.tzid).utcOffset() - : 0; - const utcStartDate = new Date(startDate.getTime() - timezoneOffsetMinutes * 60 * 1000); - const endDate = new Date(utcStartDate.getTime() + durationMs); - const dateInMillis = utcStartDate.valueOf(); + const endDate = new Date(startDate.getTime() + durationMs); + const dateInMillis = startDate.valueOf(); + + // Get color property from the component + const colorProperty = veventComponent.getFirstProperty("color"); + const color = colorProperty?.getFirstValue() as string | undefined; return { title: veventObject.summary, subTitle: null, - description: veventObject.description, - startDate: utcStartDate, + description: veventObject.description || null, + startDate, endDate, image: null, location: veventObject.location || null, - indicatorColor: - "color" in veventObject && typeof veventObject.color === "string" ? veventObject.color : "#ff8600", + indicatorColor: color ?? "#ff8600", links: [ { href: this.externalUrl( diff --git a/packages/integrations/src/search-ch/search-ch-integration.ts b/packages/integrations/src/search-ch/search-ch-integration.ts new file mode 100644 index 000000000..cbaf8e80d --- /dev/null +++ b/packages/integrations/src/search-ch/search-ch-integration.ts @@ -0,0 +1,141 @@ +import z from "zod"; + +import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; + +import type { IntegrationTestingInput } from "../base/integration"; +import { Integration } from "../base/integration"; +import { TestConnectionError } from "../base/test-connection/test-connection-error"; +import type { TestingResult } from "../base/test-connection/test-connection-service"; +import type { TimetableIntegration } from "../interfaces/timetable/timetable-integration"; +import type { Station, Timetable, TimetableOptions } from "../interfaces/timetable/timetable-types"; + +const supportedStationTypes = ["bus", "tram", "train", "ship", "cablecar", "funicular", "chairlift"]; + +export class SearchChIntegration extends Integration implements TimetableIntegration { + protected async testingAsync(input: IntegrationTestingInput): Promise { + const response = await input.fetchAsync(this.url("/timetable/api/completion.json", { term: "Zürich" })); + + if (!response.ok) { + return TestConnectionError.StatusResult(response); + } + + return { + success: true, + }; + } + + /** + * Searches for stations matching the given query. + * @param query The search query for station names. + * @returns A list of stations matching the search query. + * @docs https://search.ch/fahrplan/api/help + */ + public async searchStationsAsync(query: string): Promise { + const response = await fetchWithTrustedCertificatesAsync( + this.url("/timetable/api/completion.json", { + term: query, + show_ids: 1, + nofavorites: 1, + }), + ); + + if (!response.ok) { + throw new ResponseError(response); + } + + const data = await searchSchema.parseAsync(await response.json()); + + return ( + data + // Remove completions like addresses, we only want stations. + .filter((item) => supportedStationTypes.some((type) => item.iconclass.endsWith(type))) + .map((item) => (item.id !== undefined ? { id: item.id, name: item.label } : null)) + .filter((item) => item !== null) + ); + } + + /** + * Gets the departure timetable for a given station. + * @param options Timetable options including station ID and limit. + * @returns timetable for the given station. + * @docs https://search.ch/fahrplan/api/help + */ + public async getTimetableAsync(options: TimetableOptions): Promise { + const now = new Date(); + // The following format is used [01.01.2000, 23:59:00]. The API allows usage of DD.MM.YYYY format and HH:mm format + const [date, time] = now.toLocaleString("en-CH", { timeZone: "Europe/Zurich" }).split(", ") as [string, string]; + + const response = await fetchWithTrustedCertificatesAsync( + this.url("/timetable/api/stationboard.json", { + stop: options.stationId, + limit: options.limit, + show_delays: 1, + show_tracks: 1, + date, + time: time.substring(0, 5), // Only keep the HH:mm part of the time, as that's what the API expects + }), + ); + + if (!response.ok) { + throw new ResponseError(response); + } + + const data = await timetableSchema.parseAsync(await response.json()); + + return { + stationId: options.stationId, + timestamp: now, + entries: data.connections.map((connection) => { + // Color is in format A35~F10~ for foreground and background color. Some only have ~~ and therefore no color + const color = connection.color.split("~")[0]; + + return { + timestamp: connection.time, + line: connection.line + ? { + name: connection.line, + color: color && color.length >= 1 ? `#${color}` : null, + } + : null, + location: connection.terminal.name, + delay: connection.dep_delay, + platform: connection.track + ? { + // If the track name contains "!", it indicates a change in the track + // so we remove the "!" from the name and set hasChanged to true + name: connection.track.replace("!", ""), + hasChanged: connection.track.includes("!"), + } + : null, + }; + }), + }; + } +} + +const timetableSchema = z.object({ + connections: z.array( + z.object({ + time: z.string().transform((str) => new Date(str)), + line: z.string().optional(), + color: z.string(), + terminal: z.object({ + name: z.string(), + }), + track: z.string().optional(), + dep_delay: z + .string() + .optional() + .transform((str) => (str ? parseInt(str) : 0)), + }), + ), +}); + +const searchSchema = z.array( + z.object({ + id: z.string().optional(), + label: z.string(), + iconclass: z.string(), // This is in format sl-icon-type-{type} + }), +); diff --git a/packages/integrations/src/types.ts b/packages/integrations/src/types.ts index debdd041b..7c7bc4780 100644 --- a/packages/integrations/src/types.ts +++ b/packages/integrations/src/types.ts @@ -12,4 +12,5 @@ export * from "./unifi-controller/unifi-controller-types"; export * from "./opnsense/opnsense-types"; export * from "./interfaces/media-releases"; export * from "./coolify/coolify-types"; +export * from "./interfaces/timetable/timetable-types"; export * from "./tracearr/tracearr-types"; diff --git a/packages/integrations/test/nextcloud.spec.ts b/packages/integrations/test/nextcloud.spec.ts new file mode 100644 index 000000000..cef3ae494 --- /dev/null +++ b/packages/integrations/test/nextcloud.spec.ts @@ -0,0 +1,454 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { StartedTestContainer } from "testcontainers"; +import { GenericContainer, getContainerRuntimeClient, ImageName, Wait } from "testcontainers"; +import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; + +import { createId } from "@homarr/common"; +import { createDb } from "@homarr/db/test"; + +import { NextcloudIntegration } from "../src"; + +// Mock the database +vi.mock("@homarr/db", async (importActual) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importActual(); + return { + ...actual, + db: createDb(), + }; +}); + +// Mock certificate verification +vi.mock("@homarr/core/infrastructure/certificates", async (importActual) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importActual(); + return { + ...actual, + getTrustedCertificateHostnamesAsync: vi.fn().mockImplementation(() => { + return Promise.resolve([]); + }), + }; +}); + +const IMAGE_NAME = "nextcloud:latest"; +const DEFAULT_USERNAME = "admin"; +const DEFAULT_PASSWORD = "admin123"; + +interface CalendarTestEvent { + summary: string; + description?: string; + location?: string; + color?: string; + dtstart: string; + dtend: string; + rrule?: string; + timezone?: string; +} + +describe("Nextcloud integration", () => { + let startedContainer: StartedTestContainer | null = null; + let nextcloudIntegration: NextcloudIntegration | null = null; + + beforeAll(async () => { + const containerRuntimeClient = await getContainerRuntimeClient(); + await containerRuntimeClient.image.pull(ImageName.fromString(IMAGE_NAME)); + + startedContainer = await prepareNextcloudContainerAsync(); + nextcloudIntegration = createNextcloudIntegration(startedContainer); + }, 100_000); + + afterAll(async () => { + if (startedContainer) { + await startedContainer.stop(); + startedContainer = null; + } + }); + + describe("Test connection", () => { + test("Test connection > Should work with valid credentials", async () => { + // Act + const result = await nextcloudIntegration!.testConnectionAsync(); + + // Assert + expect(result.success).toBe(true); + }, 20_000); + + test("Test connection > Should fail with invalid credentials", async () => { + // Arrange + const invalidIntegration = createNextcloudIntegration(startedContainer!, "wrong-user", "wrong-pass"); + + // Act + const result = await invalidIntegration.testConnectionAsync(); + + // Assert + expect(result.success).toBe(false); + }, 20_000); + }); + + describe("Retrieve calendar events", () => { + test("Retrieve calendar events > Should retrieve all fields correctly", async () => { + // Arrange + const calendarName = createId(); + const testEvent: CalendarTestEvent = { + summary: "Test Meeting", + description: "This is a test meeting", + location: "Conference Room A", + color: "#FF5733", + dtstart: "20260415T140000Z", + dtend: "20260415T150000Z", + }; + const start = new Date("2026-04-01T00:00:00Z"); + const end = new Date("2026-04-30T23:59:59Z"); + + await createCalendarAsync(startedContainer!, calendarName); + await createCalendarEventAsync(startedContainer!, calendarName, testEvent); + + // Act + const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end); + + // Assert + const relevantEvents = events.filter((event) => event.title === testEvent.summary); + expect(relevantEvents).toHaveLength(1); + const retrievedEvent = relevantEvents[0]; + expect(retrievedEvent).toMatchObject({ + title: testEvent.summary, + description: testEvent.description, + location: testEvent.location, + indicatorColor: testEvent.color, + }); + expect(retrievedEvent!.startDate.toISOString()).toBe("2026-04-15T14:00:00.000Z"); + expect(retrievedEvent!.endDate?.toISOString()).toBe("2026-04-15T15:00:00.000Z"); + }, 20_000); + test("Retrieve calendar events > Should work with only required fields", async () => { + // Arrange + const calendarName = createId(); + const testEvent: CalendarTestEvent = { + summary: "Simple Event", + dtstart: "20260415T140000Z", + dtend: "20260415T150000Z", + }; + const start = new Date("2026-04-01T00:00:00Z"); + const end = new Date("2026-04-30T23:59:59Z"); + + await createCalendarAsync(startedContainer!, calendarName); + await createCalendarEventAsync(startedContainer!, calendarName, testEvent); + + // Act + const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end); + + // Assert + const relevantEvents = events.filter((event) => event.title === testEvent.summary); + expect(relevantEvents).toHaveLength(1); + const retrievedEvent = relevantEvents[0]; + expect(retrievedEvent?.location).toBeNull(); + expect(retrievedEvent?.description).toBeNull(); + expect(retrievedEvent?.indicatorColor).toBe("#ff8600"); // Default color + }, 20_000); + test("Retrieve calendar events > Should work with timezone specified", async () => { + // Arrange + const calendarName = createId(); + const testEvent: CalendarTestEvent = { + summary: "Timezone Event", + dtstart: "20260415T140000", + dtend: "20260415T150000", + timezone: "Asia/Tokyo", + }; + const start = new Date("2026-04-01T00:00:00Z"); + const end = new Date("2026-04-30T23:59:59Z"); + + await createCalendarAsync(startedContainer!, calendarName); + await createCalendarEventAsync(startedContainer!, calendarName, testEvent); + + // Act + const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end); + + // Assert + const relevantEvents = events.filter((event) => event.title === testEvent.summary); + expect(relevantEvents).toHaveLength(1); + const retrievedEvent = relevantEvents[0]; + + expect(retrievedEvent!.startDate.toISOString()).toBe("2026-04-15T05:00:00.000Z"); // 14:00 in Tokyo is 05:00 UTC + expect(retrievedEvent!.endDate?.toISOString()).toBe("2026-04-15T06:00:00.000Z"); // 15:00 in Tokyo is 06:00 UTC + }, 120_000); + test("Retrieve calendar events > Should work with event that starts before the range but ends within the range", async () => { + // Arrange + const calendarName = createId(); + const testEvent: CalendarTestEvent = { + summary: "Early Meeting", + dtstart: "20260401T230000Z", // Starts before the range + dtend: "20260402T010000Z", // Ends within the range + }; + const start = new Date("2026-04-02T00:00:00Z"); + const end = new Date("2026-04-30T23:59:59Z"); + + await createCalendarAsync(startedContainer!, calendarName); + await createCalendarEventAsync(startedContainer!, calendarName, testEvent); + + // Act + const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end); + + // Assert + const relevantEvents = events.filter((event) => event.title === testEvent.summary); + expect(relevantEvents).toHaveLength(1); + }, 20_000); + test("Retrieve calendar events > Should work with event that starts within the range but ends after the range", async () => { + // Arrange + const calendarName = createId(); + const testEvent: CalendarTestEvent = { + summary: "Late Meeting", + dtstart: "20260430T230000Z", // Starts within the range + dtend: "20260501T010000Z", // Ends after the range + }; + const start = new Date("2026-04-01T00:00:00Z"); + const end = new Date("2026-04-30T23:59:59Z"); + + await createCalendarAsync(startedContainer!, calendarName); + await createCalendarEventAsync(startedContainer!, calendarName, testEvent); + + // Act + const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end); + + // Assert + const relevantEvents = events.filter((event) => event.title === testEvent.summary); + expect(relevantEvents).toHaveLength(1); + }, 20_000); + test("Retrieve calendar events > Should not retrieve events outside the range", async () => { + // Arrange + const calendarName = createId(); + const testEvent: CalendarTestEvent = { + summary: "Out of Range Meeting", + dtstart: "20260301T140000Z", // Starts before the range + dtend: "20260301T150000Z", // Ends before the range + }; + const start = new Date("2026-04-01T00:00:00Z"); + const end = new Date("2026-04-30T23:59:59Z"); + + await createCalendarAsync(startedContainer!, calendarName); + await createCalendarEventAsync(startedContainer!, calendarName, testEvent); + + // Act + const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end); + + // Assert + const relevantEvents = events.filter((event) => event.title === testEvent.summary); + expect(relevantEvents).toHaveLength(0); + }, 20_000); + test("Retrieve calendar events > Should work with events that begin before the range and end after the range", async () => { + // Arrange + const calendarName = createId(); + const testEvent: CalendarTestEvent = { + summary: "Spanning Meeting", + dtstart: "20260330T230000Z", // Starts before the range + dtend: "20260501T010000Z", // Ends after the range + }; + const start = new Date("2026-04-01T00:00:00Z"); + const end = new Date("2026-04-30T23:59:59Z"); + + await createCalendarAsync(startedContainer!, calendarName); + await createCalendarEventAsync(startedContainer!, calendarName, testEvent); + + // Act + const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end); + + // Assert + const relevantEvents = events.filter((event) => event.title === testEvent.summary); + expect(relevantEvents).toHaveLength(1); + }, 20_000); + test("Retrieve calendar events > Should work with recurring events first occurrence before the range", async () => { + // Arrange + const calendarName = createId(); + const testEvent: CalendarTestEvent = { + summary: "Recurring Meeting", + dtstart: "20260330T140000Z", // First occurrence starts before the range + dtend: "20260330T150000Z", + rrule: "FREQ=DAILY;COUNT=5", // Daily for 5 occurrences + }; + const start = new Date("2026-04-01T00:00:00Z"); + const end = new Date("2026-04-30T23:59:59Z"); + + await createCalendarAsync(startedContainer!, calendarName); + await createCalendarEventAsync(startedContainer!, calendarName, testEvent); + + // Act + const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end); + + // Assert + const relevantEvents = events.filter((event) => event.title === testEvent.summary); + expect(relevantEvents).toHaveLength(3); + }, 20_000); + test("Retrieve calendar events > Should work with recurring events that go beyond the range", async () => { + // Arrange + const calendarName = createId(); + const testEvent: CalendarTestEvent = { + summary: "Working Week", + dtstart: "20260302T090000Z", + dtend: "20260306T170000Z", + rrule: "FREQ=WEEKLY;COUNT=10", + }; + const start = new Date("2026-04-01T00:00:00Z"); + const end = new Date("2026-04-30T23:59:59Z"); + + await createCalendarAsync(startedContainer!, calendarName); + await createCalendarEventAsync(startedContainer!, calendarName, testEvent); + + // Act + const events = await nextcloudIntegration!.getCalendarEventsAsync(start, end); + + // Assert + const relevantEvents = events.filter((event) => event.title === testEvent.summary); + expect(relevantEvents).toHaveLength(5); + }); + }); +}); + +/** + * Prepares a Nextcloud container with proper configuration + */ +async function prepareNextcloudContainerAsync(): Promise { + const container = createNextcloudContainer(); + const startedContainer = await container.start(); + + return startedContainer; +} + +/** + * Creates a Nextcloud container with proper configuration + */ +function createNextcloudContainer() { + return new GenericContainer(IMAGE_NAME) + .withEnvironment({ + NEXTCLOUD_ADMIN_USER: DEFAULT_USERNAME, + NEXTCLOUD_ADMIN_PASSWORD: DEFAULT_PASSWORD, + NEXTCLOUD_TRUSTED_DOMAINS: "*", + SQLITE_DATABASE: "nextcloud", // Use SQLite for faster startup + }) + .withExposedPorts(80) + .withWaitStrategy(Wait.forHttp("/status.php", 80).forStatusCode(200)) + .withStartupTimeout(120_000); +} + +/** + * Creates a Nextcloud integration instance + */ +function createNextcloudIntegration( + container: StartedTestContainer, + username?: string, + password?: string, +): NextcloudIntegration { + return new NextcloudIntegration({ + id: "1", + decryptedSecrets: [ + { + kind: "username", + value: username ?? DEFAULT_USERNAME, + }, + { + kind: "password", + value: password ?? DEFAULT_PASSWORD, + }, + ], + name: "Nextcloud", + url: `http://${container.getHost()}:${container.getMappedPort(80)}`, + externalUrl: null, + }); +} + +/** + * Creates a calendar in Nextcloud via OCS API + */ +async function createCalendarAsync(container: StartedTestContainer, calendarName: string): Promise { + const host = container.getHost(); + const port = container.getMappedPort(80); + const url = `http://${host}:${port}/remote.php/dav/calendars/${DEFAULT_USERNAME}/${calendarName}`; + + const auth = Buffer.from(`${DEFAULT_USERNAME}:${DEFAULT_PASSWORD}`).toString("base64"); + + const response = await fetch(url, { + method: "MKCALENDAR", + headers: { + Authorization: `Basic ${auth}`, + "Content-Type": "application/xml", + }, + body: ` + + + + ${calendarName} + + + `, + }); + + if (!response.ok && response.status !== 201) { + throw new Error(`Failed to create calendar: ${response.status} ${response.statusText}`); + } +} + +/** + * Creates a calendar event in Nextcloud via CalDAV + */ +async function createCalendarEventAsync( + container: StartedTestContainer, + calendarName: string, + event: CalendarTestEvent, +): Promise { + const host = container.getHost(); + const port = container.getMappedPort(80); + const id = createId(); + const url = `http://${host}:${port}/remote.php/dav/calendars/${DEFAULT_USERNAME}/${calendarName}/${id}.ics`; + + const auth = Buffer.from(`${DEFAULT_USERNAME}:${DEFAULT_PASSWORD}`).toString("base64"); + + // Build iCalendar format + const icalData = buildICalEvent(id, event); + + const response = await fetch(url, { + method: "PUT", + headers: { + Authorization: `Basic ${auth}`, + "Content-Type": "text/calendar", + }, + body: icalData, + }); + + if (!response.ok && response.status !== 201) { + throw new Error(`Failed to create event: ${response.status} ${response.statusText}`); + } +} + +/** + * Builds an iCalendar format string from event data + */ +function buildICalEvent(uid: string, event: CalendarTestEvent): string { + const lines = [ + "BEGIN:VCALENDAR", + "VERSION:2.0", + "PRODID:-//Homarr Tests//EN", + "BEGIN:VEVENT", + `UID:${uid}`, + `DTSTAMP:${new Date().toISOString().replace(/[-:]/g, "").split(".")[0]}Z`, + `DTSTART${event.timezone ? `;TZID=${event.timezone}` : ""}:${event.dtstart}`, + `DTEND${event.timezone ? `;TZID=${event.timezone}` : ""}:${event.dtend}`, + `SUMMARY:${event.summary}`, + ]; + + if (event.description) { + lines.push(`DESCRIPTION:${event.description}`); + } + + if (event.location) { + lines.push(`LOCATION:${event.location}`); + } + + if (event.color) { + lines.push(`COLOR:${event.color}`); + } + + if (event.rrule) { + lines.push(`RRULE:${event.rrule}`); + } + + lines.push("END:VEVENT", "END:VCALENDAR"); + + return lines.join("\r\n"); +} diff --git a/packages/modals-collection/package.json b/packages/modals-collection/package.json index 72b7845ad..4349797fd 100644 --- a/packages/modals-collection/package.json +++ b/packages/modals-collection/package.json @@ -34,12 +34,12 @@ "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "@mantine/core": "^8.3.18", - "@tabler/icons-react": "^3.40.0", + "@tabler/icons-react": "^3.41.0", "dayjs": "^1.11.20", "next": "16.2.1", "react": "19.2.4", "react-dom": "19.2.4", - "zod": "^4.2.1" + "zod": "^4.3.6" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/notifications/package.json b/packages/notifications/package.json index 0af9a8e93..ba8f2c6d4 100644 --- a/packages/notifications/package.json +++ b/packages/notifications/package.json @@ -25,7 +25,7 @@ "dependencies": { "@homarr/ui": "workspace:^0.1.0", "@mantine/notifications": "^8.3.18", - "@tabler/icons-react": "^3.40.0" + "@tabler/icons-react": "^3.41.0" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/old-import/package.json b/packages/old-import/package.json index df86a28b2..21145a5cb 100644 --- a/packages/old-import/package.json +++ b/packages/old-import/package.json @@ -44,7 +44,7 @@ "react": "19.2.4", "react-dom": "19.2.4", "superjson": "2.2.6", - "zod": "^4.2.1", + "zod": "^4.3.6", "zod-form-data": "^3.0.1" }, "devDependencies": { diff --git a/packages/old-schema/package.json b/packages/old-schema/package.json index 0c3b01d24..d64229c41 100644 --- a/packages/old-schema/package.json +++ b/packages/old-schema/package.json @@ -23,7 +23,7 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^0.1.0", - "zod": "^4.2.1" + "zod": "^4.3.6" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/request-handler/package.json b/packages/request-handler/package.json index f18a1a97a..f8c3f710a 100644 --- a/packages/request-handler/package.json +++ b/packages/request-handler/package.json @@ -33,8 +33,8 @@ "dayjs": "^1.11.20", "octokit": "^5.0.5", "superjson": "2.2.6", - "undici": "7.24.5", - "zod": "^4.2.1" + "undici": "7.24.6", + "zod": "^4.3.6" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/request-handler/src/timetable.ts b/packages/request-handler/src/timetable.ts new file mode 100644 index 000000000..0946ac67d --- /dev/null +++ b/packages/request-handler/src/timetable.ts @@ -0,0 +1,36 @@ +import dayjs from "dayjs"; + +import type { IntegrationKindByCategory } from "@homarr/definitions"; +import { createIntegrationAsync } from "@homarr/integrations"; +import type { Station, Timetable } from "@homarr/integrations/types"; + +import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; + +export const timetableSearchStationsRequestHandler = createCachedIntegrationRequestHandler< + Station[], + IntegrationKindByCategory<"timetable">, + { query: string } +>({ + async requestAsync(integration, input) { + const integrationInstance = await createIntegrationAsync(integration); + return await integrationInstance.searchStationsAsync(input.query); + }, + queryKey: "timetableSearchStations", + cacheDuration: dayjs.duration(1, "day"), +}); + +export const timetableGetTimetableRequestHandler = createCachedIntegrationRequestHandler< + Timetable, + IntegrationKindByCategory<"timetable">, + { stationId: string; limit: number } +>({ + async requestAsync(integration, input) { + const integrationInstance = await createIntegrationAsync(integration); + return await integrationInstance.getTimetableAsync({ + stationId: input.stationId, + limit: input.limit, + }); + }, + queryKey: "timetableGetTimetable", + cacheDuration: dayjs.duration(1, "minute"), +}); diff --git a/packages/spotlight/package.json b/packages/spotlight/package.json index b459c21ea..215c68bc6 100644 --- a/packages/spotlight/package.json +++ b/packages/spotlight/package.json @@ -36,8 +36,8 @@ "@mantine/core": "^8.3.18", "@mantine/hooks": "^8.3.18", "@mantine/spotlight": "^8.3.18", - "@tabler/icons-react": "^3.40.0", - "jotai": "^2.18.1", + "@tabler/icons-react": "^3.41.0", + "jotai": "^2.19.0", "next": "16.2.1", "react": "19.2.4", "react-dom": "19.2.4", diff --git a/packages/translation/src/lang/ca.json b/packages/translation/src/lang/ca.json index 978817e9b..99a134e6b 100644 --- a/packages/translation/src/lang/ca.json +++ b/packages/translation/src/lang/ca.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/cn.json b/packages/translation/src/lang/cn.json index 33443c465..93c7122f6 100644 --- a/packages/translation/src/lang/cn.json +++ b/packages/translation/src/lang/cn.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "允许地理位置" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "显示内存信息" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "显示运行时间" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "CPU", "memory": "内存", + "gpu": "", "network": "网络" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "CPU", "memory": "内存", + "gpu": "", "network": "网络", "up": "上传", "down": "下载" diff --git a/packages/translation/src/lang/cr.json b/packages/translation/src/lang/cr.json index c6a8b4da3..63ab2b8d7 100644 --- a/packages/translation/src/lang/cr.json +++ b/packages/translation/src/lang/cr.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "crwdns970:0crwdne970:0" + }, + "allowModals": { + "label": "crwdns3992:0crwdne3992:0" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "crwdns1080:0crwdne1080:0" }, + "gpu": { + "label": "crwdns3994:0crwdne3994:0" + }, "showUptime": { "label": "crwdns3446:0crwdne3446:0" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "crwdns3700:0crwdne3700:0", "memory": "crwdns3702:0crwdne3702:0", + "gpu": "crwdns3996:0crwdne3996:0", "network": "crwdns3704:0crwdne3704:0" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "crwdns3604:0crwdne3604:0", "memory": "crwdns3606:0crwdne3606:0", + "gpu": "crwdns3998:0crwdne3998:0", "network": "crwdns3608:0crwdne3608:0", "up": "crwdns3610:0crwdne3610:0", "down": "crwdns3612:0crwdne3612:0" diff --git a/packages/translation/src/lang/cs.json b/packages/translation/src/lang/cs.json index 124de13bb..e807a0e09 100644 --- a/packages/translation/src/lang/cs.json +++ b/packages/translation/src/lang/cs.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Povolit polohu" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Zobrazit informace o paměti" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/da.json b/packages/translation/src/lang/da.json index df562d1ff..67ccdf8ca 100644 --- a/packages/translation/src/lang/da.json +++ b/packages/translation/src/lang/da.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Tillad geolokalisering" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Vis hukommelsesoplysninger" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "Vis Oppetid" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "CPU", "memory": "Hukommelse", + "gpu": "", "network": "Netværk" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "CPU", "memory": "MEM", + "gpu": "", "network": "NET", "up": "OP", "down": "NED" diff --git a/packages/translation/src/lang/de-CH.json b/packages/translation/src/lang/de-CH.json index 62b300aff..3b1c56710 100644 --- a/packages/translation/src/lang/de-CH.json +++ b/packages/translation/src/lang/de-CH.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Geolokalisierung zulassen" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Speicher-Info anzeigen" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/de.json b/packages/translation/src/lang/de.json index aa16650e6..ae6b7b4c6 100644 --- a/packages/translation/src/lang/de.json +++ b/packages/translation/src/lang/de.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Geolokalisierung zulassen" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Speicher-Info anzeigen" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "Laufzeit anzeigen" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "CPU", "memory": "Speicher", + "gpu": "", "network": "Netzwerk" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "CPU", "memory": "Arbeitsspeicher", + "gpu": "", "network": "Netzwerk", "up": "OBEN", "down": "UNTEN" diff --git a/packages/translation/src/lang/el.json b/packages/translation/src/lang/el.json index ea7862559..bcb635de7 100644 --- a/packages/translation/src/lang/el.json +++ b/packages/translation/src/lang/el.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Επιτρέπεται ο γεωεντοπισμός" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Εμφάνιση Πληροφοριών Μνήμης" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/en-gb.json b/packages/translation/src/lang/en-gb.json index 5042ed477..bda80ea94 100644 --- a/packages/translation/src/lang/en-gb.json +++ b/packages/translation/src/lang/en-gb.json @@ -1125,17 +1125,17 @@ "userAvatar": { "menu": { "switchToDarkMode": "Switch to dark mode", - "switchToLightMode": "Switch to light mode", - "management": "Management", - "preferences": "Your preferences", - "logout": "Log-out", + "switchToLightMode": "التبديل إلى الوضع الفاتح", + "management": "الإدارة", + "preferences": "تفضيلاتك", + "logout": "تسجيل الخروج", "login": "Log-in", - "homeBoard": "Your home board", + "homeBoard": "لوحة منزلك", "loggedOut": "Logged out", - "updateAvailable": "New update available: {tag}" + "updateAvailable": "{countUpdates} يتوفر تحديث جديد: {tag}" } }, - "dangerZone": "Danger zone", + "dangerZone": "المنطقة الخطرة", "noResults": "No results have been found", "unsavedChanges": "You have unsaved changes!", "servers": "Servers", @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Allow geolocation" + }, + "allowModals": { + "label": "Allow modals" } }, "error": { @@ -1856,107 +1859,110 @@ "label": "Show CPU Info" }, "memory": { - "label": "" + "label": "Show Memory Info" + }, + "gpu": { + "label": "Show GPU Info" }, "showUptime": { - "label": "" + "label": "Show Uptime" }, "fileSystem": { - "label": "" + "label": "Show Filesystem Info" }, "defaultTab": { - "label": "" + "label": "Default tab" }, "visibleClusterSections": { - "label": "" + "label": "Visible cluster sections" }, "sectionIndicatorRequirement": { - "label": "" + "label": "Section indicator requirement" } }, "popover": { - "information": "", - "processor": "", - "memory": "", - "memoryAvailable": "", - "version": "", - "uptime": "", - "loadAverage": "", - "minute": "", - "minutes": "", - "used": "", - "available": "", - "lastSeen": "" + "information": "Information", + "processor": "Processor: {cpuModelName}", + "memory": "Memory: {memory}GiB", + "memoryAvailable": "Available: {memoryAvailable}GiB ({percent}%)", + "version": "Version: {version}", + "uptime": "Uptime: {months} Months, {days} Days, {hours} Hours, {minutes} Minutes", + "loadAverage": "Load average:", + "minute": "1 minute", + "minutes": "{count} minutes", + "used": "Used", + "available": "Available", + "lastSeen": "Last status update: {lastSeen}" }, "memory": {}, "error": { - "internalServerError": "" + "internalServerError": "Failed to fetch health status" }, "cluster": { "summary": { - "cpu": "", - "memory": "" + "cpu": "CPU", + "memory": "RAM" }, "resource": { "node": { - "name": "" + "name": "Nodes" }, "qemu": { - "name": "" + "name": "VMs" }, "lxc": { - "name": "" + "name": "LXCs" }, "storage": { - "name": "" + "name": "Storage" } }, "popover": { "rightSection": { - "node": "", - "vmId": "", - "plugin": "" + "node": "Node", + "vmId": "VM ID", + "plugin": "Plugin" }, "detail": { - "cpu": "", - "memory": "", - "storage": "", - "uptime": "", - "haState": "", + "cpu": "Cores", + "memory": "Memory", + "storage": "Storage", + "uptime": "Uptime", + "haState": "HA State", "storageType": { - "local": "", - "shared": "" + "local": "Local storage", + "shared": "Shared storage" } } }, "table": { "header": { - "name": "", - "cpu": "", - "memory": "", - "node": "" + "name": "Name", + "cpu": "CPU", + "memory": "RAM", + "node": "Node" } } } }, "dockerContainers": { - "name": "", - "description": "", + "name": "Docker stats", + "description": "Stats of your containers (This widget can only be added with administrator privileges)", "option": { "enableRowSorting": { - "label": "" + "label": "Enable items sorting" }, "defaultSort": { - "label": "", + "label": "Column used for sorting by default", "option": { - "name": "", - "state": "", - "cpuUsage": "", - "memoryUsage": "" + "name": "Name", + "state": "State", + "cpuUsage": "CPU usage", + "memoryUsage": "Memory usage" } }, "descendingDefaultSort": { - "label": "" + "label": "Invert sorting" } }, "error": { @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" @@ -3149,98 +3157,98 @@ }, "navbar": { "items": { - "home": "", - "boards": "", - "apps": "", - "integrations": "", - "searchEngies": "", - "medias": "", + "home": "الصفحة الرئسية", + "boards": "الألواح", + "apps": "التطبيقات", + "integrations": "التكاملات", + "searchEngies": "محركات البحث", + "medias": "الوسائط", "users": { - "label": "", + "label": "المستخدمين", "items": { - "manage": "", - "invites": "", - "groups": "" + "manage": "إدارة المستخدمين", + "invites": "الدعوات", + "groups": "المجموعات" } }, "tools": { - "label": "", + "label": "الأدوات", "items": { - "docker": "", + "docker": "الحاويات", "kubernetes": "", - "logs": "", - "api": "", - "certificates": "", - "tasks": "" + "logs": "السجلات", + "api": "API", + "certificates": "الشهادات", + "tasks": "المهام" } }, - "settings": "", + "settings": "الإعدادات", "help": { - "label": "", + "label": "المساعدة", "items": { - "documentation": "", - "submitIssue": "", - "discord": "", - "sourceCode": "" + "documentation": "المستندات", + "submitIssue": "إبلاغ عن مشكلة", + "discord": "مجتمع ديسكورد", + "sourceCode": "كود المصدر" } }, - "about": "" + "about": "حول" } }, "page": { "home": { "statistic": { - "board": "", - "user": "", - "invite": "", - "integration": "", - "app": "", - "group": "", - "searchEngine": "", - "media": "" + "board": "الألواح", + "user": "مستخدمين", + "invite": "دعوات", + "integration": "التكاملات", + "app": "البرامج", + "group": "المجموعات", + "searchEngine": "محركات البحث", + "media": "الوسائط" }, "statisticLabel": { - "boards": "", - "resources": "", - "authentication": "", - "authorization": "" + "boards": "اللوحات", + "resources": "الموارد", + "authentication": "المصادقة", + "authorization": "الأذونات" }, "heroBanner": { - "title": "", - "subtitle": "" + "title": "مرحبًا بعودتك إلى", + "subtitle": "{app} اللوحات" } }, "board": { - "title": "", + "title": "لوحاتك", "action": { "new": { - "label": "" + "label": "لوحة جديدة" }, "open": { - "label": "" + "label": "فتح اللوحة" }, "settings": { - "label": "" + "label": "الإعدادات" }, "setHomeBoard": { - "label": "", + "label": "تعيينه كلوحة رئيسية", "badge": { - "label": "", + "label": "الرئيسية", "tooltip": "" } }, "setMobileHomeBoard": { - "label": "", + "label": "تعيينه كصفحتك الرئيسية للجوال", "badge": { - "label": "", + "label": "الجوال", "tooltip": "" } }, "duplicate": { - "label": "" + "label": "تكرار اللوحة" }, "delete": { - "label": "", + "label": "حذف نهائيًا", "confirm": { "title": "", "description": "" @@ -3329,10 +3337,10 @@ } }, "invite": { - "title": "", + "title": "إدارة دعوات المستخدمين", "action": { "new": { - "title": "", + "title": "دعوة جديدة", "description": "" }, "copy": { @@ -3348,13 +3356,13 @@ }, "field": { "id": { - "label": "" + "label": "المعرّف" }, "creator": { - "label": "" + "label": "المنشئ" }, "expirationDate": { - "label": "" + "label": "انتهاء الصلاحية" }, "token": { "label": "" @@ -4121,7 +4129,7 @@ "manage": { "label": "", "boards": { - "label": "" + "label": "الألواح" }, "integrations": { "label": "", @@ -4154,7 +4162,7 @@ } }, "users": { - "label": "", + "label": "المستخدمين", "create": { "label": "" }, @@ -4165,7 +4173,7 @@ "label": "" }, "invites": { - "label": "" + "label": "الدعوات" } }, "tools": { @@ -4222,7 +4230,7 @@ } }, "search": { - "placeholder": "", + "placeholder": "البحث عن", "nothingFound": "", "error": { "fetch": "" @@ -4382,13 +4390,13 @@ "title": "", "option": { "documentation": { - "label": "" + "label": "Documentation" }, "submitIssue": { - "label": "" + "label": "Submit an issue" }, "discord": { - "label": "" + "label": "Community Discord" } } } @@ -4397,20 +4405,20 @@ "home": { "group": { "search": { - "title": "", + "title": "Search", "option": { "other": { - "label": "" + "label": "Search with another search engine" }, "no-default": { - "label": "", - "description": "" + "label": "No default search engine", + "description": "Set a default search engine in preferences" }, "search": { - "label": "" + "label": "Search for \"{query}\" with {name}" }, "from-integration": { - "description": "" + "description": "Start typing to search" } } }, diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 75812b45e..a069f9c4e 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -1525,6 +1525,17 @@ "offline": "Offline" } }, + "timetable": { + "name": "Timetable", + "description": "Displays departure times for a station", + "title": "Departures {station}", + "noStation": "No station selected", + "option": { + "station": { + "label": "Station" + } + } + }, "notebook": { "name": "Notebook", "description": "A simple notebook widget that supports markdown", @@ -3513,6 +3524,9 @@ "minecraftServerStatus": { "label": "Minecraft server status" }, + "timetable": { + "label": "Timetable" + }, "iconsUpdater": { "label": "Icons Updater" }, diff --git a/packages/translation/src/lang/es.json b/packages/translation/src/lang/es.json index 507c4630f..4685315ff 100644 --- a/packages/translation/src/lang/es.json +++ b/packages/translation/src/lang/es.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Permitir geolocalización" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Mostrar información de la memoria" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "Mostrar tiempo de actividad" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "CPU", "memory": "Memoria", + "gpu": "", "network": "Red" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "CPU", "memory": "MEM", + "gpu": "", "network": "RED", "up": "ENCENDIDO", "down": "APAGADO" diff --git a/packages/translation/src/lang/et.json b/packages/translation/src/lang/et.json index 2bab54e1e..73acbf075 100644 --- a/packages/translation/src/lang/et.json +++ b/packages/translation/src/lang/et.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/fr.json b/packages/translation/src/lang/fr.json index af40dd744..bc814c21f 100644 --- a/packages/translation/src/lang/fr.json +++ b/packages/translation/src/lang/fr.json @@ -114,7 +114,7 @@ }, "invite": { "title": "Rejoignez Homarr", - "subtitle": "Bienvenue sur Homarr! Veuillez créer votre compte", + "subtitle": "Bienvenue sur Homarr ! Veuillez créer votre compte", "description": "Vous avez été invité par {username}" }, "init": { @@ -177,7 +177,7 @@ } }, "forgotPassword": { - "label": "Mot de passe oublié?", + "label": "Mot de passe oublié ?", "description": "Un administrateur peut utiliser la commande suivante pour réinitialiser votre mot de passe :" } }, @@ -578,7 +578,7 @@ }, "delete": { "title": "Supprimer l'app", - "message": "Êtes-vous sûr de vouloir supprimer l'app {name}?", + "message": "Êtes-vous sûr de vouloir supprimer l'app {name} ?", "notification": { "success": { "title": "Suppression effectuée avec succès", @@ -684,7 +684,7 @@ }, "delete": { "title": "Supprimer l'intégration", - "message": "Êtes-vous sûr(e) de vouloir supprimer l'intégration {name}?", + "message": "Êtes-vous sûr(e) de vouloir supprimer l'intégration {name} ?", "notification": { "success": { "title": "Suppression réussie", @@ -742,7 +742,7 @@ "description": "La réponse n'a pas pu être analysée. Veuillez vérifier que l'URL pointe vers l'URL de base du service." }, "authorization": { - "title": "", + "title": "Erreur d'autorisation", "description": "La requête n'a pas été autorisée. Veuillez vérifier que les informations d'identification sont correctes et que vous les avez configurées avec suffisamment d'autorisations." }, "statusCode": { @@ -794,7 +794,7 @@ "hostnameMismatch": { "confirm": { "title": "Faire confiant à l'incohérence du nom d'hôte", - "message": "" + "message": "Êtes-vous sûr de vouloir faire confiance ce certificat avec un nom d'hôte incohérent ?" }, "notification": { "success": { @@ -1164,8 +1164,8 @@ "number": "Ce champ doit être inférieur ou égal à {maximum}" }, "custom": { - "passwordsDoNotMatch": "Les mots de passe ne correspondent pas.", - "passwordRequirements": "Le mot de passe ne respecte pas les exigences requises.", + "passwordsDoNotMatch": "Les mots de passe ne correspondent pas", + "passwordRequirements": "Le mot de passe ne respecte pas les exigences requises", "boardAlreadyExists": "Un tableau avec ce nom existe déjà", "invalidFileType": "Type de fichier invalide, {expected} attendu", "invalidFileName": "Nom de fichier invalide", @@ -1248,7 +1248,7 @@ "action": { "create": "Nouvel élément", "import": "Importer un élément", - "edit": "Editer l'élément", + "edit": "Éditer l'élément", "moveResize": "Déplacer / redimensionner l'élément", "duplicate": "Dupliquer l'élément", "remove": "Supprimer l'élément" @@ -1281,7 +1281,7 @@ } }, "edit": { - "title": "Editer l'élément", + "title": "Éditer l'élément", "advancedOptions": { "label": "Options avancées", "title": "Options avancées de l'élément" @@ -1309,7 +1309,7 @@ "widget": { "app": { "name": "App", - "description": "Intégre une application dans le tableau.", + "description": "Intègre une application dans le tableau.", "option": { "appId": { "label": "Choisir une application" @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Autoriser la géolocalisation" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1634,7 +1637,7 @@ } }, "smartHome-entityState": { - "name": "Etat de l'entité", + "name": "État de l'entité", "description": "Affichez l'état d'une entité et optionnellement activez la", "option": { "entityId": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Afficher les infos de la mémoire" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "Afficher le temps de disponibilité" }, @@ -1877,16 +1883,16 @@ "popover": { "information": "Information", "processor": "Processeur : {cpuModelName}", - "memory": "Mémoire: {memory}GiB", - "memoryAvailable": "Disponible: {memoryAvailable}GiB ({percent}%)", + "memory": "Mémoire : {memory}GiB", + "memoryAvailable": "Disponible : {memoryAvailable}GiB ({percent}%)", "version": "Version : {version}", - "uptime": "Disponibilité: {months} Mois, {days} Jours, {hours} Heures, {minutes} Minutes", - "loadAverage": "Charge moyenne:", + "uptime": "Disponibilité : {months} Mois, {days} Jours, {hours} Heures, {minutes} Minutes", + "loadAverage": "Charge moyenne :", "minute": "1 minute", "minutes": "{count} minutes", "used": "Utilisé ", "available": "Disponible", - "lastSeen": "Dernière mise à jour de statut: {lastSeen}" + "lastSeen": "Dernière mise à jour de statut : {lastSeen}" }, "memory": {}, "error": { @@ -1922,7 +1928,7 @@ "memory": "Mémoire", "storage": "Stockage", "uptime": "Disponibilité", - "haState": "Etat HA", + "haState": "État HA", "storageType": { "local": "Stockage local", "shared": "Stockage partagé" @@ -2056,7 +2062,7 @@ "query": "Ville / Code Postal", "latitude": "Latitude", "longitude": "Longitude", - "disabledTooltip": "Veuillez saisir une ville ou un code postal.", + "disabledTooltip": "Veuillez saisir une ville ou un code postal", "unknownLocation": "Lieu inconnu", "search": "Rechercher", "table": { @@ -2075,7 +2081,7 @@ } }, "integration": { - "noData": "Aucune intégration trouvée.", + "noData": "Aucune intégration trouvée", "description": "Cliquez sur pour créer une nouvelle intégration" }, "app": { @@ -2091,7 +2097,7 @@ }, "video": { "name": "Flux vidéo", - "description": "Intégre un flux vidéo ou une vidéo provenant d'une caméra ou d'un site web", + "description": "Intègre un flux vidéo ou une vidéo provenant d'une caméra ou d'un site web", "option": { "feedUrl": { "label": "URL du flux" @@ -2104,7 +2110,7 @@ "label": "Muet" }, "hasControls": { - "label": "Afficher les controles" + "label": "Afficher les contrôles" } }, "error": { @@ -2330,7 +2336,7 @@ "pause": "Mettre en pause l'élément", "resume": "Reprendre l'élément", "delete": { - "title": "Supprimer l'élement", + "title": "Supprimer l'élément", "modalTitle": "Êtes-vous sûr de vouloir supprimer cette tâche ?", "entry": "Supprimer l'entrée", "entryAndFiles": "Supprimer l'entrée et le(s) fichier(s)" @@ -2542,7 +2548,7 @@ "allImagesAlreadyImported": "Toutes les images déjà importées", "onlyAdminCanImport": "Seuls les administrateurs peuvent importer depuis docker", "selectAll": "Tout sélectionner", - "deselectAll": "Tout déselectionner" + "deselectAll": "Tout désélectionner" }, "provider": { "label": "Fournisseur" @@ -2667,7 +2673,7 @@ }, "error": { "integrationsDisconnected": "Aucune donnée disponible, toutes les intégrations sont déconnectées", - "unknownContentOption": "Option de contenu inconnue pour le widget d'état du réseau: " + "unknownContentOption": "Option de contenu inconnue pour le widget d'état du réseau : " }, "name": "État du réseau", "description": "Afficher les périphériques connectés sur un réseau" @@ -2725,6 +2731,7 @@ "option": { "cpu": "CPU", "memory": "Mémoire", + "gpu": "", "network": "Réseau" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "CPU", "memory": "RAM", + "gpu": "", "network": "NET", "up": "UP", "down": "HORS SERVICE" @@ -2757,7 +2765,7 @@ "label": "Mode d'affichage", "option": { "percentage": { - "label": "Pourcentage (ex : 70%)" + "label": "Pourcentage (ex : 76%)" }, "absolute": { "label": "Valeurs absolues (ex. : 800 Go / 1 To)" @@ -2965,7 +2973,7 @@ }, "customCss": { "label": "CSS personnalisé pour ce tableau", - "description": "En outre, vous pouvez personnaliser votre tableau de bord à l'aide de CSS. Réservé aux utilisateurs expérimentés.", + "description": "De plus, personnalisez votre tableau de bord à l'aide de CSS, recommandé uniquement pour les utilisateurs expérimentés", "customClassesAlert": { "title": "Classes personnalisées", "description": "Vous pouvez ajouter des classes personnalisées aux éléments de votre tableau dans les options avancées de chaque élément et les utiliser dans le CSS personnalisé ci-dessus." @@ -3243,7 +3251,7 @@ "label": "Supprimer définitivement", "confirm": { "title": "Supprimer le tableau de bord", - "description": "Êtes-vous sûr de vouloir supprimer le tableau {name}?" + "description": "Êtes-vous sûr de vouloir supprimer le tableau {name} ?" } } }, diff --git a/packages/translation/src/lang/he.json b/packages/translation/src/lang/he.json index 7e4e13549..212114ea6 100644 --- a/packages/translation/src/lang/he.json +++ b/packages/translation/src/lang/he.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "אפשר מיקום גיאוגרפי" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "הצג מידע זיכרון" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "הצג זמן פעולה" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "מעבד", "memory": "זיכרון", + "gpu": "", "network": "רשת" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "מעבד", "memory": "זיכרון", + "gpu": "", "network": "רשת", "up": "העלאה", "down": "הורדה" diff --git a/packages/translation/src/lang/hr.json b/packages/translation/src/lang/hr.json index 513022ea1..c869f5779 100644 --- a/packages/translation/src/lang/hr.json +++ b/packages/translation/src/lang/hr.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Dopusti geolociranje" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/hu.json b/packages/translation/src/lang/hu.json index c0d07454b..7b541e118 100644 --- a/packages/translation/src/lang/hu.json +++ b/packages/translation/src/lang/hu.json @@ -51,7 +51,7 @@ "title": "Import token megadása", "field": { "token": { - "label": "", + "label": "Token", "description": "Írja be a megjelenített import tokent az előző homarr példányból" } }, @@ -153,10 +153,10 @@ "label": "Ikonok használata a pingekhez" }, "defaultSearchEngine": { - "label": "" + "label": "Alapértelmezett kereső" }, "openSearchInNewTab": { - "label": "" + "label": "Nyissa meg a keresési eredményeket új ablakban" } }, "error": { @@ -219,10 +219,10 @@ "changeSearchPreferences": { "notification": { "success": { - "message": "" + "message": "A keresési beállítások sikeresen megváltoztak" }, "error": { - "message": "" + "message": "A keresési beállításokat nem sikerült megváltoztatni" } } }, @@ -307,12 +307,12 @@ "name": "Név", "members": "Tagok", "homeBoard": { - "label": "", - "description": "" + "label": "Főoldal", + "description": "Csak a csoport számára elérhető oldalak választhatók ki" }, "mobileBoard": { - "label": "", - "description": "" + "label": "Mobiloldal", + "description": "Csak a csoport számára elérhető oldalak választhatók ki" } }, "permission": { @@ -514,12 +514,12 @@ "board": { "notification": { "success": { - "title": "", - "message": "" + "title": "Beállítások elmentve", + "message": "Oldal beállítások sikeresen elmentve" }, "error": { - "title": "", - "message": "" + "title": "Nem sikerült menteni a beállításokat", + "message": "Az oldalbeállítások nem menthetők" } } } @@ -527,21 +527,21 @@ "changePosition": { "notification": { "success": { - "message": "" + "message": "Pozíció sikeresen megváltozott" }, "error": { - "message": "" + "message": "Pozíció nem módosítható" } } } }, "defaultGroup": { - "name": "", - "description": "" + "name": "Alapértelmezett csoport", + "description": "{name} - Minden bejelentkezett felhasználó" } }, "app": { - "search": "", + "search": "Alkalmazás megtalálása", "page": { "list": { "title": "Alkalmazások", @@ -599,12 +599,12 @@ "label": "Leírás" }, "url": { - "label": "" + "label": "Url" }, "useDifferentUrlForPing": { "checkbox": { - "label": "", - "description": "" + "label": "Más URL használata a ping-hez", + "description": "Hasznos, ha a Homarr közvetlenül, egy belső hosztnév vagy hálózaton keresztül fér hozzá, hogy elkerülje az internetszolgáltató által használt sávszélesség terhelését" } } }, @@ -612,17 +612,17 @@ "select": { "label": "Alkalmazás kiválasztása", "notFound": "Nem található alkalmazás", - "search": "", - "noResults": "", - "action": "", - "title": "" + "search": "Alkalmazás keresése", + "noResults": "Nincs találat", + "action": "Kiválasztás {app}", + "title": "Válassz egy programot az oldalhoz adáshoz" }, "create": { - "title": "", - "description": "", - "action": "" + "title": "Új alkalmazás készítése", + "description": "Új alkalmazás létrehozása ", + "action": "Alkalmazás készítő megnyitása" }, - "add": "" + "add": "Alkalmazás hozzáadása" } }, "integration": { @@ -649,14 +649,14 @@ "app": { "option": { "existing": { - "title": "", - "label": "" + "title": "Meglévő", + "label": "Meglévő alkalmazás kiválasztása" }, "new": { - "title": "", + "title": "Új", "url": { - "label": "", - "description": "" + "label": "Alkalmazás url", + "description": "Az url, amelyre az alkalmazás a műszerfalról történő eléréskor irányít" } } } @@ -676,9 +676,9 @@ }, "app": { "action": { - "add": "", - "remove": "", - "select": "" + "add": "Alkalmazás megtalálása", + "remove": "Leválasztás", + "select": "Válasszon ki egy alkalmazást a hivatkozáshoz" } } }, @@ -702,21 +702,21 @@ "label": "Név" }, "url": { - "label": "" + "label": "Url" }, "attemptSearchEngineCreation": { - "label": "", - "description": "" + "label": "Keresőmotor létrehozása", + "description": "A \"{kind}\" integráció használható a keresőmotorokkal. Jelölje be ezt a jelölőnégyzetet, ha automatikusan szeretné konfigurálni a keresőmotort." }, "app": { - "sectionTitle": "" + "sectionTitle": "Hivattkozott alkalmazás" }, "createApp": { - "label": "", - "description": "" + "label": "Alkalmazás létrehozása", + "description": "Hozzon létre egy alkalmazást, amelynek ugyanaz a neve és ikonja van, mint az integrációnak. Hagyja üresen az alábbi beviteli mezőt, hogy az alkalmazás az integrációs URL-lel jöjjön létre." }, "appHref": { - "placeholder": "" + "placeholder": "Egyéni alkalmazás URL" } }, "action": { @@ -730,59 +730,59 @@ "error": { "common": { "cause": { - "title": "" + "title": "Részletesebb leírás a problémáról" } }, "unknown": { - "title": "", - "description": "" + "title": "Ismeretlen hiba", + "description": "Egy ismeretlen hiba történt, nyissa meg a részleteket alul, hogy többet megtudjon" }, "parse": { - "title": "", - "description": "" + "title": "Elemzési hiba", + "description": "A válasz nem volt elemezhető. Kérjük, ellenőrizze, hogy az URL a szolgáltatás alap URL-jére mutat-e." }, "authorization": { - "title": "", - "description": "" + "title": "Hitelesítési hiba", + "description": "A kérés nem volt engedélyezett. Kérjük, ellenőrizze, hogy a hitelesítő adatok helyesek-e, és hogy a megfelelő engedélyekkel vannak-e beállítva." }, "statusCode": { - "title": "", - "description": "", - "otherDescription": "", + "title": "Válaszhiba", + "description": "Váratlan {statusCode} ({reason}) választ kapott a címen. Kérjük, ellenőrizze, hogy az URL az integráció alap URL-jére mutat-e.", + "otherDescription": "Váratlan {statusCode} választ kapott a címen. Kérjük, ellenőrizze, hogy az URL az integráció alap URL-jére mutat-e.", "reason": { - "badRequest": "", - "notFound": "", - "tooManyRequests": "", - "internalServerError": "", - "serviceUnavailable": "", - "gatewayTimeout": "" + "badRequest": "Hibás kérés", + "notFound": "Nem található", + "tooManyRequests": "Túl sok kérés", + "internalServerError": "Belső szerverhiba", + "serviceUnavailable": "A szolgáltatás nem elérhető", + "gatewayTimeout": "Átjáró időtúllépés" } }, "certificate": { - "title": "", + "title": "Tanúsítványhiba", "description": { - "expired": "", - "notYetValid": "", - "untrusted": "", - "hostnameMismatch": "" + "expired": "A tanúsítvány lejárt.", + "notYetValid": "A tanúsítvány még nem érvényes.", + "untrusted": "A tanúsítvány nem megbízható.", + "hostnameMismatch": "A tanúsítványban szereplő hosztnév nem egyezik meg az URL-lel." }, "alert": { "permission": { - "title": "", - "message": "" + "title": "Nincsenek megfelelő engedélyek", + "message": "Önnek nincs joga tanúsítványokat megbízhatónak nyilvánítani vagy feltölteni. Kérjük, lépjen kapcsolatba a rendszergazdájával, hogy feltölthesse a szükséges gyökér tanúsítványt." }, "hostnameMismatch": { - "title": "", - "message": "" + "title": "Állomásnév eltérés", + "message": "A tanúsítványban szereplő hosztnév nem egyezik meg azzal a hosztnévvel, amelyhez kapcsolódik. Ez biztonsági kockázatot jelezhet, de mégis dönthet úgy, hogy megbízik ebben a tanúsítványban." }, "extract": { - "title": "", - "message": "" + "title": "A tanúsító hatóság (CA) tanúsítványának kinyerése sikertelen", + "message": "Csak a lánc nélküli, önaláírt tanúsítványok tölthetők le automatikusan. Ha önaláírt tanúsítványt használ, kérjük, feltétlenül töltse fel manuálisan a tanúsító hatóság (CA) tanúsítványát. Az ehhez szükséges lépések leírását itt találja: ." } }, "action": { "retry": { - "label": "" + "label": "Újrapróbál" }, "trust": { "label": "" @@ -920,8 +920,8 @@ "message": "Az útvonal valószínűleg nem helyes" }, "tooManyRequests": { - "title": "", - "message": "" + "title": "Túl sok kérés adott idő alatt", + "message": "Túl sok kérést küldött. Valószínűleg a célrendszer korlátozza vagy elutasítja a kéréseket" } } }, @@ -955,36 +955,36 @@ "newLabel": "Új jelszó" }, "tokenId": { - "label": "", - "newLabel": "" + "label": "Token azonosító", + "newLabel": "Új token azonosító" }, "realm": { - "label": "", - "newLabel": "" + "label": "Tartomány", + "newLabel": "Új tartomány" }, "personalAccessToken": { - "label": "", - "newLabel": "" + "label": "Személyes Hozzáférési Token", + "newLabel": "Új Személyes Hozzáférési Token" }, "topic": { - "label": "", - "newLabel": "" + "label": "Téma", + "newLabel": "Új téma" }, "url": { - "label": "", - "newLabel": "" + "label": "Url", + "newLabel": "Új url" }, "opnsenseApiKey": { - "label": "", - "newLabel": "" + "label": "API kulcs (Kulcs)", + "newLabel": "Új API Kulcs (Kulcs)" }, "opnsenseApiSecret": { - "label": "", - "newLabel": "" + "label": "Api Kulcs (Titkos)", + "newLabel": "Új API Kulcs (Titkos)" }, "githubAppId": { - "label": "", - "newLabel": "" + "label": "App Azonosító", + "newLabel": "Új App Azonosító" }, "githubInstallationId": { "label": "", @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Geolokáció engedélyezése" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Memóriainformációk megjelenítése" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/it.json b/packages/translation/src/lang/it.json index c0737ed8f..16679b728 100644 --- a/packages/translation/src/lang/it.json +++ b/packages/translation/src/lang/it.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Consenti geo-localizzazione" + }, + "allowModals": { + "label": "Consenti modali" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Mostra Informazioni Memoria" }, + "gpu": { + "label": "Mostra informazioni sulla GPU" + }, "showUptime": { "label": "Mostra Uptime" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "CPU", "memory": "Memoria", + "gpu": "GPU", "network": "Rete" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "CPU", "memory": "MEM", + "gpu": "GPU", "network": "NET", "up": "SU", "down": "DOWN" diff --git a/packages/translation/src/lang/ja.json b/packages/translation/src/lang/ja.json index aeb7a1da6..63fc1436d 100644 --- a/packages/translation/src/lang/ja.json +++ b/packages/translation/src/lang/ja.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "位置情報を許可する" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "メモリー情報を表示" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "稼働時間を表示" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "CPU", "memory": "メモリ", + "gpu": "", "network": "ネットワーク" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "CPU", "memory": "MEM", + "gpu": "", "network": "NET", "up": "UP", "down": "DOWN" diff --git a/packages/translation/src/lang/ko.json b/packages/translation/src/lang/ko.json index c7140e4cd..4170c9373 100644 --- a/packages/translation/src/lang/ko.json +++ b/packages/translation/src/lang/ko.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "지리적 위치 허용" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/lt.json b/packages/translation/src/lang/lt.json index 9436577ff..a3268fb95 100644 --- a/packages/translation/src/lang/lt.json +++ b/packages/translation/src/lang/lt.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Leisti nustatyti geografinę buvimo vietą" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/lv.json b/packages/translation/src/lang/lv.json index 05994cbcf..416f8d20e 100644 --- a/packages/translation/src/lang/lv.json +++ b/packages/translation/src/lang/lv.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Atļaut ģeogrāfiskās atrašanās vietas noteikšanu" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Rādīt atmiņas informāciju" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/nl.json b/packages/translation/src/lang/nl.json index eb963c46c..3485be428 100644 --- a/packages/translation/src/lang/nl.json +++ b/packages/translation/src/lang/nl.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Geolocatie toestaan" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Werkgeheugen info weergeven" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "Uptime weergeven" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "Processor", "memory": "Geheugen", + "gpu": "", "network": "Netwerk" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "CPU", "memory": "MEM", + "gpu": "", "network": "NET", "up": "OMHOOG", "down": "OMLAAG" diff --git a/packages/translation/src/lang/no.json b/packages/translation/src/lang/no.json index 690126ff2..7358822d3 100644 --- a/packages/translation/src/lang/no.json +++ b/packages/translation/src/lang/no.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Tillat geolokalisering" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Vis minneinfo" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "Vis oppetid" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "CPU", "memory": "Minne", + "gpu": "", "network": "Nettverk" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "CPU", "memory": "Minne", + "gpu": "", "network": "NETT", "up": "OPP", "down": "NED" diff --git a/packages/translation/src/lang/pl.json b/packages/translation/src/lang/pl.json index 9fdba5951..414be2c40 100644 --- a/packages/translation/src/lang/pl.json +++ b/packages/translation/src/lang/pl.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Zezwalaj na geolokalizację" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Pokaż informacje o pamięci" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "Pokaż czas pracy" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "Procesor", "memory": "Pamięć", + "gpu": "", "network": "Sieć" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "Procesor", "memory": "Pamięć", + "gpu": "", "network": "Sieć", "up": "Aktywny", "down": "Nieaktywny" diff --git a/packages/translation/src/lang/pt.json b/packages/translation/src/lang/pt.json index 6c47bc477..0362995fe 100644 --- a/packages/translation/src/lang/pt.json +++ b/packages/translation/src/lang/pt.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Permitir geolocalização" + }, + "allowModals": { + "label": "Permitir modais" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Mostrar informações da memória" }, + "gpu": { + "label": "Mostrar informações da GPU" + }, "showUptime": { "label": "Exibir tempo de atividade" }, @@ -1997,58 +2003,58 @@ } }, "tracearr": { - "name": "", - "description": "", + "name": "Tracearr", + "description": "Monitoramento avançado de servidor de mídia com dados detalhados de transmissão e transcodificação\n", "option": { "showStreams": { - "label": "" + "label": "Exibir transmissões ativas" }, "showStats": { - "label": "" + "label": "Exibir resumo de estatísticas" }, "showRecentActivity": { - "label": "" + "label": "Exibir Atividade Recente" }, "showViolations": { - "label": "" + "label": "Mostrar Violações" } }, "stats": { - "activeStreams": "", - "totalUsers": "", - "transcodes": "", - "bandwidth": "" + "activeStreams": "Streams Ativas", + "totalUsers": "Total de usuários", + "transcodes": "Transcodificações", + "bandwidth": "Largura da banda" }, "streams": { - "empty": "", - "user": "", - "media": "", - "state": "", - "quality": "", - "device": "", - "progress": "", + "empty": "Sem transmissões ativas", + "user": "Usuário", + "media": "Mídia", + "state": "Estado", + "quality": "Qualidade", + "device": "Dispositivo", + "progress": "Progresso", "videoDecision": { - "directPlay": "", - "transcode": "", - "directStream": "" + "directPlay": "Reprodução direta", + "transcode": "Transcodificação", + "directStream": "Transmissão direta" } }, "recentActivity": { - "title": "", - "empty": "", - "watched": "", - "partial": "" + "title": "Atividade recente", + "empty": "Nenhuma atividade recente", + "watched": "Assistido", + "partial": "Parcial" }, "violations": { - "title": "", - "empty": "", - "rule": "", - "user": "" + "title": "Violações", + "empty": "Não há violações recentes", + "rule": "Regra", + "user": "Usuário" }, - "noSectionsEnabled": "", + "noSectionsEnabled": "Nenhuma seção habilitada", "error": { - "noIntegration": "", - "internalServerError": "" + "noIntegration": "Nenhuma integração Coolify configurada", + "internalServerError": "Falha ao buscar dados do Tracearr" } }, "common": { @@ -2147,50 +2153,50 @@ } }, "immich-serverStats": { - "name": "", - "description": "", + "name": "Estatísticas do Servidor Immich", + "description": "Informações sobre sua instância do Immich", "option": { "showUsers": { - "label": "", - "description": "" + "label": "Mostrar usuários", + "description": "Mostrar a contagem de usuários na sua instância" }, "showPhotos": { - "label": "", - "description": "" + "label": "Mostrar Fotos", + "description": "Mostrar a contagem de fotos na sua instância" }, "showVideos": { - "label": "", - "description": "" + "label": "Mostrar vídeos", + "description": "Mostrar contagem de vídeos na sua instância" }, "showStorage": { - "label": "", - "description": "" + "label": "Mostrar armazenamento", + "description": "Mostrar a quantidade total de armazenamento utilizado" } }, - "users": "", - "photos": "", - "videos": "", - "storage": "" + "users": "Usuários", + "photos": "Fotos", + "videos": "Vídeos", + "storage": "Armazenamento Utilizado" }, "immich-albumCarousel": { - "name": "", + "name": "Álbum do Immich", "option": { "albumId": { - "label": "", - "description": "" + "label": "ID do Álbum", + "description": "Esta é uma identificação única que se refere ao seu álbum Immich. Você pode encontrar essa identificação abrindo um álbum e copiando a string única da URL, por exemplo 'a21aca09-9f05-47dc-b1a1-7b53c547226e'" }, "rotationIntervalSeconds": { - "label": "", - "description": "" + "label": "Intervalo em segundos", + "description": "Girar entre as imagens no álbum a cada X segundos. Deve ser pelo menos 1. Deve estar abaixo de 3600." }, "showPhotoInfo": { - "label": "", - "description": "" + "label": "Mostrar informações da foto", + "description": "Exibir a data da imagem e o índice atual na parte inferior da apresentação" } }, - "description": "", - "noAlbumSelected": "", - "noPhotos": "" + "description": "Apresentação de imagens em seu álbum Imich", + "noAlbumSelected": "Nenhum álbum selecionado, edite o widget e escolha um álbum", + "noPhotos": "Nenhuma foto neste álbum" }, "downloads": { "name": "Cliente de Download", @@ -2725,6 +2731,7 @@ "option": { "cpu": "CPU", "memory": "Memória", + "gpu": "GPU", "network": "Rede" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "CPU", "memory": "MEM", + "gpu": "GPU", "network": "REDE", "up": "ENVIO", "down": "RECEBIDO" @@ -3575,7 +3583,7 @@ "label": "Clima" }, "tracearr": { - "label": "" + "label": "Tracearr" } }, "interval": { diff --git a/packages/translation/src/lang/ro.json b/packages/translation/src/lang/ro.json index c97064d78..367759d61 100644 --- a/packages/translation/src/lang/ro.json +++ b/packages/translation/src/lang/ro.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Permiteți geolocalizarea" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Afișare informații memorie" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/ru.json b/packages/translation/src/lang/ru.json index 27444a388..336f932b6 100644 --- a/packages/translation/src/lang/ru.json +++ b/packages/translation/src/lang/ru.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Разрешить геолокацию" + }, + "allowModals": { + "label": "Разрешить модальные окна" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Показать информацию о памяти" }, + "gpu": { + "label": "Показать информацию о GPU" + }, "showUptime": { "label": "Показывать время работы" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "ЦП", "memory": "Память", + "gpu": "GPU", "network": "Сеть" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "ЦП", "memory": "ОЗУ", + "gpu": "GPU", "network": "Сеть", "up": "Исх.", "down": "Вх." diff --git a/packages/translation/src/lang/sk.json b/packages/translation/src/lang/sk.json index 207429c4d..7a0290e2d 100644 --- a/packages/translation/src/lang/sk.json +++ b/packages/translation/src/lang/sk.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Povolenie geografickej lokalizácie" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Zobraziť informácie o pamäti" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "Zobraziť Uptime" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/sl.json b/packages/translation/src/lang/sl.json index c98da18f2..9f9c5edf7 100644 --- a/packages/translation/src/lang/sl.json +++ b/packages/translation/src/lang/sl.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Omogočanje zemljepisne lokacije" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/sv.json b/packages/translation/src/lang/sv.json index 38ac40bd7..47628f60b 100644 --- a/packages/translation/src/lang/sv.json +++ b/packages/translation/src/lang/sv.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Tillåt geografisk lokalisering" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Visa minnesinformation" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/tr.json b/packages/translation/src/lang/tr.json index 732858cd4..8c889b755 100644 --- a/packages/translation/src/lang/tr.json +++ b/packages/translation/src/lang/tr.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Coğrafi konuma izin ver (geolocation)" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Bellek Bilgilerini Göster" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "Çalışma Süresini Göster" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "İşlemci", "memory": "Bellek", + "gpu": "", "network": "Ağ" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "İşlemci", "memory": "BELLEK", + "gpu": "", "network": "AĞ", "up": "YÜKLEME", "down": "İNDİRME" diff --git a/packages/translation/src/lang/uk.json b/packages/translation/src/lang/uk.json index 22c8bac56..72f827771 100644 --- a/packages/translation/src/lang/uk.json +++ b/packages/translation/src/lang/uk.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Дозволити геолокацію" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "Показати інформацію про пам'ять" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/vi.json b/packages/translation/src/lang/vi.json index 2a59c0256..acc948451 100644 --- a/packages/translation/src/lang/vi.json +++ b/packages/translation/src/lang/vi.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "Cho phép định vị" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/translation/src/lang/zh.json b/packages/translation/src/lang/zh.json index 97bfe3b04..ed36577fc 100644 --- a/packages/translation/src/lang/zh.json +++ b/packages/translation/src/lang/zh.json @@ -1625,6 +1625,9 @@ }, "allowGeolocation": { "label": "允許地址位置" + }, + "allowModals": { + "label": "" } }, "error": { @@ -1858,6 +1861,9 @@ "memory": { "label": "顯示記憶體訊息" }, + "gpu": { + "label": "" + }, "showUptime": { "label": "" }, @@ -2725,6 +2731,7 @@ "option": { "cpu": "", "memory": "", + "gpu": "", "network": "" } }, @@ -2741,6 +2748,7 @@ "card": { "cpu": "", "memory": "", + "gpu": "", "network": "", "up": "", "down": "" diff --git a/packages/ui/package.json b/packages/ui/package.json index 7a66befa1..0556e69c9 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -33,7 +33,7 @@ "@mantine/core": "^8.3.18", "@mantine/dates": "^8.3.18", "@mantine/hooks": "^8.3.18", - "@tabler/icons-react": "^3.40.0", + "@tabler/icons-react": "^3.41.0", "crypto-js": "^4.2.0", "mantine-react-table": "2.0.0-beta.9", "next": "16.2.1", diff --git a/packages/validation/package.json b/packages/validation/package.json index 7b91a5d3e..0ac311093 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -24,7 +24,7 @@ "dependencies": { "@homarr/definitions": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", - "zod": "^4.2.1", + "zod": "^4.3.6", "zod-form-data": "^3.0.1" }, "devDependencies": { diff --git a/packages/validation/src/user.ts b/packages/validation/src/user.ts index c00d9226d..6baaf8150 100644 --- a/packages/validation/src/user.ts +++ b/packages/validation/src/user.ts @@ -57,12 +57,13 @@ export const userBaseCreateSchema = z.object({ password: userPasswordSchema, confirmPassword: z.string(), email: z.string().email().or(z.string().length(0)).optional(), - groupIds: z.array(z.string()), }); -export const userCreateSchema = addConfirmPasswordRefinement(userBaseCreateSchema); +export const userCreateSchema = addConfirmPasswordRefinement(userBaseCreateSchema).and( + z.object({ groupIds: z.array(z.string()) }), +); -export const userInitSchema = addConfirmPasswordRefinement(userBaseCreateSchema.omit({ groupIds: true })); +export const userInitSchema = addConfirmPasswordRefinement(userBaseCreateSchema); export const userSignInSchema = z.object({ name: z.string().min(1), @@ -111,12 +112,15 @@ const baseChangePasswordSchema = z.object({ previousPassword: z.string().min(1), password: userPasswordSchema, confirmPassword: z.string(), - userId: z.string(), }); -export const userChangePasswordSchema = addConfirmPasswordRefinement(baseChangePasswordSchema.omit({ userId: true })); +export const userChangePasswordSchema = addConfirmPasswordRefinement(baseChangePasswordSchema); -export const userChangePasswordApiSchema = addConfirmPasswordRefinement(baseChangePasswordSchema); +export const userChangePasswordApiSchema = addConfirmPasswordRefinement(baseChangePasswordSchema).and( + z.object({ + userId: z.string(), + }), +); export const userChangeHomeBoardsSchema = z.object({ homeBoardId: z.string().nullable(), diff --git a/packages/widgets/package.json b/packages/widgets/package.json index 4c75fac2b..46963188d 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -51,23 +51,23 @@ "@mantine/charts": "^8.3.18", "@mantine/core": "^8.3.18", "@mantine/hooks": "^8.3.18", - "@tabler/icons-react": "^3.40.0", - "@tiptap/extension-color": "3.20.4", - "@tiptap/extension-highlight": "3.20.4", - "@tiptap/extension-image": "3.20.4", - "@tiptap/extension-link": "^3.20.4", - "@tiptap/extension-placeholder": "^3.20.4", - "@tiptap/extension-table": "3.20.4", - "@tiptap/extension-table-cell": "3.20.4", - "@tiptap/extension-table-header": "3.20.4", - "@tiptap/extension-table-row": "3.20.4", - "@tiptap/extension-task-item": "3.20.4", - "@tiptap/extension-task-list": "3.20.4", - "@tiptap/extension-text-align": "3.20.4", - "@tiptap/extension-text-style": "3.20.4", - "@tiptap/extension-underline": "3.20.4", - "@tiptap/react": "^3.20.4", - "@tiptap/starter-kit": "^3.20.4", + "@tabler/icons-react": "^3.41.0", + "@tiptap/extension-color": "3.21.0", + "@tiptap/extension-highlight": "3.21.0", + "@tiptap/extension-image": "3.21.0", + "@tiptap/extension-link": "^3.21.0", + "@tiptap/extension-placeholder": "^3.21.0", + "@tiptap/extension-table": "3.21.0", + "@tiptap/extension-table-cell": "3.21.0", + "@tiptap/extension-table-header": "3.21.0", + "@tiptap/extension-table-row": "3.21.0", + "@tiptap/extension-task-item": "3.21.0", + "@tiptap/extension-task-list": "3.21.0", + "@tiptap/extension-text-align": "3.21.0", + "@tiptap/extension-text-style": "3.21.0", + "@tiptap/extension-underline": "3.21.0", + "@tiptap/react": "^3.21.0", + "@tiptap/starter-kit": "^3.21.0", "clsx": "^2.1.1", "dayjs": "^1.11.20", "mantine-form-zod-resolver": "^1.3.0", @@ -78,7 +78,7 @@ "react-markdown": "^10.1.0", "recharts": "^2.15.4", "video.js": "^8.23.7", - "zod": "^4.2.1" + "zod": "^4.3.6" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/widgets/src/_inputs/index.ts b/packages/widgets/src/_inputs/index.ts index b19ab8c9c..7018e8dc2 100644 --- a/packages/widgets/src/_inputs/index.ts +++ b/packages/widgets/src/_inputs/index.ts @@ -1,5 +1,6 @@ import type { WidgetOptionType } from "../options"; import { WidgetAppInput } from "./widget-app-input"; +import { WidgetDynamicSelectInput } from "./widget-dynamic-select-input"; import { WidgetLocationInput } from "./widget-location-input"; import { WidgetMultiTextInput } from "./widget-multi-text-input"; import { WidgetMultiReleasesRepositoriesInput } from "./widget-multiReleasesRepositories-input"; @@ -23,6 +24,7 @@ const mapping = { app: WidgetAppInput, sortableItemList: WidgetSortedItemListInput, multiReleasesRepositories: WidgetMultiReleasesRepositoriesInput, + dynamicSelect: WidgetDynamicSelectInput, } satisfies Record; export const getInputForType = (type: TType) => { diff --git a/packages/widgets/src/_inputs/widget-dynamic-select-input.tsx b/packages/widgets/src/_inputs/widget-dynamic-select-input.tsx new file mode 100644 index 000000000..39b70b325 --- /dev/null +++ b/packages/widgets/src/_inputs/widget-dynamic-select-input.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { useState } from "react"; +import { Group, Loader, Select } from "@mantine/core"; +import { useDebouncedValue } from "@mantine/hooks"; +import { IconCheck } from "@tabler/icons-react"; + +import { translateIfNecessary } from "@homarr/translation"; +import { useI18n } from "@homarr/translation/client"; + +import type { CommonWidgetInputProps } from "./common"; +import { useWidgetInputTranslation } from "./common"; +import { useFormContext } from "./form"; + +export interface DynamicSelectOption { + value: string; + label: string; +} + +export const WidgetDynamicSelectInput = ({ property, kind, options }: CommonWidgetInputProps<"dynamicSelect">) => { + const t = useI18n(); + const tWidget = useWidgetInputTranslation(kind, property); + const form = useFormContext(); + const inputProps = form.getInputProps(`options.${property}`); + const [search, setSearch] = useState(""); + const [debouncedSearch] = useDebouncedValue(search, 300); + const { isPending, options: selectOptions } = options.useOptions(debouncedSearch, form.values.integrationIds); + const value = inputProps.value as DynamicSelectOption | null; + const onChange = inputProps.onChange as (value: DynamicSelectOption | null) => void; + + return ( +