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
+
+ |
+
@@ -342,15 +349,15 @@
Transmission
- |
-
+ |
+
TrueNAS
- |
-|
+ |
+
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 (
+ |