chore(release): automatic release v1.33.0
This commit is contained in:
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -31,6 +31,7 @@ body:
|
|||||||
label: Version
|
label: Version
|
||||||
description: What version of Homarr are you running?
|
description: What version of Homarr are you running?
|
||||||
options:
|
options:
|
||||||
|
- 1.32.0
|
||||||
- 1.31.0
|
- 1.31.0
|
||||||
- 1.30.1
|
- 1.30.1
|
||||||
- 1.30.0
|
- 1.30.0
|
||||||
|
|||||||
4
.github/renovate.json5
vendored
4
.github/renovate.json5
vendored
@@ -6,10 +6,6 @@
|
|||||||
matchPackagePatterns: ["^@homarr/"],
|
matchPackagePatterns: ["^@homarr/"],
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
matchPackagePatterns: ["^zod$", "^drizzle-zod$", "^zod-form-data$"],
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
|
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
|
||||||
automerge: true,
|
automerge: true,
|
||||||
|
|||||||
2
.github/workflows/automatic-approval.yml
vendored
2
.github/workflows/automatic-approval.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
if: github.actor_id == 158783068 || github.actor_id == 190541745 || github.actor_id == 210161987 # Id of renovate bot and crowdin bot see https://api.github.com/users/homarr-renovate%5Bbot%5D and https://api.github.com/users/homarr-crowdin%5Bbot%5D and https://api.github.com/users/homarr-update-contributors%5Bbot%5D
|
if: github.actor_id == 158783068 || github.actor_id == 190541745 || github.actor_id == 210161987 # Id of renovate bot and crowdin bot see https://api.github.com/users/homarr-renovate%5Bbot%5D and https://api.github.com/users/homarr-crowdin%5Bbot%5D and https://api.github.com/users/homarr-update-contributors%5Bbot%5D
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v2
|
||||||
|
|||||||
12
.github/workflows/code-quality.yml
vendored
12
.github/workflows/code-quality.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
format:
|
format:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
typecheck:
|
typecheck:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
- name: Build docker image
|
- name: Build docker image
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- name: Setup
|
- name: Setup
|
||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
- name: Copy env
|
- name: Copy env
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
|
|||||||
2
.github/workflows/crowdin-upload.yml
vendored
2
.github/workflows/crowdin-upload.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Upload Crowdin translations
|
- name: Upload Crowdin translations
|
||||||
uses: crowdin/github-action@v2
|
uses: crowdin/github-action@v2
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
private_key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
private_key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||||
app_id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
app_id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
digest: ${{ steps.build.outputs.digest }}
|
digest: ${{ steps.build.outputs.digest }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.release.outputs.git_ref }}
|
ref: ${{ needs.release.outputs.git_ref }}
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
digest: ${{ steps.build.outputs.digest }}
|
digest: ${{ steps.build.outputs.digest }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.release.outputs.git_ref }}
|
ref: ${{ needs.release.outputs.git_ref }}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
|
args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Get Next Version
|
- name: Get Next Version
|
||||||
|
|||||||
2
.github/workflows/update-contributors.yml
vendored
2
.github/workflows/update-contributors.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
app_id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
app_id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
private_key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
private_key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
||||||
app_id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
app_id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||||
- name: Setup
|
- name: Setup
|
||||||
|
|||||||
@@ -58,9 +58,9 @@
|
|||||||
"@mantine/tiptap": "^8.2.4",
|
"@mantine/tiptap": "^8.2.4",
|
||||||
"@million/lint": "1.0.14",
|
"@million/lint": "1.0.14",
|
||||||
"@tabler/icons-react": "^3.34.1",
|
"@tabler/icons-react": "^3.34.1",
|
||||||
"@tanstack/react-query": "^5.84.2",
|
"@tanstack/react-query": "^5.85.3",
|
||||||
"@tanstack/react-query-devtools": "^5.84.2",
|
"@tanstack/react-query-devtools": "^5.85.3",
|
||||||
"@tanstack/react-query-next-experimental": "^5.84.2",
|
"@tanstack/react-query-next-experimental": "^5.85.3",
|
||||||
"@trpc/client": "^11.4.4",
|
"@trpc/client": "^11.4.4",
|
||||||
"@trpc/next": "^11.4.4",
|
"@trpc/next": "^11.4.4",
|
||||||
"@trpc/react-query": "^11.4.4",
|
"@trpc/react-query": "^11.4.4",
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"flag-icons": "^7.5.0",
|
"flag-icons": "^7.5.0",
|
||||||
"glob": "^11.0.3",
|
"glob": "^11.0.3",
|
||||||
"jotai": "^2.13.0",
|
"jotai": "^2.13.1",
|
||||||
"mantine-react-table": "2.0.0-beta.9",
|
"mantine-react-table": "2.0.0-beta.9",
|
||||||
"next": "15.4.6",
|
"next": "15.4.6",
|
||||||
"postcss-preset-mantine": "^1.18.0",
|
"postcss-preset-mantine": "^1.18.0",
|
||||||
@@ -87,20 +87,20 @@
|
|||||||
"superjson": "2.2.2",
|
"superjson": "2.2.2",
|
||||||
"swagger-ui-react": "^5.27.1",
|
"swagger-ui-react": "^5.27.1",
|
||||||
"use-deep-compare-effect": "^1.8.1",
|
"use-deep-compare-effect": "^1.8.1",
|
||||||
"zod": "^3.25.76"
|
"zod": "^4.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/chroma-js": "3.1.1",
|
"@types/chroma-js": "3.1.1",
|
||||||
"@types/node": "^22.17.1",
|
"@types/node": "^22.17.2",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/react": "19.1.9",
|
"@types/react": "19.1.10",
|
||||||
"@types/react-dom": "19.1.7",
|
"@types/react-dom": "19.1.7",
|
||||||
"@types/swagger-ui-react": "^5.18.0",
|
"@types/swagger-ui-react": "^5.18.0",
|
||||||
"concurrently": "^9.2.0",
|
"concurrently": "^9.2.0",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.33.0",
|
||||||
"node-loader": "^2.1.0",
|
"node-loader": "^2.1.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { Anchor, Button, Card, Code, Collapse, Divider, PasswordInput, Stack, Text, TextInput } from "@mantine/core";
|
import { Anchor, Button, Card, Code, Collapse, Divider, PasswordInput, Stack, Text, TextInput } from "@mantine/core";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { signIn } from "@homarr/auth/client";
|
import { signIn } from "@homarr/auth/client";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { Button, Card, Stack, TextInput } from "@mantine/core";
|
import { Button, Card, Stack, TextInput } from "@mantine/core";
|
||||||
import { IconArrowRight } from "@tabler/icons-react";
|
import { IconArrowRight } from "@tabler/icons-react";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { startTransition } from "react";
|
import { startTransition } from "react";
|
||||||
import { Button, Card, Group, Stack, Switch, Text } from "@mantine/core";
|
import { Button, Card, Group, Stack, Switch, Text } from "@mantine/core";
|
||||||
import { IconArrowRight } from "@tabler/icons-react";
|
import { IconArrowRight } from "@tabler/icons-react";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { signIn } from "@homarr/auth/client";
|
import { signIn } from "@homarr/auth/client";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Link from "next/link";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
|
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||||
import { IconBox, IconPencil } from "@tabler/icons-react";
|
import { IconBox, IconPencil } from "@tabler/icons-react";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Button, Fieldset, Group, Stack, TextInput } from "@mantine/core";
|
import { Button, Fieldset, Group, Stack, TextInput } from "@mantine/core";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconInfoCircle } from "@tabler/icons-react";
|
import { IconInfoCircle } from "@tabler/icons-react";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { Container, Group, Stack, Title } from "@mantine/core";
|
import { Container, Group, Stack, Title } from "@mantine/core";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import type { IntegrationKind } from "@homarr/definitions";
|
import type { IntegrationKind } from "@homarr/definitions";
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconExternalLink } from "@tabler/icons-react";
|
import { IconExternalLink } from "@tabler/icons-react";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Link from "next/link";
|
|||||||
import type { SegmentedControlItem } from "@mantine/core";
|
import type { SegmentedControlItem } from "@mantine/core";
|
||||||
import { Button, Fieldset, Grid, Group, SegmentedControl, Stack, Textarea, TextInput } from "@mantine/core";
|
import { Button, Fieldset, Grid, Group, SegmentedControl, Stack, Textarea, TextInput } from "@mantine/core";
|
||||||
import { WidgetIntegrationSelect } from "node_modules/@homarr/widgets/src/widget-integration-select";
|
import { WidgetIntegrationSelect } from "node_modules/@homarr/widgets/src/widget-integration-select";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { searchEngineTypes } from "@homarr/definitions";
|
import { searchEngineTypes } from "@homarr/definitions";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Link from "next/link";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
|
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||||
import { IconPencil, IconSearch } from "@tabler/icons-react";
|
import { IconPencil, IconSearch } from "@tabler/icons-react";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
|
|||||||
@@ -1,284 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState, useTransition } from "react";
|
|
||||||
import { ActionIcon, Badge, Button, Card, Group, Select, Stack, Text } from "@mantine/core";
|
|
||||||
import { useMap } from "@mantine/hooks";
|
|
||||||
import { IconPlayerPlay, IconPower, IconSettings } from "@tabler/icons-react";
|
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
|
||||||
import { getMantineColor, useTimeAgo } from "@homarr/common";
|
|
||||||
import type { TaskStatus } from "@homarr/cron-job-status";
|
|
||||||
import { useForm } from "@homarr/form";
|
|
||||||
import { createModal, useModalAction } from "@homarr/modals";
|
|
||||||
import { TranslationFunction } from "@homarr/translation";
|
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
|
||||||
import { IconPowerOff } from "@homarr/ui/icons";
|
|
||||||
|
|
||||||
interface JobsListProps {
|
|
||||||
initialJobs: RouterOutputs["cronJobs"]["getJobs"];
|
|
||||||
}
|
|
||||||
|
|
||||||
type JobName = RouterOutputs["cronJobs"]["getJobs"][number]["name"];
|
|
||||||
|
|
||||||
export const JobsList = ({ initialJobs }: JobsListProps) => {
|
|
||||||
const [jobs] = clientApi.cronJobs.getJobs.useSuspenseQuery(undefined, {
|
|
||||||
initialData: initialJobs,
|
|
||||||
refetchOnMount: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const jobStatusMap = useMap<string, TaskStatus | null>(initialJobs.map(({ name }) => [name, null] as const));
|
|
||||||
|
|
||||||
clientApi.cronJobs.subscribeToStatusUpdates.useSubscription(undefined, {
|
|
||||||
onData: (data) => {
|
|
||||||
jobStatusMap.set(data.name, data);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
{jobs.map((job) => {
|
|
||||||
const status = jobStatusMap.get(job.name);
|
|
||||||
|
|
||||||
return <JobCard key={job.name} job={job} status={status ?? null} />;
|
|
||||||
})}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cronExpressions = [
|
|
||||||
{
|
|
||||||
value: "*/5 * * * * *",
|
|
||||||
label: (t) => t("management.page.tool.tasks.interval.seconds", { interval: 5 }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "*/10 * * * * *",
|
|
||||||
label: (t) => t("management.page.tool.tasks.interval.seconds", { interval: 10 }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "*/20 * * * * *",
|
|
||||||
label: (t) => t("management.page.tool.tasks.interval.seconds", { interval: 20 }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "* * * * *",
|
|
||||||
label: (t) => t("management.page.tool.tasks.interval.minutes", { interval: 1 }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "*/5 * * * *",
|
|
||||||
label: (t) => t("management.page.tool.tasks.interval.minutes", { interval: 5 }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "*/10 * * * *",
|
|
||||||
label: (t) => t("management.page.tool.tasks.interval.minutes", { interval: 10 }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "*/15 * * * *",
|
|
||||||
label: (t) => t("management.page.tool.tasks.interval.minutes", { interval: 15 }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "0 * * * *",
|
|
||||||
label: (t) => t("management.page.tool.tasks.interval.hours", { interval: 1 }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "0 0 * * */1",
|
|
||||||
label: (t) => t("management.page.tool.tasks.interval.midnight"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "0 0 * * 1",
|
|
||||||
label: (t) => t("management.page.tool.tasks.interval.weeklyMonday"),
|
|
||||||
},
|
|
||||||
] satisfies { value: string; label: (t: TranslationFunction) => string }[];
|
|
||||||
|
|
||||||
interface JobCardProps {
|
|
||||||
job: RouterOutputs["cronJobs"]["getJobs"][number];
|
|
||||||
status: TaskStatus | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const JobCard = ({ job, status }: JobCardProps) => {
|
|
||||||
const t = useI18n();
|
|
||||||
const tTasks = useScopedI18n("management.page.tool.tasks");
|
|
||||||
const triggerMutation = clientApi.cronJobs.triggerJob.useMutation();
|
|
||||||
const handleJobTrigger = React.useCallback(
|
|
||||||
async (name: JobName) => {
|
|
||||||
if (status?.status === "running") return;
|
|
||||||
await triggerMutation.mutateAsync(name);
|
|
||||||
},
|
|
||||||
[triggerMutation, status],
|
|
||||||
);
|
|
||||||
|
|
||||||
const { openModal } = useModalAction(TaskConfigurationModal);
|
|
||||||
const [isEnabled, setEnabled] = useState(job.isEnabled);
|
|
||||||
const disableMutation = clientApi.cronJobs.disableJob.useMutation();
|
|
||||||
const enableMutation = clientApi.cronJobs.enableJob.useMutation();
|
|
||||||
|
|
||||||
const [activeStatePending, startActiveTransition] = useTransition();
|
|
||||||
const handleActiveChange = () =>
|
|
||||||
startActiveTransition(async () => {
|
|
||||||
if (isEnabled) {
|
|
||||||
await disableMutation.mutateAsync(job.name, {
|
|
||||||
onSuccess() {
|
|
||||||
setEnabled(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await enableMutation.mutateAsync(job.name, {
|
|
||||||
onSuccess() {
|
|
||||||
setEnabled(true);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card key={job.name} withBorder>
|
|
||||||
<Group justify={"space-between"} gap={"md"}>
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Group>
|
|
||||||
<Text>{tTasks(`job.${job.name}.label`)}</Text>
|
|
||||||
<StatusBadge isEnabled={isEnabled} status={status} />
|
|
||||||
{status?.lastExecutionStatus === "error" && <Badge color="red">{tTasks("status.error")}</Badge>}
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs">
|
|
||||||
{status && (
|
|
||||||
<>
|
|
||||||
<TimeAgo timestamp={status.lastExecutionTimestamp} />
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
•
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{cronExpressions.find((expression) => expression.value === job.cron)?.label(t) ?? job.cron}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Group>
|
|
||||||
{!job.preventManualExecution && (
|
|
||||||
<ActionIcon
|
|
||||||
onClick={() => handleJobTrigger(job.name)}
|
|
||||||
disabled={status?.status === "running"}
|
|
||||||
loading={triggerMutation.isPending}
|
|
||||||
variant="default"
|
|
||||||
size="xl"
|
|
||||||
radius="xl"
|
|
||||||
>
|
|
||||||
<IconPlayerPlay color={getMantineColor("green", 6)} stroke={1.5} />
|
|
||||||
</ActionIcon>
|
|
||||||
)}
|
|
||||||
<ActionIcon onClick={handleActiveChange} loading={activeStatePending} variant="default" size="xl" radius="xl">
|
|
||||||
{isEnabled ? (
|
|
||||||
<IconPower color={getMantineColor("green", 6)} stroke={1.5} />
|
|
||||||
) : (
|
|
||||||
<IconPowerOff color={getMantineColor("gray", 6)} stroke={1.5} />
|
|
||||||
)}
|
|
||||||
</ActionIcon>
|
|
||||||
<ActionIcon
|
|
||||||
onClick={() =>
|
|
||||||
openModal(
|
|
||||||
{ job },
|
|
||||||
{
|
|
||||||
title: tTasks("settings.title", {
|
|
||||||
jobName: tTasks(`job.${job.name}.label`),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
variant={"default"}
|
|
||||||
size={"xl"}
|
|
||||||
radius={"xl"}
|
|
||||||
>
|
|
||||||
<IconSettings stroke={1.5} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface StatusBadgeProps {
|
|
||||||
isEnabled: boolean;
|
|
||||||
status: TaskStatus | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StatusBadge = ({ isEnabled, status }: StatusBadgeProps) => {
|
|
||||||
const t = useScopedI18n("management.page.tool.tasks");
|
|
||||||
if (!isEnabled) return <Badge color="yellow">{t("status.disabled")}</Badge>;
|
|
||||||
|
|
||||||
if (!status) return null;
|
|
||||||
|
|
||||||
if (status.status === "running") return <Badge color="green">{t("status.running")}</Badge>;
|
|
||||||
return <Badge variant="default">{t("status.idle")}</Badge>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TimeAgo = ({ timestamp }: { timestamp: string }) => {
|
|
||||||
const timeAgo = useTimeAgo(new Date(timestamp));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Text size={"sm"} c={"dimmed"}>
|
|
||||||
{timeAgo}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TaskConfigurationModal = createModal<{
|
|
||||||
job: RouterOutputs["cronJobs"]["getJobs"][number];
|
|
||||||
}>(({ actions, innerProps }) => {
|
|
||||||
const t = useI18n();
|
|
||||||
const form = useForm({
|
|
||||||
initialValues: {
|
|
||||||
cron: innerProps.job.cron,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const { mutateAsync, isPending } = clientApi.cronJobs.updateJobInterval.useMutation();
|
|
||||||
const utils = clientApi.useUtils();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
onSubmit={form.onSubmit(async (values) => {
|
|
||||||
utils.cronJobs.getJobs.setData(undefined, (data) =>
|
|
||||||
data?.map((job) =>
|
|
||||||
job.name === innerProps.job.name
|
|
||||||
? {
|
|
||||||
...job,
|
|
||||||
cron: values.cron,
|
|
||||||
}
|
|
||||||
: job,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await mutateAsync(
|
|
||||||
{
|
|
||||||
name: innerProps.job.name,
|
|
||||||
cron: values.cron,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess() {
|
|
||||||
actions.closeModal();
|
|
||||||
},
|
|
||||||
async onSettled() {
|
|
||||||
await utils.cronJobs.getJobs.invalidate();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Stack gap="sm">
|
|
||||||
<Select
|
|
||||||
label={t("management.page.tool.tasks.field.interval.label")}
|
|
||||||
{...form.getInputProps("cron")}
|
|
||||||
data={cronExpressions.map(({ value, label }) => ({ value, label: label(t) }))}
|
|
||||||
/>
|
|
||||||
<Group justify="end">
|
|
||||||
<Button variant="subtle" color="gray" disabled={isPending} onClick={actions.closeModal}>
|
|
||||||
{t("common.action.cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" loading={isPending}>
|
|
||||||
{t("common.action.save")}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}).withOptions({
|
|
||||||
defaultTitle: "",
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,451 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ActionIcon, Badge, Button, Group, Select, Text } from "@mantine/core";
|
||||||
|
import { useMap } from "@mantine/hooks";
|
||||||
|
import { IconPlayerPlay, IconPower, IconRefresh } from "@tabler/icons-react";
|
||||||
|
import type { MRT_ColumnDef } from "mantine-react-table";
|
||||||
|
import { MantineReactTable } from "mantine-react-table";
|
||||||
|
|
||||||
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import { useTimeAgo } from "@homarr/common";
|
||||||
|
import type { TaskStatus } from "@homarr/cron-job-status";
|
||||||
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
|
import type { ScopedTranslationFunction, TranslationFunction } from "@homarr/translation";
|
||||||
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
|
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
||||||
|
import { IconPowerOff } from "@homarr/ui/icons";
|
||||||
|
|
||||||
|
const cronExpressions = [
|
||||||
|
{
|
||||||
|
value: "*/5 * * * * *",
|
||||||
|
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.seconds", { interval: 5 }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "*/10 * * * * *",
|
||||||
|
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.seconds", { interval: 10 }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "*/20 * * * * *",
|
||||||
|
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.seconds", { interval: 20 }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "*/30 * * * * *",
|
||||||
|
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.seconds", { interval: 30 }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "* * * * *",
|
||||||
|
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.minutes", { interval: 1 }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "*/5 * * * *",
|
||||||
|
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.minutes", { interval: 5 }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "*/10 * * * *",
|
||||||
|
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.minutes", { interval: 10 }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "*/15 * * * *",
|
||||||
|
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.minutes", { interval: 15 }),
|
||||||
|
},
|
||||||
|
// Every hour
|
||||||
|
{
|
||||||
|
value: "0 * * * *",
|
||||||
|
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.hours", { interval: 1 }),
|
||||||
|
},
|
||||||
|
// Every two hours
|
||||||
|
{
|
||||||
|
value: "0 */2 * * *",
|
||||||
|
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.hours", { interval: 2 }),
|
||||||
|
},
|
||||||
|
// Every four hours
|
||||||
|
{
|
||||||
|
value: "0 */4 * * *",
|
||||||
|
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.hours", { interval: 4 }),
|
||||||
|
},
|
||||||
|
// Every midnight
|
||||||
|
{
|
||||||
|
value: "0 0 * * */1",
|
||||||
|
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.midnight"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "0 0 * * 1",
|
||||||
|
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.weeklyMonday"),
|
||||||
|
},
|
||||||
|
] satisfies { value: string; label: (t: TranslationFunction) => string }[];
|
||||||
|
|
||||||
|
type JobData = RouterOutputs["cronJobs"]["getJobs"][number] & {
|
||||||
|
status?: TaskStatus | null;
|
||||||
|
lastExecutionTime?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createColumns = (
|
||||||
|
t: TranslationFunction,
|
||||||
|
tTasks: ScopedTranslationFunction<"management.page.tool.tasks">,
|
||||||
|
jobStatusMap: Map<string, TaskStatus | null>,
|
||||||
|
triggerMutation: ReturnType<typeof clientApi.cronJobs.triggerJob.useMutation>,
|
||||||
|
updateIntervalMutation: ReturnType<typeof clientApi.cronJobs.updateJobInterval.useMutation>,
|
||||||
|
enableMutation: ReturnType<typeof clientApi.cronJobs.enableJob.useMutation>,
|
||||||
|
disableMutation: ReturnType<typeof clientApi.cronJobs.disableJob.useMutation>,
|
||||||
|
loadingStates: Map<string, { toggle: boolean; trigger: boolean; interval: boolean }>,
|
||||||
|
): MRT_ColumnDef<JobData>[] => [
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: tTasks("field.name.label"),
|
||||||
|
Cell({ row }) {
|
||||||
|
const status = jobStatusMap.get(row.original.name);
|
||||||
|
return (
|
||||||
|
<Group gap="xs">
|
||||||
|
<Text fw={500}>{tTasks(`job.${row.original.name}.label`)}</Text>
|
||||||
|
<StatusBadge isEnabled={row.original.isEnabled} status={status ?? null} />
|
||||||
|
{status?.lastExecutionStatus === "error" && (
|
||||||
|
<Badge color="red" size="sm">
|
||||||
|
{tTasks("status.error")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "cron",
|
||||||
|
header: tTasks("field.interval.label"),
|
||||||
|
size: 200,
|
||||||
|
Cell({ row }) {
|
||||||
|
const handleIntervalChange = (newCron: string | null) => {
|
||||||
|
if (!newCron || newCron === row.original.cron) return;
|
||||||
|
|
||||||
|
const currentStates = loadingStates.get(row.original.name) ?? {
|
||||||
|
toggle: false,
|
||||||
|
trigger: false,
|
||||||
|
interval: false,
|
||||||
|
};
|
||||||
|
loadingStates.set(row.original.name, {
|
||||||
|
...currentStates,
|
||||||
|
interval: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateIntervalMutation.mutate({
|
||||||
|
name: row.original.name,
|
||||||
|
cron: newCron,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
const updatedStates = loadingStates.get(row.original.name) ?? {
|
||||||
|
toggle: false,
|
||||||
|
trigger: false,
|
||||||
|
interval: false,
|
||||||
|
};
|
||||||
|
loadingStates.set(row.original.name, {
|
||||||
|
...updatedStates,
|
||||||
|
interval: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={row.original.cron}
|
||||||
|
onChange={handleIntervalChange}
|
||||||
|
data={cronExpressions.map(({ value, label }) => ({
|
||||||
|
value,
|
||||||
|
label: label(t),
|
||||||
|
}))}
|
||||||
|
size="sm"
|
||||||
|
disabled={loadingStates.get(row.original.name)?.interval ?? false}
|
||||||
|
style={{ minWidth: 180 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "lastExecutionTime",
|
||||||
|
header: tTasks("field.lastExecution.label"),
|
||||||
|
size: 150,
|
||||||
|
Cell({ row }) {
|
||||||
|
const status = jobStatusMap.get(row.original.name);
|
||||||
|
if (!status?.lastExecutionTimestamp) {
|
||||||
|
return (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
—
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <TimeAgo timestamp={status.lastExecutionTimestamp} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: tTasks("field.actions.label"),
|
||||||
|
size: 120,
|
||||||
|
enableSorting: false,
|
||||||
|
Cell({ row }) {
|
||||||
|
const status = jobStatusMap.get(row.original.name);
|
||||||
|
|
||||||
|
const handleToggleEnabled = () => {
|
||||||
|
const currentStates = loadingStates.get(row.original.name) ?? {
|
||||||
|
toggle: false,
|
||||||
|
trigger: false,
|
||||||
|
interval: false,
|
||||||
|
};
|
||||||
|
loadingStates.set(row.original.name, {
|
||||||
|
...currentStates,
|
||||||
|
toggle: true,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
if (row.original.isEnabled) {
|
||||||
|
disableMutation.mutate(row.original.name);
|
||||||
|
} else {
|
||||||
|
enableMutation.mutate(row.original.name);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
const updatedStates = loadingStates.get(row.original.name) ?? {
|
||||||
|
toggle: false,
|
||||||
|
trigger: false,
|
||||||
|
interval: false,
|
||||||
|
};
|
||||||
|
loadingStates.set(row.original.name, {
|
||||||
|
...updatedStates,
|
||||||
|
toggle: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTrigger = () => {
|
||||||
|
if (status?.status === "running") return;
|
||||||
|
|
||||||
|
const currentStates = loadingStates.get(row.original.name) ?? {
|
||||||
|
toggle: false,
|
||||||
|
trigger: false,
|
||||||
|
interval: false,
|
||||||
|
};
|
||||||
|
loadingStates.set(row.original.name, {
|
||||||
|
...currentStates,
|
||||||
|
trigger: true,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
triggerMutation.mutate(row.original.name);
|
||||||
|
} finally {
|
||||||
|
const updatedStates = loadingStates.get(row.original.name) ?? {
|
||||||
|
toggle: false,
|
||||||
|
trigger: false,
|
||||||
|
interval: false,
|
||||||
|
};
|
||||||
|
loadingStates.set(row.original.name, {
|
||||||
|
...updatedStates,
|
||||||
|
trigger: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group gap="xs">
|
||||||
|
{!row.original.preventManualExecution && (
|
||||||
|
<ActionIcon
|
||||||
|
onClick={handleTrigger}
|
||||||
|
disabled={status?.status === "running"}
|
||||||
|
loading={loadingStates.get(row.original.name)?.trigger ?? false}
|
||||||
|
variant="light"
|
||||||
|
color="green"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<IconPlayerPlay size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
<ActionIcon
|
||||||
|
onClick={handleToggleEnabled}
|
||||||
|
loading={loadingStates.get(row.original.name)?.toggle ?? false}
|
||||||
|
variant="light"
|
||||||
|
color={row.original.isEnabled ? "green" : "gray"}
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
{row.original.isEnabled ? <IconPower size={16} /> : <IconPowerOff size={16} />}
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface TasksTableProps {
|
||||||
|
initialJobs: RouterOutputs["cronJobs"]["getJobs"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TasksTable = ({ initialJobs }: TasksTableProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const tTasks = useScopedI18n("management.page.tool.tasks");
|
||||||
|
|
||||||
|
const { data: jobs } = clientApi.cronJobs.getJobs.useQuery(undefined, {
|
||||||
|
initialData: initialJobs,
|
||||||
|
refetchOnMount: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const jobStatusMap = useMap<string, TaskStatus | null>(initialJobs.map(({ name }) => [name, null] as const));
|
||||||
|
|
||||||
|
const loadingStates = useMap<string, { toggle: boolean; trigger: boolean; interval: boolean }>();
|
||||||
|
|
||||||
|
clientApi.cronJobs.subscribeToStatusUpdates.useSubscription(undefined, {
|
||||||
|
onData: (data) => {
|
||||||
|
jobStatusMap.set(data.name, data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const triggerMutation = clientApi.cronJobs.triggerJob.useMutation({
|
||||||
|
onError() {
|
||||||
|
showErrorNotification({
|
||||||
|
title: t("common.error"),
|
||||||
|
message: tTasks("trigger.error.message"),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
showSuccessNotification({
|
||||||
|
title: t("common.success"),
|
||||||
|
message: tTasks("trigger.success.message"),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const updateIntervalMutation = clientApi.cronJobs.updateJobInterval.useMutation({
|
||||||
|
onError() {
|
||||||
|
showErrorNotification({
|
||||||
|
title: t("common.error"),
|
||||||
|
message: tTasks("interval.update.error.message"),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
await utils.cronJobs.getJobs.invalidate();
|
||||||
|
showSuccessNotification({
|
||||||
|
title: t("common.success"),
|
||||||
|
message: tTasks("interval.update.success.message"),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const enableMutation = clientApi.cronJobs.enableJob.useMutation({
|
||||||
|
onError() {
|
||||||
|
showErrorNotification({
|
||||||
|
title: t("common.error"),
|
||||||
|
message: tTasks("toggle.error.message"),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
await utils.cronJobs.getJobs.invalidate();
|
||||||
|
showSuccessNotification({
|
||||||
|
title: t("common.success"),
|
||||||
|
message: tTasks("enable.success.message"),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const disableMutation = clientApi.cronJobs.disableJob.useMutation({
|
||||||
|
onError() {
|
||||||
|
showErrorNotification({
|
||||||
|
title: t("common.error"),
|
||||||
|
message: tTasks("toggle.error.message"),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
await utils.cronJobs.getJobs.invalidate();
|
||||||
|
showSuccessNotification({
|
||||||
|
title: t("common.success"),
|
||||||
|
message: tTasks("disable.success.message"),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Utils for refresh functionality
|
||||||
|
const utils = clientApi.useUtils();
|
||||||
|
const handleRefreshAsync = async () => {
|
||||||
|
try {
|
||||||
|
await utils.cronJobs.getJobs.invalidate();
|
||||||
|
showSuccessNotification({
|
||||||
|
title: t("common.success"),
|
||||||
|
message: tTasks("refresh.success.message"),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
showErrorNotification({
|
||||||
|
title: t("common.error"),
|
||||||
|
message: tTasks("refresh.error.message"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const table = useTranslatedMantineReactTable({
|
||||||
|
data: jobs,
|
||||||
|
enableDensityToggle: false,
|
||||||
|
enableColumnActions: false,
|
||||||
|
enableColumnFilters: false,
|
||||||
|
enablePagination: false,
|
||||||
|
enableRowSelection: false,
|
||||||
|
enableTableFooter: false,
|
||||||
|
enableBottomToolbar: false,
|
||||||
|
enableRowActions: false,
|
||||||
|
enableColumnOrdering: false,
|
||||||
|
enableSorting: false,
|
||||||
|
enableSortingRemoval: false,
|
||||||
|
positionGlobalFilter: "right",
|
||||||
|
mantineSearchTextInputProps: {
|
||||||
|
placeholder: tTasks("table.search", { count: String(jobs.length) }),
|
||||||
|
style: { minWidth: 300 },
|
||||||
|
},
|
||||||
|
initialState: { density: "xs", showGlobalFilter: true },
|
||||||
|
renderTopToolbarCustomActions: () => (
|
||||||
|
<Button variant="default" rightSection={<IconRefresh size="1rem" />} onClick={handleRefreshAsync}>
|
||||||
|
{tTasks("action.refresh.label")}
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
columns: createColumns(
|
||||||
|
t,
|
||||||
|
tTasks,
|
||||||
|
jobStatusMap,
|
||||||
|
triggerMutation,
|
||||||
|
updateIntervalMutation,
|
||||||
|
enableMutation,
|
||||||
|
disableMutation,
|
||||||
|
loadingStates,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return <MantineReactTable table={table} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface StatusBadgeProps {
|
||||||
|
isEnabled: boolean;
|
||||||
|
status: TaskStatus | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusBadge = ({ isEnabled, status }: StatusBadgeProps) => {
|
||||||
|
const tTasks = useScopedI18n("management.page.tool.tasks");
|
||||||
|
|
||||||
|
if (!isEnabled) {
|
||||||
|
return (
|
||||||
|
<Badge color="yellow" size="sm">
|
||||||
|
{tTasks("status.disabled")}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status) return null;
|
||||||
|
|
||||||
|
if (status.status === "running") {
|
||||||
|
return (
|
||||||
|
<Badge color="green" size="sm">
|
||||||
|
{tTasks("status.running")}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge variant="default" size="sm">
|
||||||
|
{tTasks("status.idle")}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TimeAgo = ({ timestamp }: { timestamp: string }) => {
|
||||||
|
const timeAgo = useTimeAgo(new Date(timestamp));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{timeAgo}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { Box, Title } from "@mantine/core";
|
import { Stack, Title } from "@mantine/core";
|
||||||
|
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { getScopedI18n } from "@homarr/translation/server";
|
import { getScopedI18n } from "@homarr/translation/server";
|
||||||
|
|
||||||
|
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||||
import { createMetaTitle } from "~/metadata";
|
import { createMetaTitle } from "~/metadata";
|
||||||
import { JobsList } from "./_components/jobs-list";
|
import { TasksTable } from "./_components/tasks-table";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
export async function generateMetadata() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
@@ -27,10 +28,15 @@ export default async function TasksPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const jobs = await api.cronJobs.getJobs();
|
const jobs = await api.cronJobs.getJobs();
|
||||||
|
const tTasks = await getScopedI18n("management.page.tool.tasks");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<>
|
||||||
<Title mb={"md"}>Tasks</Title>
|
<DynamicBreadcrumb />
|
||||||
<JobsList initialJobs={jobs} />
|
<Stack>
|
||||||
</Box>
|
<Title order={1}>{tTasks("title")}</Title>
|
||||||
|
<TasksTable initialJobs={jobs} />
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, Group, Stack } from "@mantine/core";
|
import { Button, Group, Stack } from "@mantine/core";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, Group, Select, Stack, Switch } from "@mantine/core";
|
import { Button, Group, Select, Stack, Switch } from "@mantine/core";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Button, Group, Radio, Stack } from "@mantine/core";
|
|||||||
import type { DayOfWeek } from "@mantine/dates";
|
import type { DayOfWeek } from "@mantine/dates";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import localeData from "dayjs/plugin/localeData";
|
import localeData from "dayjs/plugin/localeData";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, Group, Stack, Switch } from "@mantine/core";
|
import { Button, Group, Stack, Switch } from "@mantine/core";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useListState } from "@mantine/hooks";
|
import { useListState } from "@mantine/hooks";
|
||||||
import { IconPlus, IconUserCheck } from "@tabler/icons-react";
|
import { IconPlus, IconUserCheck } from "@tabler/icons-react";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { everyoneGroup, groupPermissions } from "@homarr/definitions";
|
import { everyoneGroup, groupPermissions } from "@homarr/definitions";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { Button, Grid, Group, NumberInput, Stack } from "@mantine/core";
|
import { Button, Grid, Group, NumberInput, Stack } from "@mantine/core";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import type { GridStack } from "@homarr/gridstack";
|
import type { GridStack } from "@homarr/gridstack";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import { useUpdateBoard } from "@homarr/boards/updater";
|
import { useUpdateBoard } from "@homarr/boards/updater";
|
||||||
import type { dynamicSectionOptionsSchema } from "@homarr/validation/shared";
|
import type { dynamicSectionOptionsSchema } from "@homarr/validation/shared";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, CloseButton, ColorInput, Group, Stack, TextInput, useMantineTheme } from "@mantine/core";
|
import { Button, CloseButton, ColorInput, Group, Stack, TextInput, useMantineTheme } from "@mantine/core";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
"@homarr/widgets": "workspace:^0.1.0",
|
"@homarr/widgets": "workspace:^0.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"fastify": "^5.4.0",
|
"fastify": "^5.5.0",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.2",
|
||||||
"undici": "7.13.0"
|
"undici": "7.13.0"
|
||||||
},
|
},
|
||||||
@@ -45,12 +45,12 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node": "^22.17.1",
|
"@types/node": "^22.17.2",
|
||||||
"dotenv-cli": "^10.0.0",
|
"dotenv-cli": "^10.0.0",
|
||||||
"esbuild": "^0.25.8",
|
"esbuild": "^0.25.9",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.33.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"tsx": "4.20.3",
|
"tsx": "4.20.4",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"tsx": "4.20.3",
|
"tsx": "4.20.4",
|
||||||
"ws": "^8.18.3"
|
"ws": "^8.18.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"esbuild": "^0.25.8",
|
"esbuild": "^0.25.9",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.33.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
|
|||||||
16
package.json
16
package.json
@@ -36,12 +36,12 @@
|
|||||||
"@semantic-release/changelog": "^6.0.3",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/commit-analyzer": "^13.0.1",
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/git": "^10.0.1",
|
||||||
"@semantic-release/github": "^11.0.3",
|
"@semantic-release/github": "^11.0.4",
|
||||||
"@semantic-release/npm": "^12.0.2",
|
"@semantic-release/npm": "^12.0.2",
|
||||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||||
"@testcontainers/redis": "^11.5.1",
|
"@testcontainers/redis": "^11.5.1",
|
||||||
"@turbo/gen": "^2.5.5",
|
"@turbo/gen": "^2.5.6",
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"semantic-release": "^24.2.7",
|
"semantic-release": "^24.2.7",
|
||||||
"testcontainers": "^11.5.1",
|
"testcontainers": "^11.5.1",
|
||||||
"turbo": "^2.5.5",
|
"turbo": "^2.5.6",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
@@ -74,14 +74,14 @@
|
|||||||
"overrides": {
|
"overrides": {
|
||||||
"proxmox-api>undici": "7.13.0"
|
"proxmox-api>undici": "7.13.0"
|
||||||
},
|
},
|
||||||
|
"patchedDependencies": {
|
||||||
|
"@types/node-unifi": "patches/@types__node-unifi.patch"
|
||||||
|
},
|
||||||
"allowUnusedPatches": true,
|
"allowUnusedPatches": true,
|
||||||
"ignoredBuiltDependencies": [
|
"ignoredBuiltDependencies": [
|
||||||
"@scarf/scarf",
|
"@scarf/scarf",
|
||||||
"core-js-pure",
|
"core-js-pure",
|
||||||
"protobufjs"
|
"protobufjs"
|
||||||
],
|
]
|
||||||
"patchedDependencies": {
|
|
||||||
"@types/node-unifi": "patches/@types__node-unifi.patch"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.33.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"@homarr/auth": "workspace:^0.1.0",
|
"@homarr/auth": "workspace:^0.1.0",
|
||||||
"@homarr/certificates": "workspace:^0.1.0",
|
"@homarr/certificates": "workspace:^0.1.0",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/cron-job-api": "workspace:^0.1.0",
|
"@homarr/cron-job-api": "workspace:^0.1.0",
|
||||||
"@homarr/cron-job-status": "workspace:^0.1.0",
|
"@homarr/cron-job-status": "workspace:^0.1.0",
|
||||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||||
@@ -41,7 +42,7 @@
|
|||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@kubernetes/client-node": "^1.3.0",
|
"@kubernetes/client-node": "^1.3.0",
|
||||||
"@tanstack/react-query": "^5.84.2",
|
"@tanstack/react-query": "^5.85.3",
|
||||||
"@trpc/client": "^11.4.4",
|
"@trpc/client": "^11.4.4",
|
||||||
"@trpc/react-query": "^11.4.4",
|
"@trpc/react-query": "^11.4.4",
|
||||||
"@trpc/server": "^11.4.4",
|
"@trpc/server": "^11.4.4",
|
||||||
@@ -51,14 +52,14 @@
|
|||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.2",
|
||||||
"trpc-to-openapi": "^2.4.0",
|
"trpc-to-openapi": "^3.0.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^4.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.33.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createEnv } from "@t3-oss/env-nextjs";
|
import { z } from "zod/v4";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { shouldSkipEnvValidation } from "@homarr/common/env-validation";
|
import { createEnv } from "@homarr/core/infrastructure/env";
|
||||||
|
|
||||||
export const env = createEnv({
|
export const env = createEnv({
|
||||||
server: {
|
server: {
|
||||||
@@ -10,6 +9,4 @@ export const env = createEnv({
|
|||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME,
|
KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME,
|
||||||
},
|
},
|
||||||
skipValidation: shouldSkipEnvValidation(),
|
|
||||||
emptyStringAsUndefined: true,
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import type { Session } from "@homarr/auth";
|
import type { Session } from "@homarr/auth";
|
||||||
import { hasQueryAccessToIntegrationsAsync } from "@homarr/auth/server";
|
import { hasQueryAccessToIntegrationsAsync } from "@homarr/auth/server";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { and, eq } from "@homarr/db";
|
import { and, eq } from "@homarr/db";
|
||||||
import { items } from "@homarr/db/schema";
|
import { items } from "@homarr/db/schema";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
import { asc, eq, inArray, like } from "@homarr/db";
|
import { asc, eq, inArray, like } from "@homarr/db";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
@@ -1623,7 +1623,7 @@ const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, use
|
|||||||
const forKind = <T extends WidgetKind>(kind: T) =>
|
const forKind = <T extends WidgetKind>(kind: T) =>
|
||||||
z.object({
|
z.object({
|
||||||
kind: z.literal(kind),
|
kind: z.literal(kind),
|
||||||
options: z.record(z.unknown()),
|
options: z.record(z.string(), z.unknown()),
|
||||||
});
|
});
|
||||||
|
|
||||||
const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kind))).and(sharedItemSchema);
|
const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kind))).and(sharedItemSchema);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { X509Certificate } from "node:crypto";
|
import { X509Certificate } from "node:crypto";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
|
||||||
import { zfd } from "zod-form-data";
|
import { zfd } from "zod-form-data";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server";
|
import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server";
|
||||||
import { and, eq } from "@homarr/db";
|
import { and, eq } from "@homarr/db";
|
||||||
import { trustedCertificateHostnames } from "@homarr/db/schema";
|
import { trustedCertificateHostnames } from "@homarr/db/schema";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import { certificateValidFileNameSchema, superRefineCertificateFile } from "@homarr/validation/certificates";
|
import { certificateValidFileNameSchema, checkCertificateFile } from "@homarr/validation/certificates";
|
||||||
|
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export const certificateRouter = createTRPCRouter({
|
|||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(
|
.input(
|
||||||
zfd.formData({
|
zfd.formData({
|
||||||
file: zfd.file().superRefine(superRefineCertificateFile),
|
file: zfd.file().check(checkCertificateFile),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import type { Container, ContainerState, Docker, Port } from "@homarr/docker";
|
import type { Container, ContainerState, Docker, Port } from "@homarr/docker";
|
||||||
import { DockerSingleton } from "@homarr/docker";
|
import { DockerSingleton } from "@homarr/docker";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { analyseOldmarrImportForRouterAsync, analyseOldmarrImportInputSchema } from "@homarr/old-import/analyse";
|
import { analyseOldmarrImportForRouterAsync, analyseOldmarrImportInputSchema } from "@homarr/old-import/analyse";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
|
|
||||||
import packageJson from "../../../../package.json";
|
import packageJson from "../../../../package.json";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createId, objectEntries } from "@homarr/common";
|
import { createId, objectEntries } from "@homarr/common";
|
||||||
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { randomBytes } from "crypto";
|
import { randomBytes } from "crypto";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
import { asc, eq } from "@homarr/db";
|
import { asc, eq } from "@homarr/db";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { fetchWithTimeout } from "@homarr/common";
|
import { fetchWithTimeout } from "@homarr/common";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
import z from "zod";
|
import z from "zod/v4";
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import { logLevels } from "@homarr/log/constants";
|
import { logLevels } from "@homarr/log/constants";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
import type { InferInsertModel } from "@homarr/db";
|
import type { InferInsertModel } from "@homarr/db";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { onboarding } from "@homarr/db/schema";
|
import { onboarding } from "@homarr/db/schema";
|
||||||
import { onboardingSteps } from "@homarr/definitions";
|
import { onboardingSteps } from "@homarr/definitions";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
import { asc, eq, like } from "@homarr/db";
|
import { asc, eq, like } from "@homarr/db";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { and, eq } from "@homarr/db";
|
import { and, eq } from "@homarr/db";
|
||||||
import { sectionCollapseStates, sections } from "@homarr/db/schema";
|
import { sectionCollapseStates, sections } from "@homarr/db/schema";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
|
import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||||
import type { ServerSettings } from "@homarr/server-settings";
|
import type { ServerSettings } from "@homarr/server-settings";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import type { Session } from "@homarr/auth";
|
import type { Session } from "@homarr/auth";
|
||||||
import type { Modify } from "@homarr/common/types";
|
import type { Modify } from "@homarr/common/types";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { sendPingRequestAsync } from "@homarr/ping";
|
import { sendPingRequestAsync } from "@homarr/ping";
|
||||||
import { pingChannel, pingUrlChannel } from "@homarr/redis";
|
import { pingChannel, pingUrlChannel } from "@homarr/redis";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import { radarrReleaseTypes } from "@homarr/integrations/types";
|
import { radarrReleaseTypes } from "@homarr/integrations/types";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import type { Modify } from "@homarr/common/types";
|
import type { Modify } from "@homarr/common/types";
|
||||||
import type { Integration } from "@homarr/db/schema";
|
import type { Integration } from "@homarr/db/schema";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import type { Modify } from "@homarr/common/types";
|
import type { Modify } from "@homarr/common/types";
|
||||||
import type { Integration } from "@homarr/db/schema";
|
import type { Integration } from "@homarr/db/schema";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import { createIntegrationAsync } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import type { StreamSession } from "@homarr/integrations";
|
import type { StreamSession } from "@homarr/integrations";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import type { MinecraftServerStatus } from "@homarr/request-handler/minecraft-server-status";
|
import type { MinecraftServerStatus } from "@homarr/request-handler/minecraft-server-status";
|
||||||
import { minecraftServerStatusRequestHandler } from "@homarr/request-handler/minecraft-server-status";
|
import { minecraftServerStatusRequestHandler } from "@homarr/request-handler/minecraft-server-status";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import SuperJSON from "superjson";
|
import SuperJSON from "superjson";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { eq } from "@homarr/db";
|
import { eq } from "@homarr/db";
|
||||||
import { boards, items } from "@homarr/db/schema";
|
import { boards, items } from "@homarr/db/schema";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { escapeForRegEx } from "@tiptap/react";
|
import { escapeForRegEx } from "@tiptap/react";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import { releasesRequestHandler } from "@homarr/request-handler/releases";
|
import { releasesRequestHandler } from "@homarr/request-handler/releases";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
|
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import { createIntegrationAsync } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { fetchStockPriceHandler } from "@homarr/request-handler/stock-price";
|
import { fetchStockPriceHandler } from "@homarr/request-handler/stock-price";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { fetchWithTimeout } from "@homarr/common";
|
import { fetchWithTimeout } from "@homarr/common";
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import type { AnyZodObject, ZodIntersection, ZodObject } from "zod";
|
import type { ZodIntersection, ZodObject } from "zod/v4";
|
||||||
|
|
||||||
export function convertIntersectionToZodObject<TIntersection extends ZodIntersection<AnyZodObject, AnyZodObject>>(
|
export function convertIntersectionToZodObject<TIntersection extends ZodIntersection<ZodObject, ZodObject>>(
|
||||||
intersection: TIntersection,
|
intersection: TIntersection,
|
||||||
) {
|
) {
|
||||||
const { _def } = intersection;
|
const left = intersection.def.left;
|
||||||
|
const right = intersection.def.right;
|
||||||
|
|
||||||
// Merge the shapes
|
// Merge the shapes
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
const mergedShape = { ...left.def.shape, ...right.def.shape };
|
||||||
const mergedShape = { ..._def.left.shape, ..._def.right.shape };
|
|
||||||
|
|
||||||
// Return a new ZodObject
|
// Return a new ZodObject
|
||||||
return z.object(mergedShape) as unknown as TIntersection extends ZodIntersection<infer TLeft, infer TRight>
|
return z.object(mergedShape) as unknown as TIntersection extends ZodIntersection<infer TLeft, infer TRight>
|
||||||
? TLeft extends AnyZodObject
|
? TLeft extends ZodObject
|
||||||
? TRight extends AnyZodObject
|
? TRight extends ZodObject
|
||||||
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
? ZodObject<TLeft["shape"] & TRight["shape"]>
|
||||||
ZodObject<TLeft["shape"] & TRight["shape"], any, any, z.infer<TLeft> & z.infer<TRight>>
|
|
||||||
: never
|
: never
|
||||||
: never
|
: never
|
||||||
: never;
|
: never;
|
||||||
|
|||||||
23
packages/api/src/test/schema-merger.spec.ts
Normal file
23
packages/api/src/test/schema-merger.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import z from "zod/v4";
|
||||||
|
|
||||||
|
import { convertIntersectionToZodObject } from "../schema-merger";
|
||||||
|
|
||||||
|
describe("convertIntersectionToZodObject should convert zod intersection to zod object", () => {
|
||||||
|
test("should merge two ZodObjects with different properties", () => {
|
||||||
|
const objectA = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
});
|
||||||
|
const objectB = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const intersection = objectA.and(objectB);
|
||||||
|
|
||||||
|
const result = convertIntersectionToZodObject(intersection);
|
||||||
|
|
||||||
|
expect(result.def.type).toBe("object");
|
||||||
|
expect(result.shape).toHaveProperty("id");
|
||||||
|
expect(result.shape).toHaveProperty("name");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
import { initTRPC, TRPCError } from "@trpc/server";
|
import { initTRPC, TRPCError } from "@trpc/server";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import type { OpenApiMeta } from "trpc-to-openapi";
|
import type { OpenApiMeta } from "trpc-to-openapi";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod/v4";
|
||||||
|
|
||||||
import type { Session } from "@homarr/auth";
|
import type { Session } from "@homarr/auth";
|
||||||
import { FlattenError } from "@homarr/common";
|
import { FlattenError } from "@homarr/common";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createBooleanSchema, createDurationSchema, createEnv } from "@homarr/core/infrastructure/env";
|
import { createBooleanSchema, createDurationSchema, createEnv } from "@homarr/core/infrastructure/env";
|
||||||
import { supportedAuthProviders } from "@homarr/definitions";
|
import { supportedAuthProviders } from "@homarr/definitions";
|
||||||
@@ -19,7 +19,7 @@ const authProvidersSchema = z
|
|||||||
return false;
|
return false;
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.default("credentials");
|
.default(["credentials"]);
|
||||||
|
|
||||||
const authProviders = authProvidersSchema.safeParse(process.env.AUTH_PROVIDERS).data ?? [];
|
const authProviders = authProvidersSchema.safeParse(process.env.AUTH_PROVIDERS).data ?? [];
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
"next-auth": "5.0.0-beta.29",
|
"next-auth": "5.0.0-beta.29",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"zod": "^3.25.76"
|
"zod": "^4.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/bcrypt": "6.0.0",
|
"@types/bcrypt": "6.0.0",
|
||||||
"@types/cookies": "0.9.1",
|
"@types/cookies": "0.9.1",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.33.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { and, eq } from "@homarr/db";
|
import { and, eq } from "@homarr/db";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CredentialsSignin } from "@auth/core/errors";
|
import { CredentialsSignin } from "@auth/core/errors";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createId, extractErrorMessage } from "@homarr/common";
|
import { createId, extractErrorMessage } from "@homarr/common";
|
||||||
import type { Database, InferInsertModel } from "@homarr/db";
|
import type { Database, InferInsertModel } from "@homarr/db";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { expireDateAfter, generateSessionToken } from "../session";
|
import { expireDateAfter, generateSessionToken } from "../session";
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.33.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.33.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,8 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"esbuild": "^0.25.8",
|
"esbuild": "^0.25.9",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.33.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const recreateAdmin = command({
|
|||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.error("Invalid username:");
|
console.error("Invalid username:");
|
||||||
console.error(result.error.errors.map((error) => `- ${error.message}`).join("\n"));
|
console.error(result.error.issues.map((error) => `- ${error.message}`).join("\n"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { randomBytes } from "crypto";
|
import { randomBytes } from "crypto";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createEnv } from "@homarr/core/infrastructure/env";
|
import { createEnv } from "@homarr/core/infrastructure/env";
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export const env = createEnv({
|
|||||||
server: {
|
server: {
|
||||||
SECRET_ENCRYPTION_KEY: z
|
SECRET_ENCRYPTION_KEY: z
|
||||||
.string({
|
.string({
|
||||||
required_error: `SECRET_ENCRYPTION_KEY is required${errorSuffix}`,
|
error: `SECRET_ENCRYPTION_KEY is required${errorSuffix}`,
|
||||||
})
|
})
|
||||||
.min(64, {
|
.min(64, {
|
||||||
message: `SECRET_ENCRYPTION_KEY has to be 64 characters${errorSuffix}`,
|
message: `SECRET_ENCRYPTION_KEY has to be 64 characters${errorSuffix}`,
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
"./types": "./src/types.ts",
|
"./types": "./src/types.ts",
|
||||||
"./server": "./src/server.ts",
|
"./server": "./src/server.ts",
|
||||||
"./client": "./src/client.ts",
|
"./client": "./src/client.ts",
|
||||||
"./env": "./env.ts",
|
"./env": "./env.ts"
|
||||||
"./env-validation": "./src/env-validation.ts"
|
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
@@ -35,14 +34,14 @@
|
|||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.1",
|
||||||
"undici": "7.13.0",
|
"undici": "7.13.0",
|
||||||
"zod": "^3.25.76",
|
"zod": "^4.0.14",
|
||||||
"zod-validation-error": "^3.5.3"
|
"zod-validation-error": "^3.5.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.33.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const trueStrings = ["1", "yes", "t", "true"];
|
|
||||||
const falseStrings = ["0", "no", "f", "false"];
|
|
||||||
|
|
||||||
export const createBooleanSchema = (defaultValue: boolean) =>
|
|
||||||
z
|
|
||||||
.string()
|
|
||||||
.default(defaultValue.toString())
|
|
||||||
.transform((value, ctx) => {
|
|
||||||
const normalized = value.trim().toLowerCase();
|
|
||||||
if (trueStrings.includes(normalized)) return true;
|
|
||||||
if (falseStrings.includes(normalized)) return false;
|
|
||||||
|
|
||||||
throw new Error(`Invalid boolean value for ${ctx.path.join(".")}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createDurationSchema = (defaultValue: `${number}${"s" | "m" | "h" | "d"}`) =>
|
|
||||||
z
|
|
||||||
.string()
|
|
||||||
.regex(/^\d+[smhd]?$/)
|
|
||||||
.default(defaultValue)
|
|
||||||
.transform((duration) => {
|
|
||||||
const lastChar = duration[duration.length - 1] as "s" | "m" | "h" | "d";
|
|
||||||
if (!isNaN(Number(lastChar))) {
|
|
||||||
return Number(defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
const multipliers = {
|
|
||||||
s: 1,
|
|
||||||
m: 60,
|
|
||||||
h: 60 * 60,
|
|
||||||
d: 60 * 60 * 24,
|
|
||||||
};
|
|
||||||
const numberDuration = Number(duration.slice(0, -1));
|
|
||||||
const multiplier = multipliers[lastChar];
|
|
||||||
|
|
||||||
return numberDuration * multiplier;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const shouldSkipEnvValidation = () =>
|
|
||||||
Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION) || process.env.npm_lifecycle_event === "lint";
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ZodError } from "zod";
|
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { ZodError } from "zod/v4";
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
export type MaybePromise<T> = T | Promise<T>;
|
export type MaybePromise<T> = T | Promise<T>;
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ export type Inverse<T extends Invertible> = {
|
|||||||
|
|
||||||
type Invertible = Record<PropertyKey, PropertyKey>;
|
type Invertible = Record<PropertyKey, PropertyKey>;
|
||||||
|
|
||||||
export type inferSearchParamsFromSchema<TSchema extends z.AnyZodObject> = inferSearchParamsFromSchemaInner<
|
export type inferSearchParamsFromSchema<TSchema extends z.ZodObject> = inferSearchParamsFromSchemaInner<
|
||||||
z.infer<TSchema>
|
z.infer<TSchema>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|||||||
@@ -26,13 +26,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@t3-oss/env-nextjs": "^0.13.8",
|
"@t3-oss/env-nextjs": "^0.13.8",
|
||||||
"ioredis": "5.7.0",
|
"ioredis": "5.7.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^4.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.33.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
packages/core/src/infrastructure/env/schemas.ts
vendored
17
packages/core/src/infrastructure/env/schemas.ts
vendored
@@ -1,19 +1,16 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
const trueStrings = ["1", "yes", "t", "true"];
|
const trueStrings = ["1", "yes", "t", "true"];
|
||||||
const falseStrings = ["0", "no", "f", "false"];
|
const falseStrings = ["0", "no", "f", "false"];
|
||||||
|
|
||||||
export const createBooleanSchema = (defaultValue: boolean) =>
|
export const createBooleanSchema = (defaultValue: boolean) =>
|
||||||
z
|
z
|
||||||
.string()
|
.stringbool({
|
||||||
.default(defaultValue.toString())
|
truthy: trueStrings,
|
||||||
.transform((value, ctx) => {
|
falsy: falseStrings,
|
||||||
const normalized = value.trim().toLowerCase();
|
case: "insensitive",
|
||||||
if (trueStrings.includes(normalized)) return true;
|
})
|
||||||
if (falseStrings.includes(normalized)) return false;
|
.default(defaultValue);
|
||||||
|
|
||||||
throw new Error(`Invalid boolean value for ${ctx.path.join(".")}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createDurationSchema = (defaultValue: `${number}${"s" | "m" | "h" | "d"}`) =>
|
export const createDurationSchema = (defaultValue: `${number}${"s" | "m" | "h" | "d"}`) =>
|
||||||
z
|
z
|
||||||
|
|||||||
@@ -29,21 +29,21 @@
|
|||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@tanstack/react-query": "^5.84.2",
|
"@tanstack/react-query": "^5.85.3",
|
||||||
"@trpc/client": "^11.4.4",
|
"@trpc/client": "^11.4.4",
|
||||||
"@trpc/server": "^11.4.4",
|
"@trpc/server": "^11.4.4",
|
||||||
"@trpc/tanstack-react-query": "^11.4.4",
|
"@trpc/tanstack-react-query": "^11.4.4",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"zod": "^3.25.76"
|
"zod": "^4.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/react": "19.1.9",
|
"@types/react": "19.1.10",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.33.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.33.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user