Compare commits

..
60 Commits
Author SHA1 Message Date
Releases Homarr e179b28bc0 chore(release): 1.34.0 [skip ci] 2025-08-22 19:16:20 +00:00
homarr-releases[bot] 3218cd2333 chore(release): automatic release v1.34.0 2025-08-22 19:14:51 +00:00
homarr-update-contributors[bot] 8d5d258905 chore: update contributors (#3903)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2025-08-22 12:33:59 +00:00
homarr-crowdin[bot] d2ac99765b chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-08-22 03:29:26 +00:00
homarr-crowdin[bot] 00d979931b chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-08-21 00:49:56 +00:00
Meier Lukas 6fb32a6061 ci: add on-release workflow to trigger docs-release, update bug-report-template and update security-policy (#3881) 2025-08-20 20:37:22 +02:00
homarr-renovate[bot] 1451c3c41e fix(deps): update nextjs monorepo to v15.5.0 (#3896)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-20 18:18:09 +00:00
Meier Lukas 498acdcdc1 fix(nextcloud): integration ignores trusted certificates (#3893) 2025-08-20 20:12:13 +02:00
homarr-crowdin[bot] afdf9d9c0f chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-08-20 00:49:44 +00:00
homarr-renovate[bot] f2ff90b6e2 chore(deps): update pnpm to v10.15.0 (#3891)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-19 18:36:38 +00:00
homarr-renovate[bot] fad7207efe fix(deps): update tanstack-query monorepo to ^5.85.5 (#3890)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-19 12:35:41 +00:00
homarr-renovate[bot] 683e817493 fix(deps): update nextjs monorepo to v15.4.7 (#3886)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-19 11:19:23 +00:00
homarr-renovate[bot] de98156d84 chore(deps): update dependency @vitejs/plugin-react to ^5.0.1 (#3888)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-19 10:22:14 +00:00
homarr-renovate[bot] 7685438790 fix(deps): update tanstack-query monorepo to ^5.85.4 (#3889)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-19 09:23:12 +00:00
homarr-renovate[bot] 1a8e40e1f9 fix(deps): update dependency typescript-eslint to ^8.40.0 (#3884)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-18 18:28:44 +00:00
homarr-crowdin[bot] 1c32bb73bb chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-08-18 00:58:31 +00:00
Meier Lukas acd09c8c02 feat(app-widget): add layout option (#3875) 2025-08-17 17:49:05 +02:00
homarr-renovate[bot] d7b2298d14 fix(deps): update dependency undici to v7.14.0 (#3879)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-17 15:00:18 +00:00
homarr-renovate[bot] 20a2ea8968 chore(deps): update dependency proxmox-api&gt;undici to v7.14.0 (#3878)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-17 13:30:51 +00:00
homarr-renovate[bot] 7e700b2732 fix(deps): update mantine monorepo to ^8.2.5 (#3874)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-17 09:20:44 +00:00
Meier Lukas adad43f9fe chore(issue-template): add version 1.33.0 2025-08-17 11:07:01 +02:00
Meier Lukas 950636e81d feat(app-widget): multiline description (#3873) 2025-08-16 23:39:20 +02:00
homarr-renovate[bot] a88f1c3cba chore(deps): update amannn/action-semantic-pull-request action to v6 (#3852)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-16 14:09:23 +02:00
homarr-renovate[bot] 09fdb167a7 fix(deps): update dependency zod-validation-error to v4 (#3670)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-16 14:08:06 +02:00
homarr-renovate[bot] 72cdba79cb fix(deps): update dependency mantine-form-zod-resolver to ^1.3.0 (#3868)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-16 03:17:53 +00:00
homarr-renovate[bot] 82fcdae7e5 fix(deps): update dependency zod to ^4.0.17 (#3867)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-16 01:14:49 +00:00
homarr-crowdin[bot] 7c4ad40198 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-08-16 00:50:29 +00:00
homarr-renovate[bot] 2457f9e4a7 fix(deps): update dependency trpc-to-openapi to ^3.0.1 (#3761)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-15 19:18:34 +00:00
homarr-renovate[bot] dc43a015c4 fix(deps): update dependency drizzle-zod to ^0.8.3 (#3866)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-15 19:18:19 +00:00
Releases Homarr f281b45cf8 chore(release): 1.33.0 [skip ci] 2025-08-15 19:17:28 +00:00
homarr-releases[bot] 8417ddebd7 chore(release): automatic release v1.33.0 2025-08-15 19:15:03 +00:00
9d14fcba36 feat(tasks): replace card layout with table interface for better UX (#3804)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-08-15 20:57:07 +02:00
Leandro Scardua 063b9a87c7 add correct path (#3848) 2025-08-15 18:20:24 +00:00
Meier Lukasandhomarr-renovate[bot] 5c99622fa8 fix(deps): upgrade zod to v4 and fix breaking changes (#3461)
* fix(deps): update dependency drizzle-zod to ^0.8.2

* chore: update zod to v4 import

* fix: path is no longer available in transform context

* fix: AnyZodObject does no longer exist

* fix: auth env.ts using wrong createEnv and remove unused file env-validation.ts

* fix: required_error no longer exists on z.string

* fix: zod error map is deprecated and replaced with config

* fix: default requires callback now

* fix: migrate zod resolver for mantine

* fix: remove unused form translation file

* fix: wrong enum type

* fix: record now requires two arguments

* fix: add-confirm-password-refinement type issues

* fix: add missing first record argument for entityStateSchema

* fix: migrate superrefine to check

* fix(deps): upgrade zod-form-data to v3

* fix: migrate superRefine to check for mediaUploadSchema

* fix: authProvidersSchema default is array

* fix: use stringbool instead of custom implementation

* fix: record requires first argument

* fix: migrate superRefine to check for certificate router

* fix: confirm pasword refinement is overwriting types

* fix: email optional not working

* fix: migrate intersection to object converter

* fix: safe parse return value rename

* fix: easier access for min and max number value

* fix: migrate superRefine to check for oldmarr import file

* fix: inference of enum shape for old-import board-size wrong

* fix: errors renamed to issues

* chore: address pull request feedback

* fix: zod form requires object

* fix: inference for use-zod-form not working

* fix: remove unnecessary convertion

* fix(deps): upgrade trpc-to-openapi to v3

* fix: build error

* fix: migrate missing zod imports to v4

* fix: migrate zod records to v4

* fix: missing core package dependency in api module

* fix: unable to convert custom zod schema to openapi schema

* fix(deps): upgrade zod to v4

* chore(renovate): enable zod dependency updates

* test: add simple unit test for convertIntersectionToZodObject

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-15 18:15:58 +00:00
Meier Lukas b7455d18ed fix(dashdot): disabled widgets cause parse errors (#3861) 2025-08-15 18:11:57 +00:00
homarr-renovate[bot] 12ced491b3 chore(deps): update dependency @types/node to ^22.17.2 (#3863)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-15 13:34:06 +00:00
homarr-renovate[bot] 98d344c4b7 fix(deps): update turbo monorepo to ^2.5.6 (#3865)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-15 12:36:17 +00:00
homarr-update-contributors[bot] f36ae8b52b chore: update contributors (#3864)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2025-08-15 12:34:37 +00:00
homarr-crowdin[bot] eea387b092 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-08-15 00:55:42 +00:00
Meier Lukas 7ffcd1a08e fix(mysql): increase max media size restricted from 16kb to 4gb (#3839) 2025-08-14 21:12:56 +02:00
homarr-renovate[bot] c72c154305 fix(deps): update tanstack-query monorepo to ^5.85.3 (#3860)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-14 13:38:35 +00:00
homarr-renovate[bot] e9cb878169 fix(deps): update tanstack-query monorepo to ^5.85.2 (#3859)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-14 09:24:50 +00:00
homarr-renovate[bot] 70ce3e168b fix(deps): update tanstack-query monorepo to ^5.85.1 (#3858)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-14 08:27:25 +00:00
homarr-renovate[bot] 8a4198db7e fix(deps): update dependency @ianvs/prettier-plugin-sort-imports to ^4.6.2 (#3854)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-14 01:17:56 +00:00
homarr-crowdin[bot] ab5c03512e chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-08-14 00:54:17 +00:00
homarr-renovate[bot] 134ce6f82d chore(deps): update dependency esbuild to ^0.25.9 (#3849)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-13 01:18:13 +00:00
homarr-renovate[bot] 99c42722d9 chore(deps): update dependency @types/react to v19.1.10 (#3841)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-12 14:21:56 +00:00
homarr-renovate[bot] 7d123b83e0 fix(deps): update tanstack-query monorepo to ^5.85.0 (#3846)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-12 13:38:02 +00:00
homarr-renovate[bot] 7a4961e9ad fix(deps): update dependency tsx to v4.20.4 (#3845)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-12 13:37:31 +00:00
homarr-renovate[bot] 433be5e82c fix(deps): update dependency typescript-eslint to ^8.39.1 (#3840)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-11 18:29:38 +00:00
homarr-renovate[bot] 8044abd093 chore(deps): update dependency @vitejs/plugin-react to v5 (#3802)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-11 17:02:24 +02:00
homarr-renovate[bot] 16efaae67e chore(deps): update dependency @semantic-release/github to ^11.0.4 (#3833)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-11 14:59:28 +00:00
homarr-renovate[bot] 53860dda9b chore(deps): update actions/checkout action to v5 (#3837)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-11 16:57:38 +02:00
homarr-renovate[bot] ff1e02ec78 fix(deps): update dependency fastify to ^5.5.0 (#3836)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-11 12:38:47 +00:00
homarr-renovate[bot] 5fe5d6f64d fix(deps): update dependency jotai to ^2.13.1 (#3834)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-11 03:45:55 +00:00
Meier Lukas 166de16bef chore(docs): update sitemap 2025-08-10 20:20:01 +02:00
homarr-crowdin[bot] f9506b0ce5 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-08-10 01:01:34 +00:00
homarr-crowdin[bot] 47226f7032 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-08-09 00:52:39 +00:00
homarr-renovate[bot] aeb338f6b1 chore(deps): update dependency eslint to ^9.33.0 (#3820)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-08 21:21:19 +00:00
Meier Lukas f6b7ca1059 chore(issue-template): add version 1.32.0 2025-08-08 21:29:56 +02:00
263 changed files with 8046 additions and 2847 deletions

No files matched your search

+4
View File
@@ -31,6 +31,10 @@ body:
label: Version
description: What version of Homarr are you running?
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.33.0
- 1.32.0
- 1.31.0
- 1.30.1
- 1.30.0
-4
View File
@@ -6,10 +6,6 @@
matchPackagePatterns: ["^@homarr/"],
enabled: false,
},
{
matchPackagePatterns: ["^zod$", "^drizzle-zod$", "^zod-form-data$"],
enabled: false,
},
{
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
automerge: true,
+1 -1
View File
@@ -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
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Obtain token
id: obtainToken
uses: tibdex/github-app-token@v2
+6 -6
View File
@@ -22,7 +22,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup
uses: ./tooling/github/setup
@@ -37,7 +37,7 @@ jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup
uses: ./tooling/github/setup
@@ -48,7 +48,7 @@ jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup
uses: ./tooling/github/setup
@@ -59,7 +59,7 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup
uses: ./tooling/github/setup
@@ -77,7 +77,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup
uses: ./tooling/github/setup
- name: Build docker image
@@ -100,7 +100,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup
uses: ./tooling/github/setup
- name: Copy env
@@ -14,6 +14,6 @@ jobs:
validate-pull-request-title:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
- uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Obtain token
id: obtainToken
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Upload Crowdin translations
uses: crowdin/github-action@v2
@@ -53,7 +53,7 @@ jobs:
with:
private_key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
app_id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
- uses: actions/checkout@v4
- uses: actions/checkout@v5
if: env.SKIP_RELEASE == 'false'
with:
persist-credentials: false
@@ -110,7 +110,7 @@ jobs:
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
ref: ${{ needs.release.outputs.git_ref }}
@@ -149,7 +149,7 @@ jobs:
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
ref: ${{ needs.release.outputs.git_ref }}
@@ -27,7 +27,7 @@ jobs:
with:
args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Get Next Version
+82
View File
@@ -0,0 +1,82 @@
permissions: {}
on:
release:
types: [published]
jobs:
trigger-docs-release:
name: Trigger Documentation Release
runs-on: ubuntu-latest
steps:
- name: Obtain token
id: obtainToken
uses: tibdex/github-app-token@v2
with:
private_key: ${{ secrets.HOMARR_DOCS_RELEASE_APP_PRIVATE_KEY }}
app_id: ${{ vars.HOMARR_DOCS_RELEASE_APP_ID }}
installation_retrieval_mode: repository
installation_retrieval_payload: homarr-labs/documentation
- name: Trigger documentation release
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
SOURCE_TAG: ${{ github.event.release.tag_name }}
run: |
curl -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/homarr-labs/documentation/dispatches \
-d @- <<EOF
{
"event_type": "trigger-release",
"client_payload": {
"tag": "${SOURCE_TAG}"
}
}
EOF
update-bug-report-template:
name: Update Bug Report Template
runs-on: ubuntu-latest
steps:
- name: Obtain token
id: obtainToken
uses: tibdex/github-app-token@v2
with:
private_key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
app_id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
- name: Checkout code
uses: actions/checkout@v5
with:
token: ${{ steps.obtainToken.outputs.token }}
- name: Setup
uses: ./tooling/github/setup
- run: pnpm run scripts:update-bug-report-template
env:
NEXT_VERSION: ${{ github.event.release.tag_name }}
- name: Commit changes
run: |
git config --global user.email "175486441+homarr-releases[bot]@users.noreply.github.com"
git config --global user.name "Releases Homarr"
git add .
git commit -m "chore: update bug report template"
- name: Create Pull Request
id: create-pull-request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ steps.obtainToken.outputs.token }}
branch: update-bug-report-template
base: dev
title: "chore: update bug report template"
delete-branch: true
body: |
This PR automatically updates the bug report template of Homarr in the .github/ISSUE_TEMPLATE/bug_report.yml
- name: Install GitHub CLI
run: sudo apt-get install -y gh
- name: Enable auto-merge
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
run: |
gh pr merge ${{steps.create-pull-request.outputs.pull-request-number}} --auto --squash
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
app_id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
@@ -25,7 +25,7 @@ jobs:
private_key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
app_id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
- name: Setup
+45
View File
@@ -1,3 +1,48 @@
## [1.34.0](https://github.com/homarr-labs/homarr/compare/v1.33.0...v1.34.0) (2025-08-22)
### Features
* **app-widget:** add layout option ([#3875](https://github.com/homarr-labs/homarr/issues/3875)) ([acd09c8](https://github.com/homarr-labs/homarr/commit/acd09c8c02c13daac6e5b2432bb72640f075cec6))
* **app-widget:** multiline description ([#3873](https://github.com/homarr-labs/homarr/issues/3873)) ([950636e](https://github.com/homarr-labs/homarr/commit/950636e81d4a934183f27694829dd3af05e5373f))
### Bug Fixes
* **deps:** update dependency drizzle-zod to ^0.8.3 ([#3866](https://github.com/homarr-labs/homarr/issues/3866)) ([dc43a01](https://github.com/homarr-labs/homarr/commit/dc43a015c43d13ae01a0f32be654c756f41eb352))
* **deps:** update dependency mantine-form-zod-resolver to ^1.3.0 ([#3868](https://github.com/homarr-labs/homarr/issues/3868)) ([72cdba7](https://github.com/homarr-labs/homarr/commit/72cdba79cbbaa03f35c90dd71a3703bacbe57558))
* **deps:** update dependency trpc-to-openapi to ^3.0.1 ([#3761](https://github.com/homarr-labs/homarr/issues/3761)) ([2457f9e](https://github.com/homarr-labs/homarr/commit/2457f9e4a754221e8674a95d721ffc77573a4633))
* **deps:** update dependency typescript-eslint to ^8.40.0 ([#3884](https://github.com/homarr-labs/homarr/issues/3884)) ([1a8e40e](https://github.com/homarr-labs/homarr/commit/1a8e40e1f9dbd4dbcf4ec0c54aef3297bc4cea7f))
* **deps:** update dependency undici to v7.14.0 ([#3879](https://github.com/homarr-labs/homarr/issues/3879)) ([d7b2298](https://github.com/homarr-labs/homarr/commit/d7b2298d1488ef8d38558864cf359fc23bc5bb26))
* **deps:** update dependency zod to ^4.0.17 ([#3867](https://github.com/homarr-labs/homarr/issues/3867)) ([82fcdae](https://github.com/homarr-labs/homarr/commit/82fcdae7e5cbc326f0051beb56c3a3805623e2a0))
* **deps:** update dependency zod-validation-error to v4 ([#3670](https://github.com/homarr-labs/homarr/issues/3670)) ([09fdb16](https://github.com/homarr-labs/homarr/commit/09fdb167a76db2caf3f9aa253b516ee28baba3dd))
* **deps:** update mantine monorepo to ^8.2.5 ([#3874](https://github.com/homarr-labs/homarr/issues/3874)) ([7e700b2](https://github.com/homarr-labs/homarr/commit/7e700b2732e2f0abff13d79191addb04097ca9bc))
* **deps:** update nextjs monorepo to v15.4.7 ([#3886](https://github.com/homarr-labs/homarr/issues/3886)) ([683e817](https://github.com/homarr-labs/homarr/commit/683e817493eb1b391751fe36b59d9b95a760324c))
* **deps:** update nextjs monorepo to v15.5.0 ([#3896](https://github.com/homarr-labs/homarr/issues/3896)) ([1451c3c](https://github.com/homarr-labs/homarr/commit/1451c3c41e08945eb31d025d187ff466c5104a05))
* **deps:** update tanstack-query monorepo to ^5.85.4 ([#3889](https://github.com/homarr-labs/homarr/issues/3889)) ([7685438](https://github.com/homarr-labs/homarr/commit/76854387903b48b3af7be466972cf7b9c165acc5))
* **deps:** update tanstack-query monorepo to ^5.85.5 ([#3890](https://github.com/homarr-labs/homarr/issues/3890)) ([fad7207](https://github.com/homarr-labs/homarr/commit/fad7207efe553111334b21739717b9813dea0b56))
* **nextcloud:** integration ignores trusted certificates ([#3893](https://github.com/homarr-labs/homarr/issues/3893)) ([498acdc](https://github.com/homarr-labs/homarr/commit/498acdcdc190de457dc95e5b13f43e1b2c31f624))
## [1.33.0](https://github.com/homarr-labs/homarr/compare/v1.32.0...v1.33.0) (2025-08-15)
### Features
* **tasks:** replace card layout with table interface for better UX ([#3804](https://github.com/homarr-labs/homarr/issues/3804)) ([9d14fcb](https://github.com/homarr-labs/homarr/commit/9d14fcba3646485d73d20455552e56602a346736))
### Bug Fixes
* **dashdot:** disabled widgets cause parse errors ([#3861](https://github.com/homarr-labs/homarr/issues/3861)) ([b7455d1](https://github.com/homarr-labs/homarr/commit/b7455d18edb3f4a76e9aec35f715cc722b13f0c3))
* **deps:** update dependency @ianvs/prettier-plugin-sort-imports to ^4.6.2 ([#3854](https://github.com/homarr-labs/homarr/issues/3854)) ([8a4198d](https://github.com/homarr-labs/homarr/commit/8a4198db7e7fcb1e8556cf92c51888779ac6b614))
* **deps:** update dependency fastify to ^5.5.0 ([#3836](https://github.com/homarr-labs/homarr/issues/3836)) ([ff1e02e](https://github.com/homarr-labs/homarr/commit/ff1e02ec78b31a9cd155828b2f143fbae8e15b88))
* **deps:** update dependency jotai to ^2.13.1 ([#3834](https://github.com/homarr-labs/homarr/issues/3834)) ([5fe5d6f](https://github.com/homarr-labs/homarr/commit/5fe5d6f64d5c5b4096db8d4b2c7aefeab1fcef31))
* **deps:** update dependency tsx to v4.20.4 ([#3845](https://github.com/homarr-labs/homarr/issues/3845)) ([7a4961e](https://github.com/homarr-labs/homarr/commit/7a4961e9ad3e58ab09160f5cb7f0d8968f16d73a))
* **deps:** update dependency typescript-eslint to ^8.39.1 ([#3840](https://github.com/homarr-labs/homarr/issues/3840)) ([433be5e](https://github.com/homarr-labs/homarr/commit/433be5e82cbfb557bc1f69f119505c58f2612d25))
* **deps:** update tanstack-query monorepo to ^5.85.0 ([#3846](https://github.com/homarr-labs/homarr/issues/3846)) ([7d123b8](https://github.com/homarr-labs/homarr/commit/7d123b83e05a43d86d3822155cd9ed9d5cd4b55d))
* **deps:** update tanstack-query monorepo to ^5.85.1 ([#3858](https://github.com/homarr-labs/homarr/issues/3858)) ([70ce3e1](https://github.com/homarr-labs/homarr/commit/70ce3e168b0220486f90ef434ade61047de2ccb6))
* **deps:** update tanstack-query monorepo to ^5.85.2 ([#3859](https://github.com/homarr-labs/homarr/issues/3859)) ([e9cb878](https://github.com/homarr-labs/homarr/commit/e9cb878169b151c6090e2b736f6597be934bf4b5))
* **deps:** update tanstack-query monorepo to ^5.85.3 ([#3860](https://github.com/homarr-labs/homarr/issues/3860)) ([c72c154](https://github.com/homarr-labs/homarr/commit/c72c154305bc40fbaae728f3bffeeee065ab819d))
* **deps:** update turbo monorepo to ^2.5.6 ([#3865](https://github.com/homarr-labs/homarr/issues/3865)) ([98d344c](https://github.com/homarr-labs/homarr/commit/98d344c4b7f88fd0b7c6ce508b30cefd47f98ce5))
* **deps:** upgrade zod to v4 and fix breaking changes ([#3461](https://github.com/homarr-labs/homarr/issues/3461)) ([5c99622](https://github.com/homarr-labs/homarr/commit/5c99622fa86b7b0721384f089862a8405a27a61e))
* **mysql:** increase max media size restricted from 16kb to 4gb ([#3839](https://github.com/homarr-labs/homarr/issues/3839)) ([7ffcd1a](https://github.com/homarr-labs/homarr/commit/7ffcd1a08ede840cf67da2c74a4ba0965645a673))
## [1.32.0](https://github.com/homarr-labs/homarr/compare/v1.31.0...v1.32.0) (2025-08-08)
### Features
+4 -4
View File
@@ -1,18 +1,18 @@
# Security Policy
This policy is relevant if you found potential vulnerabilities in an audit.
We consider something as a vulnerability if it...
1. puts users or user data at risk
2. enables third parties to gain control or access (e.g. [RATs](https://en.wikipedia.org/wiki/Remote_desktop_software#RAT), [privilege escalation](https://en.wikipedia.org/wiki/Privilege_escalation), ...)
3. abuses the system in an unintended way (e.g. crypto mining, proxy, ...)
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| >1.0.0 | :white_check_mark: |
| <1.0.0 | :x: |
We only fix security issues in the [latest stable version](https://github.com/homarr-labs/homarr/releases/latest). Meaning security issues in prior versions will not be fixed and users have to upgrade to the latest version to receive them.
## Reporting a Vulnerability
We use [GitHub's system for reporting vulnerabilities](https://docs.github.com/en/enterprise-cloud@latest/code-security/security-advisories/working-with-repository-security-advisories/creating-a-repository-security-advisory).
Click [**here to report an advisory**](https://github.com/homarr-labs/homarr/security/advisories/new). Our team will get notified and will get back to you within 1-6 business days.
+15 -15
View File
@@ -50,17 +50,17 @@
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "^8.2.4",
"@mantine/core": "^8.2.4",
"@mantine/dropzone": "^8.2.4",
"@mantine/hooks": "^8.2.4",
"@mantine/modals": "^8.2.4",
"@mantine/tiptap": "^8.2.4",
"@mantine/colors-generator": "^8.2.5",
"@mantine/core": "^8.2.5",
"@mantine/dropzone": "^8.2.5",
"@mantine/hooks": "^8.2.5",
"@mantine/modals": "^8.2.5",
"@mantine/tiptap": "^8.2.5",
"@million/lint": "1.0.14",
"@tabler/icons-react": "^3.34.1",
"@tanstack/react-query": "^5.84.2",
"@tanstack/react-query-devtools": "^5.84.2",
"@tanstack/react-query-next-experimental": "^5.84.2",
"@tanstack/react-query": "^5.85.5",
"@tanstack/react-query-devtools": "^5.85.5",
"@tanstack/react-query-next-experimental": "^5.85.5",
"@trpc/client": "^11.4.4",
"@trpc/next": "^11.4.4",
"@trpc/react-query": "^11.4.4",
@@ -74,9 +74,9 @@
"dotenv": "^17.2.1",
"flag-icons": "^7.5.0",
"glob": "^11.0.3",
"jotai": "^2.13.0",
"jotai": "^2.13.1",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.4.6",
"next": "15.5.0",
"postcss-preset-mantine": "^1.18.0",
"prismjs": "^1.30.0",
"react": "19.1.1",
@@ -87,20 +87,20 @@
"superjson": "2.2.2",
"swagger-ui-react": "^5.27.1",
"use-deep-compare-effect": "^1.8.1",
"zod": "^3.25.76"
"zod": "^4.0.17"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1",
"@types/node": "^22.17.1",
"@types/node": "^22.17.2",
"@types/prismjs": "^1.26.5",
"@types/react": "19.1.9",
"@types/react": "19.1.10",
"@types/react-dom": "19.1.7",
"@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.2.0",
"eslint": "^9.32.0",
"eslint": "^9.33.0",
"node-loader": "^2.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2"
@@ -2,7 +2,7 @@
import { useRouter } from "next/navigation";
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 { useZodForm } from "@homarr/form";
@@ -5,7 +5,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Anchor, Button, Card, Code, Collapse, Divider, PasswordInput, Stack, Text, TextInput } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { z } from "zod";
import { z } from "zod/v4";
import { signIn } from "@homarr/auth/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
@@ -2,7 +2,7 @@
import { Button, Card, Stack, TextInput } from "@mantine/core";
import { IconArrowRight } from "@tabler/icons-react";
import type { z } from "zod";
import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
@@ -3,7 +3,7 @@
import { startTransition } from "react";
import { Button, Card, Group, Stack, Switch, Text } from "@mantine/core";
import { IconArrowRight } from "@tabler/icons-react";
import type { z } from "zod";
import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
@@ -1,7 +1,7 @@
"use client";
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 { signIn } from "@homarr/auth/client";
@@ -2,7 +2,7 @@
import { useCallback } from "react";
import { useRouter } from "next/navigation";
import type { z } from "zod";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
@@ -1,8 +1,9 @@
import { Fragment } from "react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
import { IconBox, IconPencil } from "@tabler/icons-react";
import { z } from "zod";
import { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
@@ -98,7 +99,12 @@ const AppCard = async ({ app }: AppCardProps) => {
</Text>
{app.description && (
<Text size="sm" c="gray.6" lineClamp={4}>
{app.description}
{app.description.split("\n").map((line, index) => (
<Fragment key={index}>
{line}
<br />
</Fragment>
))}
</Text>
)}
{app.href && (
@@ -4,7 +4,7 @@ import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
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 { clientApi } from "@homarr/api/client";
@@ -16,7 +16,7 @@ import {
TextInput,
} from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";
import { z } from "zod";
import { z } from "zod/v4";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
@@ -1,6 +1,6 @@
import { notFound } from "next/navigation";
import { Container, Group, Stack, Title } from "@mantine/core";
import { z } from "zod";
import { z } from "zod/v4";
import { auth } from "@homarr/auth/next";
import type { IntegrationKind } from "@homarr/definitions";
@@ -16,7 +16,7 @@ import {
Tooltip,
} from "@mantine/core";
import { IconExternalLink } from "@tabler/icons-react";
import { z } from "zod";
import { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
@@ -4,7 +4,7 @@ import Link from "next/link";
import type { SegmentedControlItem } 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 type { z } from "zod";
import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client";
import { searchEngineTypes } from "@homarr/definitions";
@@ -2,7 +2,7 @@
import { useCallback } from "react";
import { useRouter } from "next/navigation";
import type { z } from "zod";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
@@ -2,7 +2,7 @@
import { useCallback } from "react";
import { useRouter } from "next/navigation";
import type { z } from "zod";
import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
@@ -2,7 +2,7 @@ import Link from "next/link";
import { redirect } from "next/navigation";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
import { IconPencil, IconSearch } from "@tabler/icons-react";
import { z } from "zod";
import { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
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 { Box, Title } from "@mantine/core";
import { Stack, Title } from "@mantine/core";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { getScopedI18n } from "@homarr/translation/server";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { createMetaTitle } from "~/metadata";
import { JobsList } from "./_components/jobs-list";
import { TasksTable } from "./_components/tasks-table";
export async function generateMetadata() {
const session = await auth();
@@ -27,10 +28,15 @@ export default async function TasksPage() {
}
const jobs = await api.cronJobs.getJobs();
const tTasks = await getScopedI18n("management.page.tool.tasks");
return (
<Box>
<Title mb={"md"}>Tasks</Title>
<JobsList initialJobs={jobs} />
</Box>
<>
<DynamicBreadcrumb />
<Stack>
<Title order={1}>{tTasks("title")}</Title>
<TasksTable initialJobs={jobs} />
</Stack>
</>
);
}
@@ -1,7 +1,7 @@
"use client";
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 { clientApi } from "@homarr/api/client";
@@ -1,7 +1,7 @@
"use client";
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 { clientApi } from "@homarr/api/client";
@@ -4,7 +4,7 @@ import { Button, Group, Radio, Stack } from "@mantine/core";
import type { DayOfWeek } from "@mantine/dates";
import dayjs from "dayjs";
import localeData from "dayjs/plugin/localeData";
import type { z } from "zod";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
@@ -1,7 +1,7 @@
"use client";
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 { clientApi } from "@homarr/api/client";
@@ -17,7 +17,7 @@ import {
} from "@mantine/core";
import { useListState } from "@mantine/hooks";
import { IconPlus, IconUserCheck } from "@tabler/icons-react";
import { z } from "zod";
import { z } from "zod/v4";
import { clientApi } from "@homarr/api/client";
import { everyoneGroup, groupPermissions } from "@homarr/definitions";
@@ -1,6 +1,6 @@
import { useCallback, useRef } from "react";
import { Button, Grid, Group, NumberInput, Stack } from "@mantine/core";
import { z } from "zod";
import { z } from "zod/v4";
import { useZodForm } from "@homarr/form";
import type { GridStack } from "@homarr/gridstack";
@@ -1,7 +1,7 @@
"use client";
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 { useZodForm } from "@homarr/form";
@@ -1,5 +1,5 @@
import { Button, Group, Stack, TextInput } from "@mantine/core";
import { z } from "zod";
import { z } from "zod/v4";
import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
@@ -1,5 +1,5 @@
import { useCallback } from "react";
import type { z } from "zod";
import type { z } from "zod/v4";
import { useUpdateBoard } from "@homarr/boards/updater";
import type { dynamicSectionOptionsSchema } from "@homarr/validation/shared";
@@ -1,7 +1,7 @@
"use client";
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 { createModal } from "@homarr/modals";
+6 -6
View File
@@ -37,20 +37,20 @@
"@homarr/widgets": "workspace:^0.1.0",
"dayjs": "^1.11.13",
"dotenv": "^17.2.1",
"fastify": "^5.4.0",
"fastify": "^5.5.0",
"superjson": "2.2.2",
"undici": "7.13.0"
"undici": "7.14.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "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",
"esbuild": "^0.25.8",
"eslint": "^9.32.0",
"esbuild": "^0.25.9",
"eslint": "^9.33.0",
"prettier": "^3.6.2",
"tsx": "4.20.3",
"tsx": "4.20.4",
"typescript": "^5.9.2"
}
}
+3 -3
View File
@@ -26,7 +26,7 @@
"@homarr/redis": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"dotenv": "^17.2.1",
"tsx": "4.20.3",
"tsx": "4.20.4",
"ws": "^8.18.3"
},
"devDependencies": {
@@ -34,8 +34,8 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.1",
"esbuild": "^0.25.8",
"eslint": "^9.32.0",
"esbuild": "^0.25.9",
"eslint": "^9.33.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2"
}
+14 -13
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.32.0",
"version": "1.34.0",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -22,13 +22,14 @@
"lint:ws": "pnpm dlx sherif@latest",
"package:new": "turbo gen init",
"release": "semantic-release",
"scripts:update-bug-report-template": "tsx ./scripts/update-bug-report-template.mts",
"scripts:update-readme-integrations": "tsx ./scripts/update-integration-list.mts",
"start": "concurrently \"pnpm with-env node apps/tasks/tasks.cjs\" \"pnpm with-env node apps/websocket/wssServer.cjs\" \"pnpm -F nextjs start\"",
"test": "cross-env NODE_ENV=development CI=true vitest run --exclude e2e --coverage.enabled ",
"test:e2e": "cross-env NODE_ENV=development CI=true vitest e2e",
"test:ui": "cross-env NODE_ENV=development CI=true vitest --exclude e2e --ui --coverage.enabled",
"typecheck": "turbo typecheck",
"with-env": "dotenv -e .env --",
"scripts:update-readme-integrations": "tsx ./scripts/update-integration-list.mts"
"with-env": "dotenv -e .env --"
},
"prettier": "@homarr/prettier-config",
"devDependencies": {
@@ -36,12 +37,12 @@
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.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/release-notes-generator": "^14.0.3",
"@testcontainers/redis": "^11.5.1",
"@turbo/gen": "^2.5.5",
"@vitejs/plugin-react": "^4.7.0",
"@turbo/gen": "^2.5.6",
"@vitejs/plugin-react": "^5.0.1",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"conventional-changelog-conventionalcommits": "^9.1.0",
@@ -50,12 +51,12 @@
"prettier": "^3.6.2",
"semantic-release": "^24.2.7",
"testcontainers": "^11.5.1",
"turbo": "^2.5.5",
"turbo": "^2.5.6",
"typescript": "^5.9.2",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4"
},
"packageManager": "pnpm@10.14.0",
"packageManager": "pnpm@10.15.0",
"engines": {
"node": ">=22.18.0"
},
@@ -72,16 +73,16 @@
"tree-sitter-json"
],
"overrides": {
"proxmox-api>undici": "7.13.0"
"proxmox-api>undici": "7.14.0"
},
"patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch"
},
"allowUnusedPatches": true,
"ignoredBuiltDependencies": [
"@scarf/scarf",
"core-js-pure",
"protobufjs"
],
"patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch"
}
]
}
}
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.32.0",
"eslint": "^9.33.0",
"typescript": "^5.9.2"
}
}
+6 -5
View File
@@ -24,6 +24,7 @@
"@homarr/auth": "workspace:^0.1.0",
"@homarr/certificates": "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-status": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0",
@@ -41,24 +42,24 @@
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.3.0",
"@tanstack/react-query": "^5.84.2",
"@tanstack/react-query": "^5.85.5",
"@trpc/client": "^11.4.4",
"@trpc/react-query": "^11.4.4",
"@trpc/server": "^11.4.4",
"@trpc/tanstack-react-query": "^11.4.4",
"lodash.clonedeep": "^4.5.0",
"next": "15.4.6",
"next": "15.5.0",
"react": "19.1.1",
"react-dom": "19.1.1",
"superjson": "2.2.2",
"trpc-to-openapi": "^2.4.0",
"zod": "^3.25.76"
"trpc-to-openapi": "^3.0.1",
"zod": "^4.0.17"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.32.0",
"eslint": "^9.33.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2"
}
+2 -5
View File
@@ -1,7 +1,6 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
import { z } from "zod/v4";
import { shouldSkipEnvValidation } from "@homarr/common/env-validation";
import { createEnv } from "@homarr/core/infrastructure/env";
export const env = createEnv({
server: {
@@ -10,6 +9,4 @@ export const env = createEnv({
runtimeEnv: {
KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME,
},
skipValidation: shouldSkipEnvValidation(),
emptyStringAsUndefined: true,
});
+1 -1
View File
@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import type { Session } from "@homarr/auth";
import { hasQueryAccessToIntegrationsAsync } from "@homarr/auth/server";
+1 -1
View File
@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import { and, eq } from "@homarr/db";
import { items } from "@homarr/db/schema";
+1 -1
View File
@@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod/v4";
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
import { createId } from "@homarr/common";
+1 -1
View File
@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import { createId } from "@homarr/common";
import { asc, eq, inArray, like } from "@homarr/db";
+2 -2
View File
@@ -1,6 +1,6 @@
import { TRPCError } from "@trpc/server";
import superjson from "superjson";
import { z } from "zod";
import { z } from "zod/v4";
import { constructBoardPermissions } from "@homarr/auth/shared";
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) =>
z.object({
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);
@@ -1,13 +1,13 @@
import { X509Certificate } from "node:crypto";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { zfd } from "zod-form-data";
import { z } from "zod/v4";
import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server";
import { and, eq } from "@homarr/db";
import { trustedCertificateHostnames } from "@homarr/db/schema";
import { logger } from "@homarr/log";
import { certificateValidFileNameSchema, superRefineCertificateFile } from "@homarr/validation/certificates";
import { certificateValidFileNameSchema, checkCertificateFile } from "@homarr/validation/certificates";
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
@@ -16,7 +16,7 @@ export const certificateRouter = createTRPCRouter({
.requiresPermission("admin")
.input(
zfd.formData({
file: zfd.file().superRefine(superRefineCertificateFile),
file: zfd.file().check(checkCertificateFile),
}),
)
.mutation(async ({ input }) => {
@@ -1,6 +1,6 @@
import { TRPCError } from "@trpc/server";
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 { DockerSingleton } from "@homarr/docker";
+1 -1
View File
@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import { createId } from "@homarr/common";
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 {
+1 -1
View File
@@ -1,4 +1,4 @@
import z from "zod";
import z from "zod/v4";
import packageJson from "../../../../package.json";
import { createTRPCRouter, protectedProcedure } from "../trpc";
@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import { createId, objectEntries } from "@homarr/common";
import { decryptSecret, encryptSecret } from "@homarr/common/server";
+1 -1
View File
@@ -1,6 +1,6 @@
import { randomBytes } from "crypto";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import { createId } from "@homarr/common";
import { asc, eq } from "@homarr/db";
+1 -1
View File
@@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod/v4";
import { fetchWithTimeout } from "@homarr/common";
+1 -1
View File
@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable";
import z from "zod";
import z from "zod/v4";
import { logger } from "@homarr/log";
import { logLevels } from "@homarr/log/constants";
@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import { createId } from "@homarr/common";
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 { onboardingSteps } from "@homarr/definitions";
@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import { createId } from "@homarr/common";
import { asc, eq, like } from "@homarr/db";
@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import { and, eq } from "@homarr/db";
import { sectionCollapseStates, sections } from "@homarr/db/schema";
+1 -1
View File
@@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod/v4";
import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
import type { ServerSettings } from "@homarr/server-settings";
+1 -1
View File
@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
import { createId } from "@homarr/common";
@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import type { Session } from "@homarr/auth";
import type { Modify } from "@homarr/common/types";
+1 -1
View File
@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { z } from "zod/v4";
import { sendPingRequestAsync } from "@homarr/ping";
import { pingChannel, pingUrlChannel } from "@homarr/redis";
+1 -1
View File
@@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { radarrReleaseTypes } from "@homarr/integrations/types";
+1 -1
View File
@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { z } from "zod/v4";
import type { Modify } from "@homarr/common/types";
import type { Integration } from "@homarr/db/schema";
+1 -1
View File
@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { z } from "zod/v4";
import type { Modify } from "@homarr/common/types";
import type { Integration } from "@homarr/db/schema";
@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import type { StreamSession } from "@homarr/integrations";
+1 -1
View File
@@ -1,5 +1,5 @@
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 { minecraftServerStatusRequestHandler } from "@homarr/request-handler/minecraft-server-status";
+1 -1
View File
@@ -1,6 +1,6 @@
import { TRPCError } from "@trpc/server";
import SuperJSON from "superjson";
import { z } from "zod";
import { z } from "zod/v4";
import { eq } from "@homarr/db";
import { boards, items } from "@homarr/db/schema";
+1 -1
View File
@@ -1,5 +1,5 @@
import { escapeForRegEx } from "@tiptap/react";
import { z } from "zod";
import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { releasesRequestHandler } from "@homarr/request-handler/releases";
+1 -1
View File
@@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod/v4";
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
+1 -1
View File
@@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod/v4";
import { fetchStockPriceHandler } from "@homarr/request-handler/stock-price";
+1 -1
View File
@@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod/v4";
import { fetchWithTimeout } from "@homarr/common";
+9 -10
View File
@@ -1,21 +1,20 @@
import { z } from "zod";
import type { AnyZodObject, ZodIntersection, ZodObject } from "zod";
import { z } from "zod/v4";
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,
) {
const { _def } = intersection;
const left = intersection.def.left;
const right = intersection.def.right;
// Merge the shapes
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const mergedShape = { ..._def.left.shape, ..._def.right.shape };
const mergedShape = { ...left.def.shape, ...right.def.shape };
// Return a new ZodObject
return z.object(mergedShape) as unknown as TIntersection extends ZodIntersection<infer TLeft, infer TRight>
? TLeft extends AnyZodObject
? TRight extends AnyZodObject
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
ZodObject<TLeft["shape"] & TRight["shape"], any, any, z.infer<TLeft> & z.infer<TRight>>
? TLeft extends ZodObject
? TRight extends ZodObject
? ZodObject<TLeft["shape"] & TRight["shape"]>
: never
: never
: never;
@@ -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");
});
});
+1 -1
View File
@@ -9,7 +9,7 @@
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import type { OpenApiMeta } from "trpc-to-openapi";
import { ZodError } from "zod";
import { ZodError } from "zod/v4";
import type { Session } from "@homarr/auth";
import { FlattenError } from "@homarr/common";
+2 -2
View File
@@ -1,4 +1,4 @@
import { z } from "zod";
import { z } from "zod/v4";
import { createBooleanSchema, createDurationSchema, createEnv } from "@homarr/core/infrastructure/env";
import { supportedAuthProviders } from "@homarr/definitions";
@@ -19,7 +19,7 @@ const authProvidersSchema = z
return false;
}),
)
.default("credentials");
.default(["credentials"]);
const authProviders = authProvidersSchema.safeParse(process.env.AUTH_PROVIDERS).data ?? [];
+3 -3
View File
@@ -35,11 +35,11 @@
"bcrypt": "^6.0.0",
"cookies": "^0.9.1",
"ldapts": "8.0.9",
"next": "15.4.6",
"next": "15.5.0",
"next-auth": "5.0.0-beta.29",
"react": "19.1.1",
"react-dom": "19.1.1",
"zod": "^3.25.76"
"zod": "^4.0.17"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -47,7 +47,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "6.0.0",
"@types/cookies": "0.9.1",
"eslint": "^9.32.0",
"eslint": "^9.33.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2"
}
@@ -1,5 +1,5 @@
import bcrypt from "bcrypt";
import type { z } from "zod";
import type { z } from "zod/v4";
import type { Database } from "@homarr/db";
import { and, eq } from "@homarr/db";
@@ -1,5 +1,5 @@
import { CredentialsSignin } from "@auth/core/errors";
import { z } from "zod";
import { z } from "zod/v4";
import { createId, extractErrorMessage } from "@homarr/common";
import type { Database, InferInsertModel } from "@homarr/db";
+1 -1
View File
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { z } from "zod";
import { z } from "zod/v4";
import { expireDateAfter, generateSessionToken } from "../session";
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.32.0",
"eslint": "^9.33.0",
"typescript": "^5.9.2"
}
}
+2 -2
View File
@@ -24,13 +24,13 @@
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"undici": "7.13.0"
"undici": "7.14.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.32.0",
"eslint": "^9.33.0",
"typescript": "^5.9.2"
}
}
+2 -2
View File
@@ -34,8 +34,8 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"esbuild": "^0.25.8",
"eslint": "^9.32.0",
"esbuild": "^0.25.9",
"eslint": "^9.33.0",
"typescript": "^5.9.2"
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ export const recreateAdmin = command({
if (!result.success) {
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;
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { randomBytes } from "crypto";
import { z } from "zod";
import { z } from "zod/v4";
import { createEnv } from "@homarr/core/infrastructure/env";
@@ -12,7 +12,7 @@ export const env = createEnv({
server: {
SECRET_ENCRYPTION_KEY: z
.string({
required_error: `SECRET_ENCRYPTION_KEY is required${errorSuffix}`,
error: `SECRET_ENCRYPTION_KEY is required${errorSuffix}`,
})
.min(64, {
message: `SECRET_ENCRYPTION_KEY has to be 64 characters${errorSuffix}`,
+6 -7
View File
@@ -9,8 +9,7 @@
"./types": "./src/types.ts",
"./server": "./src/server.ts",
"./client": "./src/client.ts",
"./env": "./env.ts",
"./env-validation": "./src/env-validation.ts"
"./env": "./env.ts"
},
"typesVersions": {
"*": {
@@ -31,18 +30,18 @@
"@homarr/log": "workspace:^0.1.0",
"@paralleldrive/cuid2": "^2.2.2",
"dayjs": "^1.11.13",
"next": "15.4.6",
"next": "15.5.0",
"react": "19.1.1",
"react-dom": "19.1.1",
"undici": "7.13.0",
"zod": "^3.25.76",
"zod-validation-error": "^3.5.3"
"undici": "7.14.0",
"zod": "^4.0.17",
"zod-validation-error": "^4.0.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.32.0",
"eslint": "^9.33.0",
"typescript": "^5.9.2"
}
}
-42
View File
@@ -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";
@@ -0,0 +1,44 @@
import { FetchError } from "node-fetch";
import { logger } from "@homarr/log";
import { RequestError } from "../request-error";
import type { AnyRequestError } from "../request-error";
import type { ResponseError } from "../response-error";
import { matchErrorCode } from "./fetch-http-error-handler";
import { HttpErrorHandler } from "./http-error-handler";
/**
* node-fetch was a defacto standard to use fetch in nodejs.
*
* It is for example used within the cross-fetch package which is used in tsdav.
*/
export class NodeFetchHttpErrorHandler extends HttpErrorHandler {
constructor(private type = "node-fetch") {
super();
}
handleRequestError(error: unknown): AnyRequestError | undefined {
if (!(error instanceof FetchError)) return undefined;
if (error.code === undefined) return undefined;
logger.debug(`Received ${this.type} request error`, {
code: error.code,
message: error.message,
});
const requestErrorInput = matchErrorCode(error.code);
if (!requestErrorInput) return undefined;
return new RequestError(requestErrorInput, {
cause: error,
});
}
/**
* Response errors do not exist for fetch as it does not throw errors for non successful responses.
*/
handleResponseError(_: unknown): ResponseError | undefined {
return undefined;
}
}
@@ -2,12 +2,12 @@ import { logger } from "@homarr/log";
import type { AnyRequestError } from "../request-error";
import { ResponseError } from "../response-error";
import { FetchHttpErrorHandler } from "./fetch-http-error-handler";
import { HttpErrorHandler } from "./http-error-handler";
import { NodeFetchHttpErrorHandler } from "./node-fetch-http-error-handler";
export class TsdavHttpErrorHandler extends HttpErrorHandler {
handleRequestError(error: unknown): AnyRequestError | undefined {
return new FetchHttpErrorHandler("tsdav").handleRequestError(error);
return new NodeFetchHttpErrorHandler("tsdav").handleRequestError(error);
}
handleResponseError(error: unknown): ResponseError | undefined {
@@ -16,7 +16,7 @@ export class TsdavHttpErrorHandler extends HttpErrorHandler {
// https://github.com/natelindev/tsdav/blob/bf33f04b1884694d685ee6f2b43fe9354b12d167/src/account.ts#L86
if (error.message !== "Invalid credentials") return undefined;
logger.debug("Received Tsdav response error", {
logger.debug("Received tsdav response error", {
status: 401,
});
Loaded 100 of 263 files, more files were not shown because too many files have changed in this diff. Show more