Compare commits

..
243 changed files with 4598 additions and 14244 deletions

No files matched your search

-3
View File
@@ -38,6 +38,3 @@ TURBO_TELEMETRY_DISABLED=1
# Enable kubernetes tool # Enable kubernetes tool
# ENABLE_KUBERNETES=true # ENABLE_KUBERNETES=true
# Enable mock integration
UNSAFE_ENABLE_MOCK_INTEGRATION=true
-6
View File
@@ -31,12 +31,6 @@ body:
label: Version label: Version
description: What version of Homarr are you running? description: What version of Homarr are you running?
options: options:
- 1.26.0
- 1.25.0
- 1.24.0
- 1.23.0
- 1.22.0
- 1.21.0
- 1.20.0 - 1.20.0
- 1.19.1 - 1.19.1
- 1.19.0 - 1.19.0
+4 -4
View File
@@ -6,8 +6,8 @@
**Thank you for your contribution. Please ensure that your pull request meets the following pull request:** **Thank you for your contribution. Please ensure that your pull request meets the following pull request:**
- [ ] Builds without warnings or errors (`pnpm build`, autofix with `pnpm format:fix`) - [ ] Builds without warnings or errors (``pnpm build``, autofix with ``pnpm format:fix``)
- [ ] Pull request targets `dev` branch - [ ] Pull request targets ``dev`` branch
- [ ] Commits follow the [conventional commits guideline](https://www.conventionalcommits.org/en/v1.0.0/) - [ ] Commits follow the [conventional commits guideline](https://www.conventionalcommits.org/en/v1.0.0/)
- [ ] No shorthand variable names are used (eg. `x`, `y`, `i` or any abbrevation) - [ ] No shorthand variable names are used (eg. ``x``, ``y``, ``i`` or any abbrevation)
- [ ] Documentation is up to date. Create a pull request [here](https://github.com/homarr-labs/documentation/).
@@ -0,0 +1,14 @@
# https://github.com/webiny/action-conventional-commits?tab=readme-ov-file
name: "[Conventions] Semantic Commits"
on:
pull_request:
branches: [ dev ]
jobs:
build:
name: Conventional Commits
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: webiny/action-conventional-commits@v1.3.0
+1 -1
View File
@@ -1 +1 @@
22.17.0 22.16.0
-249
View File
@@ -1,252 +1,3 @@
## [1.27.0](https://github.com/homarr-labs/homarr/compare/v1.26.0...v1.27.0) (2025-07-04)
### Features
* **integrations:** add mock integration ([#3505](https://github.com/homarr-labs/homarr/issues/3505)) ([58d5b14](https://github.com/homarr-labs/homarr/commit/58d5b14c516583e79f9079899dfe6544acdc58f7))
* **tasks:** allow management of job intervals and disabling them ([#3408](https://github.com/homarr-labs/homarr/issues/3408)) ([9398dd9](https://github.com/homarr-labs/homarr/commit/9398dd983cce00959af2a4885e92da6acfe2f57e))
### Bug Fixes
* broken lockfile ([308d235](https://github.com/homarr-labs/homarr/commit/308d235fa9db658fb7c3f5a183f01c4f48f0546f))
* **deps:** update dependency better-sqlite3 to ^12.2.0 ([#3503](https://github.com/homarr-labs/homarr/issues/3503)) ([c22a963](https://github.com/homarr-labs/homarr/commit/c22a963342c5613089b0a1a4034379cc80425602))
* **deps:** update dependency dotenv to ^16.6.1 ([#3494](https://github.com/homarr-labs/homarr/issues/3494)) ([b880de8](https://github.com/homarr-labs/homarr/commit/b880de8a794a879a0c2b38cc46cb5412eedbb44e))
* **deps:** update dependency dotenv to v17 ([#3499](https://github.com/homarr-labs/homarr/issues/3499)) ([fe87d97](https://github.com/homarr-labs/homarr/commit/fe87d978c4335a700e2859c648bf997a7c323a31))
* **deps:** update dependency ldapts to v8.0.4 ([#3509](https://github.com/homarr-labs/homarr/issues/3509)) ([8d53801](https://github.com/homarr-labs/homarr/commit/8d53801f67b5458b5eb60b4ebcd6a923a60b7e5f))
* **deps:** update dependency next-intl to v4.3.2 ([#3520](https://github.com/homarr-labs/homarr/issues/3520)) ([6fda939](https://github.com/homarr-labs/homarr/commit/6fda939a2d94120998eb1365134652eb00602937))
* **deps:** update dependency next-intl to v4.3.3 ([#3521](https://github.com/homarr-labs/homarr/issues/3521)) ([ab9165f](https://github.com/homarr-labs/homarr/commit/ab9165f2cd885069c7389d62db187c3be240fd72))
* **deps:** update dependency next-intl to v4.3.4 ([#3528](https://github.com/homarr-labs/homarr/issues/3528)) ([6005784](https://github.com/homarr-labs/homarr/commit/60057842842d393b8d9c5404d6c2ae85bc82b27c))
* **deps:** update dependency node-cron to ^4.2.0 ([#3518](https://github.com/homarr-labs/homarr/issues/3518)) ([0f94ac4](https://github.com/homarr-labs/homarr/commit/0f94ac456eeacbc03183d635ea4c57b224cf8537))
* **deps:** update dependency postcss-preset-mantine to ^1.18.0 ([#3510](https://github.com/homarr-labs/homarr/issues/3510)) ([01d0ffb](https://github.com/homarr-labs/homarr/commit/01d0ffb0d143a77c94c977873ad04f661c280eb7))
* **deps:** update dependency swagger-ui-react to ^5.25.4 ([#3523](https://github.com/homarr-labs/homarr/issues/3523)) ([6c94d1a](https://github.com/homarr-labs/homarr/commit/6c94d1a10c495850958b6399a302eab582e9d40b))
* **deps:** update dependency swagger-ui-react to ^5.26.0 ([#3527](https://github.com/homarr-labs/homarr/issues/3527)) ([9da66f5](https://github.com/homarr-labs/homarr/commit/9da66f5b1db88c196dbb6866f920f1a176305667))
* **deps:** update dependency trpc-to-openapi to ^2.3.2 ([#3502](https://github.com/homarr-labs/homarr/issues/3502)) ([2ebc9f9](https://github.com/homarr-labs/homarr/commit/2ebc9f969b5a0c753622421cb5f0a81873ad4ae9))
* **deps:** update dependency typescript-eslint to ^8.35.1 ([#3513](https://github.com/homarr-labs/homarr/issues/3513)) ([17fe293](https://github.com/homarr-labs/homarr/commit/17fe293a7c9e6dbc83bc8f585a54a8fdd8640973))
* **deps:** update dependency ws to ^8.18.3 ([#3501](https://github.com/homarr-labs/homarr/issues/3501)) ([ed077d6](https://github.com/homarr-labs/homarr/commit/ed077d63db9a197510e8e139d40813a3854f39e9))
* **deps:** update dependency zod to ^3.25.68 ([#3529](https://github.com/homarr-labs/homarr/issues/3529)) ([b375ec9](https://github.com/homarr-labs/homarr/commit/b375ec977e757bed14b6fb6a04a5b30fe481eaf9))
* **deps:** update dependency zod to ^3.25.69 ([#3531](https://github.com/homarr-labs/homarr/issues/3531)) ([915eec1](https://github.com/homarr-labs/homarr/commit/915eec1b5e6d7383d36f2e67d86472c6a2932a9b))
* **deps:** update dependency zod to ^3.25.71 ([#3534](https://github.com/homarr-labs/homarr/issues/3534)) ([95c8aad](https://github.com/homarr-labs/homarr/commit/95c8aadb0c98f116ff3e256c770636267da82fdc))
* **deps:** update dependency zod to ^3.25.72 ([#3535](https://github.com/homarr-labs/homarr/issues/3535)) ([0858a42](https://github.com/homarr-labs/homarr/commit/0858a42b8137f003fb7500f0dbccbb9413a6c470))
* **deps:** update dependency zod to ^3.25.73 ([#3543](https://github.com/homarr-labs/homarr/issues/3543)) ([7061a03](https://github.com/homarr-labs/homarr/commit/7061a03b2ef90c51754a4e39b22742c34e5ebc80))
* **deps:** update dependency zod to ^3.25.74 ([#3547](https://github.com/homarr-labs/homarr/issues/3547)) ([b057059](https://github.com/homarr-labs/homarr/commit/b057059ff1fb8fcd29893000d30ddcba09f2b775))
* **deps:** update nextjs monorepo to v15.3.5 ([#3541](https://github.com/homarr-labs/homarr/issues/3541)) ([d5181d0](https://github.com/homarr-labs/homarr/commit/d5181d0c0e1f25a119a9e91a9544a8f76e0b0901))
* **deps:** update tanstack-query monorepo to ^5.81.5 ([#3495](https://github.com/homarr-labs/homarr/issues/3495)) ([5346b96](https://github.com/homarr-labs/homarr/commit/5346b9698444a503e432ee4b0679e408e2c44d96))
* **deps:** update tiptap monorepo to v2.23.1 ([#3516](https://github.com/homarr-labs/homarr/issues/3516)) ([0a0e7f9](https://github.com/homarr-labs/homarr/commit/0a0e7f9602429003d45b99fade5f1ad121aa67f3))
* **deps:** update tiptap monorepo to v2.24.0 ([#3532](https://github.com/homarr-labs/homarr/issues/3532)) ([0e1f693](https://github.com/homarr-labs/homarr/commit/0e1f6934f9755503ce289b71549b6737171e1c57))
* **deps:** update tiptap monorepo to v2.24.1 ([#3536](https://github.com/homarr-labs/homarr/issues/3536)) ([63e2ace](https://github.com/homarr-labs/homarr/commit/63e2acefbdf09d4aa7e5a9522cfb4b0c89fb66e9))
* **deps:** update tiptap monorepo to v2.24.2 ([#3544](https://github.com/homarr-labs/homarr/issues/3544)) ([66f663a](https://github.com/homarr-labs/homarr/commit/66f663aad4b35d0cbfb1ffd60545fd0272b70990))
## [1.26.0](https://github.com/homarr-labs/homarr/compare/v1.25.0...v1.26.0) (2025-06-27)
### Features
* add ntfy integration ([#2900](https://github.com/homarr-labs/homarr/issues/2900)) ([e110a84](https://github.com/homarr-labs/homarr/commit/e110a84fdd5fa2468786e1505043d8a7dc5cfa1b))
### Bug Fixes
* **deps:** update dependency @auth/core to ^0.40.0 ([#3448](https://github.com/homarr-labs/homarr/issues/3448)) ([2c7f5de](https://github.com/homarr-labs/homarr/commit/2c7f5de19271f7c65a9979b37d9602bb84c68564))
* **deps:** update dependency @auth/drizzle-adapter to ^1.10.0 ([#3449](https://github.com/homarr-labs/homarr/issues/3449)) ([7b8dc16](https://github.com/homarr-labs/homarr/commit/7b8dc1670d0c8473524f71e1ee872e6ea7d2e0ce))
* **deps:** update dependency @tiptap/extension-highlight to v2.22.1 ([#3433](https://github.com/homarr-labs/homarr/issues/3433)) ([f6039a5](https://github.com/homarr-labs/homarr/commit/f6039a5976da70388cb32eecfbf57ac02e6fc050))
* **deps:** update dependency better-sqlite3 to ^12.1.0 ([#3462](https://github.com/homarr-labs/homarr/issues/3462)) ([fa421b3](https://github.com/homarr-labs/homarr/commit/fa421b3778c00b30b68ceadfb370803eba1a72f2))
* **deps:** update dependency better-sqlite3 to ^12.1.1 ([#3470](https://github.com/homarr-labs/homarr/issues/3470)) ([eadec21](https://github.com/homarr-labs/homarr/commit/eadec212369b8ee81eec00b3ecbd40d1c90972d7))
* **deps:** update dependency better-sqlite3 to v12 ([#3441](https://github.com/homarr-labs/homarr/issues/3441)) ([95be039](https://github.com/homarr-labs/homarr/commit/95be0391a651b59a2611ca5ffa7825c6b337a996))
* **deps:** update dependency dotenv to ^16.6.0 ([#3484](https://github.com/homarr-labs/homarr/issues/3484)) ([7477d02](https://github.com/homarr-labs/homarr/commit/7477d02cadd88608f33edf0836f7d7e164800dde))
* **deps:** update dependency drizzle-kit to ^0.31.2 ([#3455](https://github.com/homarr-labs/homarr/issues/3455)) ([8ffbbd6](https://github.com/homarr-labs/homarr/commit/8ffbbd6bf9bb1c329943ae5033029eed115b43dd))
* **deps:** update dependency drizzle-kit to ^0.31.3 ([#3482](https://github.com/homarr-labs/homarr/issues/3482)) ([7d0ac46](https://github.com/homarr-labs/homarr/commit/7d0ac46753b1cf6bee1f53297b0011e4e86fdffd))
* **deps:** update dependency drizzle-kit to ^0.31.4 ([#3490](https://github.com/homarr-labs/homarr/issues/3490)) ([670de0f](https://github.com/homarr-labs/homarr/commit/670de0f5fbb9faf950fdb38e4f4cfb57f21257c4))
* **deps:** update dependency eslint-plugin-import to ^2.32.0 ([#3430](https://github.com/homarr-labs/homarr/issues/3430)) ([a7052bb](https://github.com/homarr-labs/homarr/commit/a7052bb16f52ccb47d2c8ac205fdf01cf8e8c01e))
* **deps:** update dependency ldapts to v8.0.2 ([#3481](https://github.com/homarr-labs/homarr/issues/3481)) ([114de40](https://github.com/homarr-labs/homarr/commit/114de40634b501ecea7be8d9ae1d2a19686845c1))
* **deps:** update dependency maria2 to ^0.4.1 ([#3456](https://github.com/homarr-labs/homarr/issues/3456)) ([4e892ef](https://github.com/homarr-labs/homarr/commit/4e892efab7f456b93e993c2c3cb6068f06ea1e4f))
* **deps:** update dependency next-auth to v5.0.0-beta.29 ([#3447](https://github.com/homarr-labs/homarr/issues/3447)) ([da2d22b](https://github.com/homarr-labs/homarr/commit/da2d22b1b9b87fe7aa1cbac76909af2d2ef57d3f))
* **deps:** update dependency next-intl to v4.2.0 ([#3457](https://github.com/homarr-labs/homarr/issues/3457)) ([86d4585](https://github.com/homarr-labs/homarr/commit/86d4585035c5604a219fd0be404104f62a0be0b2))
* **deps:** update dependency next-intl to v4.3.0 ([#3458](https://github.com/homarr-labs/homarr/issues/3458)) ([ded3993](https://github.com/homarr-labs/homarr/commit/ded39930ca1d8c4868f9541c7fad18a234ceeb3f))
* **deps:** update dependency next-intl to v4.3.1 ([#3465](https://github.com/homarr-labs/homarr/issues/3465)) ([5d96744](https://github.com/homarr-labs/homarr/commit/5d9674412e6e99af1fb1fce7143ce4b6717ff29b))
* **deps:** update dependency prettier to ^3.6.0 ([#3452](https://github.com/homarr-labs/homarr/issues/3452)) ([07ce1c1](https://github.com/homarr-labs/homarr/commit/07ce1c1a2d197f981818a9b1e567102f0772271d))
* **deps:** update dependency prettier to ^3.6.1 ([#3469](https://github.com/homarr-labs/homarr/issues/3469)) ([34faa9e](https://github.com/homarr-labs/homarr/commit/34faa9e58ce061c4c99d207cfc6c56cbb7045c03))
* **deps:** update dependency prettier to ^3.6.2 ([#3489](https://github.com/homarr-labs/homarr/issues/3489)) ([60786bf](https://github.com/homarr-labs/homarr/commit/60786bf9ac81ce13b6304f5284fb6049e2d7d7dc))
* **deps:** update dependency recharts to ^2.15.4 ([#3429](https://github.com/homarr-labs/homarr/issues/3429)) ([caedac4](https://github.com/homarr-labs/homarr/commit/caedac490ed3aaf8f6a815d799ba9fafe9de18a2))
* **deps:** update dependency swagger-ui-react to ^5.25.3 ([#3479](https://github.com/homarr-labs/homarr/issues/3479)) ([1cd9fa8](https://github.com/homarr-labs/homarr/commit/1cd9fa8e82eef1f69be06855d076a364883914df))
* **deps:** update dependency typescript-eslint to ^8.35.0 ([#3460](https://github.com/homarr-labs/homarr/issues/3460)) ([1813d44](https://github.com/homarr-labs/homarr/commit/1813d4435e08f6a800ee76561e66e6371b30ec80))
* **deps:** update dependency undici to v7.11.0 ([#3486](https://github.com/homarr-labs/homarr/issues/3486)) ([01fdd79](https://github.com/homarr-labs/homarr/commit/01fdd7955906c661a620b8fac148693dc4632ebd))
* **deps:** update mantine monorepo to ^8.1.2 ([#3471](https://github.com/homarr-labs/homarr/issues/3471)) ([5e6dfb3](https://github.com/homarr-labs/homarr/commit/5e6dfb3a5c2d456fa20fbcd07f98c01d0ab7c4b7))
* **deps:** update tanstack-query monorepo to ^5.80.12 ([#3436](https://github.com/homarr-labs/homarr/issues/3436)) ([86608f5](https://github.com/homarr-labs/homarr/commit/86608f5a9ffe9ea2c57a9191175994a24eea5dc7))
* **deps:** update tanstack-query monorepo to ^5.81.0 ([#3437](https://github.com/homarr-labs/homarr/issues/3437)) ([e6f05f9](https://github.com/homarr-labs/homarr/commit/e6f05f9d94912ef5f38ad4ab222e20d433c627eb))
* **deps:** update tanstack-query monorepo to ^5.81.1 ([#3440](https://github.com/homarr-labs/homarr/issues/3440)) ([b8b71da](https://github.com/homarr-labs/homarr/commit/b8b71da291541b714e053ea1bda4e49053261936))
* **deps:** update tanstack-query monorepo to ^5.81.2 ([#3442](https://github.com/homarr-labs/homarr/issues/3442)) ([3f0e9c3](https://github.com/homarr-labs/homarr/commit/3f0e9c37385ae23370618f1a11870642a9b3d030))
* **deps:** update tanstack-query monorepo to ^5.81.4 ([#3493](https://github.com/homarr-labs/homarr/issues/3493)) ([a737331](https://github.com/homarr-labs/homarr/commit/a737331d207abfd4ab8a8869c70257b5f5d50d5a))
* **deps:** update tiptap monorepo to v2.22.1 ([#3432](https://github.com/homarr-labs/homarr/issues/3432)) ([8ccbe7f](https://github.com/homarr-labs/homarr/commit/8ccbe7ff52eff742031c528bcc1e15916ba1fe81))
* **deps:** update tiptap monorepo to v2.22.2 ([#3438](https://github.com/homarr-labs/homarr/issues/3438)) ([f677bd6](https://github.com/homarr-labs/homarr/commit/f677bd6b12630130d8cf95a4f2462e4bab1226db))
* **deps:** update tiptap monorepo to v2.22.3 ([#3443](https://github.com/homarr-labs/homarr/issues/3443)) ([add73a1](https://github.com/homarr-labs/homarr/commit/add73a1a3c19e0efd2d417463e4f4d72a9fd1c02))
* **deps:** update tiptap monorepo to v2.23.0 ([#3487](https://github.com/homarr-labs/homarr/issues/3487)) ([b027f3a](https://github.com/homarr-labs/homarr/commit/b027f3a18084743b452dd35ff8e0fa30f59acd85))
* **deps:** update trpc monorepo to ^11.4.3 ([#3491](https://github.com/homarr-labs/homarr/issues/3491)) ([d326f3b](https://github.com/homarr-labs/homarr/commit/d326f3b4b3f5fc342404a12b25380d9dd6cd924a))
* **jellyseerr:** add missing deleted media status ([#3439](https://github.com/homarr-labs/homarr/issues/3439)) ([6b474c3](https://github.com/homarr-labs/homarr/commit/6b474c3df470450598b6b9472bf9f52740b8ed21))
## [1.25.0](https://github.com/homarr-labs/homarr/compare/v1.24.0...v1.25.0) (2025-06-20)
### Features
* **integrations:** add support for self-signed-cert-in-chain request error ([#3399](https://github.com/homarr-labs/homarr/issues/3399)) ([e6cb35b](https://github.com/homarr-labs/homarr/commit/e6cb35b7c2dc45349a9cf0819aa21228aea03e26))
### Bug Fixes
* **deps:** update dependency node-cron to ^4.1.1 ([#3427](https://github.com/homarr-labs/homarr/issues/3427)) ([f6a99ff](https://github.com/homarr-labs/homarr/commit/f6a99ffedd690c01e18f445ade6343f1ca476b27))
* **deps:** update dependency swagger-ui-react to ^5.24.2 ([#3405](https://github.com/homarr-labs/homarr/issues/3405)) ([bdca920](https://github.com/homarr-labs/homarr/commit/bdca92036a3dde7f655735c305e55c5c15b63440))
* **deps:** update dependency swagger-ui-react to ^5.25.2 ([#3420](https://github.com/homarr-labs/homarr/issues/3420)) ([02cd7a5](https://github.com/homarr-labs/homarr/commit/02cd7a50dad9e954cfd569b0f1efcc4fe4fd5da4))
* **deps:** update dependency tsdav to ^2.1.5 ([#3414](https://github.com/homarr-labs/homarr/issues/3414)) ([6bcfe78](https://github.com/homarr-labs/homarr/commit/6bcfe789561c518e7ce6c78a9be95412a50f04dd))
* **deps:** update dependency typescript-eslint to ^8.34.1 ([#3409](https://github.com/homarr-labs/homarr/issues/3409)) ([637bfb8](https://github.com/homarr-labs/homarr/commit/637bfb8b47b7dc6c70163bd481c47bca9bd93d55))
* **deps:** update dependency zod to ^3.25.65 ([#3410](https://github.com/homarr-labs/homarr/issues/3410)) ([a8c9c07](https://github.com/homarr-labs/homarr/commit/a8c9c075f08bca4b86307e98156cae7167ef6ab1))
* **deps:** update dependency zod to ^3.25.67 ([#3413](https://github.com/homarr-labs/homarr/issues/3413)) ([56ca8b7](https://github.com/homarr-labs/homarr/commit/56ca8b7b0b1c0187457c085af1c7f4504d72ed9a))
* **deps:** update dependency zod-validation-error to ^3.5.1 ([#3406](https://github.com/homarr-labs/homarr/issues/3406)) ([adbef25](https://github.com/homarr-labs/homarr/commit/adbef252c61c29fbbc8c7b0e325a0e9d69bb3191))
* **deps:** update dependency zod-validation-error to ^3.5.2 ([#3417](https://github.com/homarr-labs/homarr/issues/3417)) ([430f671](https://github.com/homarr-labs/homarr/commit/430f6714345b5b90859fd3302f76fd3d14956148))
* **deps:** update mantine monorepo to ^8.1.1 ([#3404](https://github.com/homarr-labs/homarr/issues/3404)) ([f97a261](https://github.com/homarr-labs/homarr/commit/f97a2619734dcf3b326c69587fa2f4e170cb9c88))
* **deps:** update nextjs monorepo to v15.3.4 ([#3421](https://github.com/homarr-labs/homarr/issues/3421)) ([3759a98](https://github.com/homarr-labs/homarr/commit/3759a9831c6fe54b0b0eb0e101c8eb912b752b41))
* **deps:** update tanstack-query monorepo to ^5.80.10 ([#3422](https://github.com/homarr-labs/homarr/issues/3422)) ([7c58e8a](https://github.com/homarr-labs/homarr/commit/7c58e8ac9bdb2868637648b43b7965189421b295))
* **deps:** update tiptap monorepo to v2.14.1 ([#3423](https://github.com/homarr-labs/homarr/issues/3423)) ([9e4b463](https://github.com/homarr-labs/homarr/commit/9e4b4635bc582e4d92821844d58fdb3d057f7fec))
* **deps:** update tiptap monorepo to v2.22.0 ([#3425](https://github.com/homarr-labs/homarr/issues/3425)) ([ec4dde6](https://github.com/homarr-labs/homarr/commit/ec4dde64fdd4302b091a0255ec1bcdc6b2caca89))
* **deps:** update trpc monorepo to ^11.4.2 ([#3419](https://github.com/homarr-labs/homarr/issues/3419)) ([8e428e2](https://github.com/homarr-labs/homarr/commit/8e428e20a764c54c15fc44b57f85fa684cfbc00e))
* **downloads:** not updating automatically ([#3412](https://github.com/homarr-labs/homarr/issues/3412)) ([b5ef7e5](https://github.com/homarr-labs/homarr/commit/b5ef7e5194a828abfddcac5eb492910de5f1f7d8))
* **widget-integration-select:** fixed error about `<p>` being descendant of `<p>` ([#3418](https://github.com/homarr-labs/homarr/issues/3418)) ([73fff02](https://github.com/homarr-labs/homarr/commit/73fff02ad0a3a710aa39cda7dccf9206569c2879))
## [1.24.0](https://github.com/homarr-labs/homarr/compare/v1.23.0...v1.24.0) (2025-06-13)
### Features
* reduce cron job intervals ([#3235](https://github.com/homarr-labs/homarr/issues/3235)) ([f67702e](https://github.com/homarr-labs/homarr/commit/f67702e1d32954b4f6db28c02b1a25e56122d779))
### Bug Fixes
* csp prevents external videos ([#3372](https://github.com/homarr-labs/homarr/issues/3372)) ([c59cded](https://github.com/homarr-labs/homarr/commit/c59cdeda47a504069c09634dafe36a9bc80c922f))
* **deps:** update dependency @t3-oss/env-nextjs to ^0.13.7 ([#3357](https://github.com/homarr-labs/homarr/issues/3357)) ([22f0cd8](https://github.com/homarr-labs/homarr/commit/22f0cd885e6b3f0cd15daa60840554a545eb2532))
* **deps:** update dependency @t3-oss/env-nextjs to ^0.13.8 ([#3364](https://github.com/homarr-labs/homarr/issues/3364)) ([ab3ee3b](https://github.com/homarr-labs/homarr/commit/ab3ee3bac186913cbc76c1381c90b3c90a447185))
* **deps:** update dependency fast-xml-parser to ^5.2.5 ([#3353](https://github.com/homarr-labs/homarr/issues/3353)) ([9e86de6](https://github.com/homarr-labs/homarr/commit/9e86de6e1781ef2c088f83af1ba341baecb496da))
* **deps:** update dependency glob to ^11.0.3 ([#3391](https://github.com/homarr-labs/homarr/issues/3391)) ([f16efac](https://github.com/homarr-labs/homarr/commit/f16efac05a17b77303f4c41b8910d491d237add2))
* **deps:** update dependency sass to ^1.89.2 ([#3362](https://github.com/homarr-labs/homarr/issues/3362)) ([b1a559d](https://github.com/homarr-labs/homarr/commit/b1a559d80161f59114ba3098005c3334662987eb))
* **deps:** update dependency swagger-ui-react to ^5.24.1 ([#3368](https://github.com/homarr-labs/homarr/issues/3368)) ([fa52b55](https://github.com/homarr-labs/homarr/commit/fa52b5582880075cd76826641547ccbf42b56f7a))
* **deps:** update dependency tsx to v4.20.0 ([#3375](https://github.com/homarr-labs/homarr/issues/3375)) ([3d07306](https://github.com/homarr-labs/homarr/commit/3d07306e7d3bf90db7f7c02d68550a137c5554bc))
* **deps:** update dependency tsx to v4.20.1 ([#3380](https://github.com/homarr-labs/homarr/issues/3380)) ([e7652ea](https://github.com/homarr-labs/homarr/commit/e7652ea4eb1ddaf8373c02abfcdb5440bf55c236))
* **deps:** update dependency tsx to v4.20.2 ([#3387](https://github.com/homarr-labs/homarr/issues/3387)) ([79519ec](https://github.com/homarr-labs/homarr/commit/79519ec0ebf4ff101fa1cd395f974e023d1af3c3))
* **deps:** update dependency tsx to v4.20.3 ([#3393](https://github.com/homarr-labs/homarr/issues/3393)) ([c9097a9](https://github.com/homarr-labs/homarr/commit/c9097a95e2072ed619e93301f8d4644aaadce2dd))
* **deps:** update dependency typescript-eslint to ^8.34.0 ([#3359](https://github.com/homarr-labs/homarr/issues/3359)) ([f6da091](https://github.com/homarr-labs/homarr/commit/f6da0915773dd60b40475f258d90e5f3dbdb6729))
* **deps:** update dependency zod to ^3.25.56 ([#3340](https://github.com/homarr-labs/homarr/issues/3340)) ([aafd883](https://github.com/homarr-labs/homarr/commit/aafd883c3250ddf3438877d82b4b63a6a15eadda))
* **deps:** update dependency zod to ^3.25.57 ([#3367](https://github.com/homarr-labs/homarr/issues/3367)) ([e7a9797](https://github.com/homarr-labs/homarr/commit/e7a9797484e9e2cd595d31e6db4cdf63e25e3b82))
* **deps:** update dependency zod to ^3.25.58 ([#3373](https://github.com/homarr-labs/homarr/issues/3373)) ([f37fc0e](https://github.com/homarr-labs/homarr/commit/f37fc0edfee45386b3469bac11c91e4f17c83673))
* **deps:** update dependency zod to ^3.25.60 ([#3378](https://github.com/homarr-labs/homarr/issues/3378)) ([7388c90](https://github.com/homarr-labs/homarr/commit/7388c907a57c7999a48118d96a3f34d2c3656f79))
* **deps:** update dependency zod to ^3.25.61 ([#3379](https://github.com/homarr-labs/homarr/issues/3379)) ([863f935](https://github.com/homarr-labs/homarr/commit/863f935ef4c27f87a632a5e0b376fe088f4e8a04))
* **deps:** update dependency zod to ^3.25.62 ([#3382](https://github.com/homarr-labs/homarr/issues/3382)) ([0b200dc](https://github.com/homarr-labs/homarr/commit/0b200dc9e2239060ddf2670f8508f68e624e821e))
* **deps:** update dependency zod to ^3.25.63 ([#3386](https://github.com/homarr-labs/homarr/issues/3386)) ([153c2ef](https://github.com/homarr-labs/homarr/commit/153c2ef8a0098cbe2d998c98a25a0c5880f0da5d))
* **deps:** update dependency zod to ^3.25.64 ([#3392](https://github.com/homarr-labs/homarr/issues/3392)) ([5dc0ee8](https://github.com/homarr-labs/homarr/commit/5dc0ee8842dbd4bde468c5762cf5dc0d73958242))
* **deps:** update dependency zod-validation-error to ^3.5.0 ([#3388](https://github.com/homarr-labs/homarr/issues/3388)) ([747d034](https://github.com/homarr-labs/homarr/commit/747d034114bd2a3b381aa7318c59015b21811353))
* **deps:** update mantine monorepo to ^8.1.0 ([#3360](https://github.com/homarr-labs/homarr/issues/3360)) ([d862ae0](https://github.com/homarr-labs/homarr/commit/d862ae03e319d173468cd7797bd74be022cbc966))
* **deps:** update tanstack-query monorepo to ^5.80.7 ([#3385](https://github.com/homarr-labs/homarr/issues/3385)) ([c5cc8f1](https://github.com/homarr-labs/homarr/commit/c5cc8f1d375d5a582b169da706a5b433a0181c20))
* **deps:** update testcontainers-node monorepo to ^11.0.1 ([#3343](https://github.com/homarr-labs/homarr/issues/3343)) ([6ec66b1](https://github.com/homarr-labs/homarr/commit/6ec66b1fe2d5779b2267532d75d6933b3460ffd5))
* **deps:** update testcontainers-node monorepo to ^11.0.2 ([#3352](https://github.com/homarr-labs/homarr/issues/3352)) ([98c4cd1](https://github.com/homarr-labs/homarr/commit/98c4cd1e39286164c09f7d8e961488e26fa2c562))
* **deps:** update testcontainers-node monorepo to ^11.0.3 ([#3369](https://github.com/homarr-labs/homarr/issues/3369)) ([ad444e8](https://github.com/homarr-labs/homarr/commit/ad444e8fa7a186710deafd98f2aa48717de1ec80))
* **deps:** update trpc monorepo to ^11.4.0 ([#3389](https://github.com/homarr-labs/homarr/issues/3389)) ([3bf4f9e](https://github.com/homarr-labs/homarr/commit/3bf4f9e0588440d74dfaf7016acc66c1ed40843a))
* **deps:** update trpc monorepo to ^11.4.1 ([#3395](https://github.com/homarr-labs/homarr/issues/3395)) ([6d9992d](https://github.com/homarr-labs/homarr/commit/6d9992d7114f97c24e02c40dcaf11fad90608a1c))
* module not found sshcrypto.node during development ([#3349](https://github.com/homarr-labs/homarr/issues/3349)) ([1b3770a](https://github.com/homarr-labs/homarr/commit/1b3770aafbe4e903e6235dc681edac556a36e4e0))
* **pi-hole:** cached session not invalidated when changing password ([#3351](https://github.com/homarr-labs/homarr/issues/3351)) ([a9e68e1](https://github.com/homarr-labs/homarr/commit/a9e68e13d53c8e06d150ecba085680cd248d4f64))
## [1.23.0](https://github.com/homarr-labs/homarr/compare/v1.22.0...v1.23.0) (2025-06-06)
### Features
* **releases-widget:** add `import from docker` functionality ([#3130](https://github.com/homarr-labs/homarr/issues/3130)) ([7071d76](https://github.com/homarr-labs/homarr/commit/7071d76c526f1b59a955abcc595894975d574218))
### Bug Fixes
* Allow nullable on cpuModelName and default to "Unknown CPU" if null in OMV Integration ([#3310](https://github.com/homarr-labs/homarr/issues/3310)) ([3e7bf62](https://github.com/homarr-labs/homarr/commit/3e7bf62500f4ce0a8662104e2a656ea98cf2558f))
* csp prevents external stylesheets ([#3337](https://github.com/homarr-labs/homarr/issues/3337)) ([c7a21b1](https://github.com/homarr-labs/homarr/commit/c7a21b1a5f4e7c9d994d81b75a3fd4fa7c013daf))
* **deps:** update dependency @tabler/icons-react to ^3.34.0 ([#3303](https://github.com/homarr-labs/homarr/issues/3303)) ([5b354ac](https://github.com/homarr-labs/homarr/commit/5b354aca2047e8316b331e327cf6031690dcfef9))
* **deps:** update dependency dockerode to ^4.0.7 ([#3331](https://github.com/homarr-labs/homarr/issues/3331)) ([07f81ce](https://github.com/homarr-labs/homarr/commit/07f81cee92627e3b6ee4b2bbf084657f9a83ac34))
* **deps:** update dependency drizzle-orm to ^0.44.1 ([#3280](https://github.com/homarr-labs/homarr/issues/3280)) ([9895c1b](https://github.com/homarr-labs/homarr/commit/9895c1b3ff028a7ea7273ab2625620c9132f5213))
* **deps:** update dependency drizzle-orm to ^0.44.2 ([#3315](https://github.com/homarr-labs/homarr/issues/3315)) ([9ac9042](https://github.com/homarr-labs/homarr/commit/9ac90425647bf1f10d3f299f043c5d40067347bc))
* **deps:** update dependency fast-xml-parser to ^5.2.4 ([#3336](https://github.com/homarr-labs/homarr/issues/3336)) ([3ad72e6](https://github.com/homarr-labs/homarr/commit/3ad72e63b1adecd18c53897a6023f09d6f5d011c))
* **deps:** update dependency node-cron to ^4.1.0 ([#3286](https://github.com/homarr-labs/homarr/issues/3286)) ([1f40868](https://github.com/homarr-labs/homarr/commit/1f408685e69397fa9e3a69bd4331a8240da3713b))
* **deps:** update dependency sass to ^1.89.1 ([#3285](https://github.com/homarr-labs/homarr/issues/3285)) ([b608ffe](https://github.com/homarr-labs/homarr/commit/b608ffe4900a5dd97ceb2669464c477404d090e7))
* **deps:** update dependency swagger-ui-react to ^5.24.0 ([#3325](https://github.com/homarr-labs/homarr/issues/3325)) ([cf7336b](https://github.com/homarr-labs/homarr/commit/cf7336ba87c617578ab52e2ece94fd84df3fc623))
* **deps:** update dependency typescript-eslint to ^8.33.1 ([#3302](https://github.com/homarr-labs/homarr/issues/3302)) ([124e8de](https://github.com/homarr-labs/homarr/commit/124e8de6c584b0efca91d92079f8b3c400bccab2))
* **deps:** update dependency video.js to ^8.23.3 ([#3320](https://github.com/homarr-labs/homarr/issues/3320)) ([81509fd](https://github.com/homarr-labs/homarr/commit/81509fd96dfe0561964eb58d3d21c02400db2e8c))
* **deps:** update dependency zod to ^3.25.43 ([#3290](https://github.com/homarr-labs/homarr/issues/3290)) ([161fc86](https://github.com/homarr-labs/homarr/commit/161fc867703c4cf634eaaeec1b73f5e3cbfa7fec))
* **deps:** update dependency zod to ^3.25.45 ([#3291](https://github.com/homarr-labs/homarr/issues/3291)) ([3337cfa](https://github.com/homarr-labs/homarr/commit/3337cfa3c7e34debcccdbb95ee0bc39f2af98a91))
* **deps:** update dependency zod to ^3.25.46 ([#3292](https://github.com/homarr-labs/homarr/issues/3292)) ([1b1671e](https://github.com/homarr-labs/homarr/commit/1b1671e0e6d808469ba7f8185c4590cf2f2f6f44))
* **deps:** update dependency zod to ^3.25.47 ([#3296](https://github.com/homarr-labs/homarr/issues/3296)) ([94f176d](https://github.com/homarr-labs/homarr/commit/94f176d544272e0378e65255aba58b05d56019b2))
* **deps:** update dependency zod to ^3.25.48 ([#3297](https://github.com/homarr-labs/homarr/issues/3297)) ([ccd8a85](https://github.com/homarr-labs/homarr/commit/ccd8a85e0fcd2cf40dbfb6058787d24a02680b93))
* **deps:** update dependency zod to ^3.25.49 ([#3304](https://github.com/homarr-labs/homarr/issues/3304)) ([0362487](https://github.com/homarr-labs/homarr/commit/0362487156457ae825cac501e7d53db58c31ab0b))
* **deps:** update dependency zod to ^3.25.50 ([#3312](https://github.com/homarr-labs/homarr/issues/3312)) ([a0a0449](https://github.com/homarr-labs/homarr/commit/a0a0449c704ff57725ab5e0b459eb209fc929446))
* **deps:** update dependency zod to ^3.25.51 ([#3316](https://github.com/homarr-labs/homarr/issues/3316)) ([a542ee0](https://github.com/homarr-labs/homarr/commit/a542ee079871c7609ed97c76aa2017305adbfdc9))
* **deps:** update dependency zod to ^3.25.55 ([#3332](https://github.com/homarr-labs/homarr/issues/3332)) ([ed3f848](https://github.com/homarr-labs/homarr/commit/ed3f848e8afb57cc1b9249551f0260eb52bbd4a6))
* **deps:** update tanstack-query monorepo to ^5.79.1 ([#3300](https://github.com/homarr-labs/homarr/issues/3300)) ([a3deed3](https://github.com/homarr-labs/homarr/commit/a3deed3b090aed0df0f45a9bd04c006640691e05))
* **deps:** update tanstack-query monorepo to ^5.79.2 ([#3301](https://github.com/homarr-labs/homarr/issues/3301)) ([a0585e3](https://github.com/homarr-labs/homarr/commit/a0585e3dd7d9cfbccf8ff5535ae2c753a8b92764))
* **deps:** update tanstack-query monorepo to ^5.80.0 ([#3307](https://github.com/homarr-labs/homarr/issues/3307)) ([df2a9a5](https://github.com/homarr-labs/homarr/commit/df2a9a5329e06847904af56a0d058c0da59f6380))
* **deps:** update tanstack-query monorepo to ^5.80.2 ([#3308](https://github.com/homarr-labs/homarr/issues/3308)) ([cae0cad](https://github.com/homarr-labs/homarr/commit/cae0cad92f68f716887e077194d4e30402466d57))
* **deps:** update tanstack-query monorepo to ^5.80.3 ([#3317](https://github.com/homarr-labs/homarr/issues/3317)) ([c051a42](https://github.com/homarr-labs/homarr/commit/c051a42791de4eae4baf20360efe5b0c8925c22b))
* **deps:** update tanstack-query monorepo to ^5.80.5 ([#3321](https://github.com/homarr-labs/homarr/issues/3321)) ([cb77a7d](https://github.com/homarr-labs/homarr/commit/cb77a7dc6d60560250765d129aac9bd236693e2b))
* **deps:** update tanstack-query monorepo to ^5.80.6 ([#3326](https://github.com/homarr-labs/homarr/issues/3326)) ([61553cf](https://github.com/homarr-labs/homarr/commit/61553cfc8e48a1179a148007fb970b1f4e6d509c))
* **deps:** update testcontainers-node monorepo to v11 (major) ([#3250](https://github.com/homarr-labs/homarr/issues/3250)) ([0d1e3a6](https://github.com/homarr-labs/homarr/commit/0d1e3a625a04abd5f667640ab40e01d28e017488))
* **deps:** update tiptap monorepo to v2.13.0 ([#3323](https://github.com/homarr-labs/homarr/issues/3323)) ([dac8234](https://github.com/homarr-labs/homarr/commit/dac82347e86dd7faacb24adbfc61ace9c917b8cd))
* **deps:** update tiptap monorepo to v2.14.0 ([#3334](https://github.com/homarr-labs/homarr/issues/3334)) ([6274a66](https://github.com/homarr-labs/homarr/commit/6274a66048163513e3faaf7011d7d6aa6ad9f161))
* **deps:** update trpc monorepo to ^11.2.0 ([#3293](https://github.com/homarr-labs/homarr/issues/3293)) ([f0ba693](https://github.com/homarr-labs/homarr/commit/f0ba693077a0c7f7897c81f02ad66b77fce52f1c))
* **deps:** update trpc monorepo to ^11.3.0 ([#3322](https://github.com/homarr-labs/homarr/issues/3322)) ([74aa561](https://github.com/homarr-labs/homarr/commit/74aa561f470f0f76421f356998caa91dc8bb65a1))
* **deps:** update trpc monorepo to ^11.3.1 ([#3329](https://github.com/homarr-labs/homarr/issues/3329)) ([8484daa](https://github.com/homarr-labs/homarr/commit/8484daaf7516ad72dd9e7de3117b5e0d50415dbb))
* **nextcloud:** correct calendar event date mapping and support showU… ([#3330](https://github.com/homarr-labs/homarr/issues/3330)) ([ffd2f80](https://github.com/homarr-labs/homarr/commit/ffd2f802bb777f13eac1ee45e1b5f0ca2b596672))
## [1.22.0](https://github.com/homarr-labs/homarr/compare/v1.21.0...v1.22.0) (2025-05-30)
### Features
* allow custom protocols for app href ([#2575](https://github.com/homarr-labs/homarr/issues/2575)) ([09af0a7](https://github.com/homarr-labs/homarr/commit/09af0a76f64c2fec512ba30726827208d5c3ad95))
* **cluster-health:** add visibility options ([#3210](https://github.com/homarr-labs/homarr/issues/3210)) ([939df8f](https://github.com/homarr-labs/homarr/commit/939df8f6d11bb36f71854425e4fdc4ca0724da31))
* **downloads:** add option to limit amount of items ([#3205](https://github.com/homarr-labs/homarr/issues/3205)) ([2dc871e](https://github.com/homarr-labs/homarr/commit/2dc871e531b58aee25db26ab7c5e94626517db36))
### Bug Fixes
* **boards:** app select modal images rendering issue ([#3218](https://github.com/homarr-labs/homarr/issues/3218)) ([85e98d2](https://github.com/homarr-labs/homarr/commit/85e98d2b603abe18b37fa74bf50836422b72c9e1))
* **calendar:** load events for all visible days of calendar month ([#3216](https://github.com/homarr-labs/homarr/issues/3216)) ([437ecd1](https://github.com/homarr-labs/homarr/commit/437ecd10239c70e5a8c4d34119076ab155a2159e))
* **calendar:** remove scrollbar for 2x2 ([#3227](https://github.com/homarr-labs/homarr/issues/3227)) ([1d27da2](https://github.com/homarr-labs/homarr/commit/1d27da22303b2133bb1a7c59102b7c292bc34833))
* **cluster-health:** storage items multiply ([#3206](https://github.com/homarr-labs/homarr/issues/3206)) ([1114934](https://github.com/homarr-labs/homarr/commit/11149348fcbe9002bba33763e0b03aed50d20f71))
* csp prevents fonts to load ([#3225](https://github.com/homarr-labs/homarr/issues/3225)) ([05e159f](https://github.com/homarr-labs/homarr/commit/05e159f5662f04dd298eb13816fe1768d7ef9312))
* **deps:** update dependency @ianvs/prettier-plugin-sort-imports to ^4.4.2 ([#3258](https://github.com/homarr-labs/homarr/issues/3258)) ([4a184d2](https://github.com/homarr-labs/homarr/commit/4a184d23f25a8a06b36cd8c6d9b2a65118399d9f))
* **deps:** update dependency @kubernetes/client-node to ^1.3.0 ([#3261](https://github.com/homarr-labs/homarr/issues/3261)) ([635c9a2](https://github.com/homarr-labs/homarr/commit/635c9a2687cf2b9a09a3093eb638ef772eddfb03))
* **deps:** update dependency @t3-oss/env-nextjs to ^0.13.6 ([#3244](https://github.com/homarr-labs/homarr/issues/3244)) ([8966e2a](https://github.com/homarr-labs/homarr/commit/8966e2ad8a2025dfc91934ccf825bd76c08dc333))
* **deps:** update dependency drizzle-orm to ^0.44.0 ([#3256](https://github.com/homarr-labs/homarr/issues/3256)) ([09b49a2](https://github.com/homarr-labs/homarr/commit/09b49a242dd5d3d34bab758f190ab948b6fc7566))
* **deps:** update dependency flag-icons to ^7.4.0 ([#3265](https://github.com/homarr-labs/homarr/issues/3265)) ([a0eda74](https://github.com/homarr-labs/homarr/commit/a0eda74fa472446c5b93c1b65829e65670620ab6))
* **deps:** update dependency flag-icons to ^7.5.0 ([#3268](https://github.com/homarr-labs/homarr/issues/3268)) ([2320d3c](https://github.com/homarr-labs/homarr/commit/2320d3c5674413e18e9bb42fd004ada016c5e605))
* **deps:** update dependency jotai to ^2.12.5 ([#3243](https://github.com/homarr-labs/homarr/issues/3243)) ([9c32385](https://github.com/homarr-labs/homarr/commit/9c323850bc49df5f6fc3437678faab0c7eab2e53))
* **deps:** update dependency ldapts to v8.0.1 ([#3264](https://github.com/homarr-labs/homarr/issues/3264)) ([bac4f64](https://github.com/homarr-labs/homarr/commit/bac4f642574caf5f170d20353c036396698e5963))
* **deps:** update dependency node-cron to v4 ([#3075](https://github.com/homarr-labs/homarr/issues/3075)) ([109f9e7](https://github.com/homarr-labs/homarr/commit/109f9e79793b36b46ab1987a56f7c206e21d32e4))
* **deps:** update dependency octokit to ^5.0.3 ([#3249](https://github.com/homarr-labs/homarr/issues/3249)) ([3253b2f](https://github.com/homarr-labs/homarr/commit/3253b2f138a9e911b50c952e37fb4afb1f71446d))
* **deps:** update dependency trpc-to-openapi to ^2.3.1 ([#3046](https://github.com/homarr-labs/homarr/issues/3046)) ([640bf7b](https://github.com/homarr-labs/homarr/commit/640bf7b939b201d9905f5119af0dc0da6120b98c))
* **deps:** update dependency typescript-eslint to ^8.33.0 ([#3247](https://github.com/homarr-labs/homarr/issues/3247)) ([8b553b8](https://github.com/homarr-labs/homarr/commit/8b553b80e61ab608d01d890d26a8ea5bae3f38b8))
* **deps:** update dependency zod to ^3.25.24 ([#3195](https://github.com/homarr-labs/homarr/issues/3195)) ([0b224e8](https://github.com/homarr-labs/homarr/commit/0b224e8835d4fc5a292a624c8af8d874216b703a))
* **deps:** update dependency zod to ^3.25.28 ([#3201](https://github.com/homarr-labs/homarr/issues/3201)) ([e0a9e4f](https://github.com/homarr-labs/homarr/commit/e0a9e4f97c9dddb95fbc0b2a65acfd802dac9c19))
* **deps:** update dependency zod to ^3.25.29 ([#3237](https://github.com/homarr-labs/homarr/issues/3237)) ([81ca6ef](https://github.com/homarr-labs/homarr/commit/81ca6ef7b2767fb0bc285ffa53998e2c586225ec))
* **deps:** update dependency zod to ^3.25.30 ([#3239](https://github.com/homarr-labs/homarr/issues/3239)) ([3062948](https://github.com/homarr-labs/homarr/commit/30629489396668125dec7d93fcb0e733099c35fe))
* **deps:** update dependency zod to ^3.25.31 ([#3254](https://github.com/homarr-labs/homarr/issues/3254)) ([e2e4656](https://github.com/homarr-labs/homarr/commit/e2e46560010a94603852ea90a0fc63fabd473198))
* **deps:** update dependency zod to ^3.25.32 ([#3255](https://github.com/homarr-labs/homarr/issues/3255)) ([9c3a46c](https://github.com/homarr-labs/homarr/commit/9c3a46c071e169459ec7b8c9ba1647b4b354cbea))
* **deps:** update dependency zod to ^3.25.34 ([#3259](https://github.com/homarr-labs/homarr/issues/3259)) ([c1324ea](https://github.com/homarr-labs/homarr/commit/c1324eab76795dabc0a41315191f9ab6a73ba501))
* **deps:** update dependency zod to ^3.25.36 ([#3262](https://github.com/homarr-labs/homarr/issues/3262)) ([48ce2af](https://github.com/homarr-labs/homarr/commit/48ce2af4c182985a7721b65535bcf1b41f1cdcba))
* **deps:** update dependency zod to ^3.25.39 ([#3270](https://github.com/homarr-labs/homarr/issues/3270)) ([862d07d](https://github.com/homarr-labs/homarr/commit/862d07db68e7624342cb81241ef9924e6407fda8))
* **deps:** update dependency zod to ^3.25.41 ([#3272](https://github.com/homarr-labs/homarr/issues/3272)) ([5bf20aa](https://github.com/homarr-labs/homarr/commit/5bf20aa3538503de1d4f4d04f9de3e3b92442d75))
* **deps:** update dependency zod to ^3.25.42 ([#3274](https://github.com/homarr-labs/homarr/issues/3274)) ([608280e](https://github.com/homarr-labs/homarr/commit/608280e670432098ce466cfb8fa2b315bf35ead2))
* **deps:** update mantine monorepo to ^8.0.2 ([#3245](https://github.com/homarr-labs/homarr/issues/3245)) ([816544a](https://github.com/homarr-labs/homarr/commit/816544a996656ccf7cbacf76aff83d013ba21ad1))
* **deps:** update nextjs monorepo to v15.3.3 ([#3269](https://github.com/homarr-labs/homarr/issues/3269)) ([d72a804](https://github.com/homarr-labs/homarr/commit/d72a804ad9feb24a2b067712128a6566930990a6))
* **deps:** update tanstack-query monorepo to ^5.77.0 ([#3204](https://github.com/homarr-labs/homarr/issues/3204)) ([ec42f26](https://github.com/homarr-labs/homarr/commit/ec42f265a04f0f96105d6b1318becd5cc94aa8fb))
* **deps:** update tanstack-query monorepo to ^5.77.1 ([#3229](https://github.com/homarr-labs/homarr/issues/3229)) ([5ea7a20](https://github.com/homarr-labs/homarr/commit/5ea7a206772924faf1f842c80033954c853b3a02))
* **deps:** update tanstack-query monorepo to ^5.77.2 ([#3232](https://github.com/homarr-labs/homarr/issues/3232)) ([7118690](https://github.com/homarr-labs/homarr/commit/71186908b96f717c6f8c528b0f983e1792a7a885))
* **deps:** update tanstack-query monorepo to ^5.79.0 ([#3263](https://github.com/homarr-labs/homarr/issues/3263)) ([1f9983f](https://github.com/homarr-labs/homarr/commit/1f9983f16db8677b8c244b63306ea4afd2195d94))
* **deps:** update trpc monorepo to ^11.1.3 ([#3240](https://github.com/homarr-labs/homarr/issues/3240)) ([78f98ed](https://github.com/homarr-labs/homarr/commit/78f98edc4145e27e2daa269121173905c642c139))
* **deps:** update trpc monorepo to ^11.1.4 ([#3260](https://github.com/homarr-labs/homarr/issues/3260)) ([bfd7b70](https://github.com/homarr-labs/homarr/commit/bfd7b706182340eaa1db70a3c6cdcc935aee8df4))
* **deps:** update turbo monorepo to ^2.5.4 ([#3279](https://github.com/homarr-labs/homarr/issues/3279)) ([c8202ab](https://github.com/homarr-labs/homarr/commit/c8202ab3c676bf796c6e7aff2ebfef224a0124c8))
* **downloads:** upspeed font size wrong ([#3214](https://github.com/homarr-labs/homarr/issues/3214)) ([c67916f](https://github.com/homarr-labs/homarr/commit/c67916f72eded64133fd458184a599f32d8fc63c))
* **media-request-stats:** users squeezed together ([#3213](https://github.com/homarr-labs/homarr/issues/3213)) ([25bd387](https://github.com/homarr-labs/homarr/commit/25bd3879db9260bbd24a1338cc5f26652ed78d72))
* navigating back results in navigation back to first page opened ([#3224](https://github.com/homarr-labs/homarr/issues/3224)) ([cbbf0c8](https://github.com/homarr-labs/homarr/commit/cbbf0c8dcf685e31e70bb62f04b1065dab90e18d))
* reduce logging ([#3200](https://github.com/homarr-labs/homarr/issues/3200)) ([be8b332](https://github.com/homarr-labs/homarr/commit/be8b332f942beb3af181ee6f39558882ed640a0e))
* **search:** removing server search engine causes loading error ([#3252](https://github.com/homarr-labs/homarr/issues/3252)) ([10f720d](https://github.com/homarr-labs/homarr/commit/10f720d74c6cc70cbf217e8daf8cc7878ac6d712))
* **visual:** check for creator board permission is wrong ([#3212](https://github.com/homarr-labs/homarr/issues/3212)) ([f7e5e82](https://github.com/homarr-labs/homarr/commit/f7e5e823d5dee36ea11f243a0da7d84a1f363419))
### Performance Improvements
* cache middleware onboarding and language checks ([#3197](https://github.com/homarr-labs/homarr/issues/3197)) ([2afa9b6](https://github.com/homarr-labs/homarr/commit/2afa9b67274edd79b52598dc0d22437048a67e2c))
## [1.21.0](https://github.com/homarr-labs/homarr/compare/v1.20.0...v1.21.0) (2025-05-23) ## [1.21.0](https://github.com/homarr-labs/homarr/compare/v1.20.0...v1.21.0) (2025-05-23)
### Features ### Features
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:22.17.0-alpine AS base FROM node:22.16.0-alpine AS base
FROM base AS builder FROM base AS builder
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
+19 -32
View File
@@ -17,16 +17,31 @@ const withNextIntl = createNextIntlPlugin({
requestConfig: "../../packages/translation/src/request.ts", requestConfig: "../../packages/translation/src/request.ts",
}); });
interface WebpackConfig {
module: {
rules: {
test: RegExp;
loader: string;
}[];
};
}
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "standalone", output: "standalone",
reactStrictMode: true, reactStrictMode: true,
/** We already do linting and typechecking as separate tasks in CI */ /** We already do linting and typechecking as separate tasks in CI */
eslint: { ignoreDuringBuilds: true }, eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true }, typescript: { ignoreBuildErrors: true },
/** webpack: (config: WebpackConfig, { isServer }) => {
* dockerode is required in the external server packages because of https://github.com/homarr-labs/homarr/issues/612 if (isServer) {
*/ config.module.rules.push({
serverExternalPackages: ["dockerode"], test: /\.node$/,
loader: "node-loader",
});
}
return config;
},
experimental: { experimental: {
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"], optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
}, },
@@ -34,34 +49,6 @@ const nextConfig: NextConfig = {
images: { images: {
domains: ["cdn.jsdelivr.net"], domains: ["cdn.jsdelivr.net"],
}, },
// eslint-disable-next-line @typescript-eslint/require-await,no-restricted-syntax
async headers() {
return [
{
source: "/(.*)", // Apply CSP to all routes
headers: [
{
key: "Content-Security-Policy",
value: `
default-src 'self';
script-src * 'unsafe-inline' 'unsafe-eval';
base-uri 'self';
connect-src *;
style-src * 'unsafe-inline';
frame-ancestors *;
frame-src *;
form-action 'self';
img-src * data:;
font-src * data:;
media-src * data:;
`
.replace(/\s{2,}/g, " ")
.trim(),
},
],
},
];
},
}; };
// Skip transform is used because of webpack loader, without it for example 'Tooltip.Floating' will not work and show an error // Skip transform is used because of webpack loader, without it for example 'Tooltip.Floating' will not work and show an error
+29 -30
View File
@@ -29,7 +29,6 @@
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
"@homarr/docker": "workspace:^0.1.0", "@homarr/docker": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0",
"@homarr/form": "workspace:^0.1.0", "@homarr/form": "workspace:^0.1.0",
"@homarr/forms-collection": "workspace:^0.1.0", "@homarr/forms-collection": "workspace:^0.1.0",
"@homarr/gridstack": "^1.12.0", "@homarr/gridstack": "^1.12.0",
@@ -49,59 +48,59 @@
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0", "@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "^8.1.2", "@mantine/colors-generator": "^8.0.1",
"@mantine/core": "^8.1.2", "@mantine/core": "^8.0.1",
"@mantine/dropzone": "^8.1.2", "@mantine/dropzone": "^8.0.1",
"@mantine/hooks": "^8.1.2", "@mantine/hooks": "^8.0.1",
"@mantine/modals": "^8.1.2", "@mantine/modals": "^8.0.1",
"@mantine/tiptap": "^8.1.2", "@mantine/tiptap": "^8.0.1",
"@million/lint": "1.0.14", "@million/lint": "1.0.14",
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.33.0",
"@tanstack/react-query": "^5.81.5", "@tanstack/react-query": "^5.76.2",
"@tanstack/react-query-devtools": "^5.81.5", "@tanstack/react-query-devtools": "^5.76.2",
"@tanstack/react-query-next-experimental": "^5.81.5", "@tanstack/react-query-next-experimental": "^5.76.2",
"@trpc/client": "^11.4.3", "@trpc/client": "^11.1.2",
"@trpc/next": "^11.4.3", "@trpc/next": "^11.1.2",
"@trpc/react-query": "^11.4.3", "@trpc/react-query": "^11.1.2",
"@trpc/server": "^11.4.3", "@trpc/server": "^11.1.2",
"@xterm/addon-canvas": "^0.7.0", "@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.10.0", "@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"chroma-js": "^3.1.2", "chroma-js": "^3.1.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dotenv": "^17.0.1", "dotenv": "^16.5.0",
"flag-icons": "^7.5.0", "flag-icons": "^7.3.2",
"glob": "^11.0.3", "glob": "^11.0.2",
"jotai": "^2.12.5", "jotai": "^2.12.4",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "15.3.5", "next": "15.3.2",
"postcss-preset-mantine": "^1.18.0", "postcss-preset-mantine": "^1.17.0",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1", "react-simple-code-editor": "^0.14.1",
"sass": "^1.89.2", "sass": "^1.89.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"swagger-ui-react": "^5.26.0", "swagger-ui-react": "^5.22.0",
"use-deep-compare-effect": "^1.8.1", "use-deep-compare-effect": "^1.8.1",
"zod": "^3.25.74" "zod": "^3.25.23"
}, },
"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.16.0", "@types/node": "^22.15.21",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@types/react": "19.1.8", "@types/react": "19.1.5",
"@types/react-dom": "19.1.6", "@types/react-dom": "19.1.5",
"@types/swagger-ui-react": "^5.18.0", "@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.2.0", "concurrently": "^9.1.2",
"eslint": "^9.30.1", "eslint": "^9.27.0",
"node-loader": "^2.1.0", "node-loader": "^2.1.0",
"prettier": "^3.6.2", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 595 KiB

@@ -1,5 +1,6 @@
"use client"; "use client";
import type { MouseEvent } from "react";
import { useCallback, useEffect } from "react"; import { useCallback, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -203,22 +204,17 @@ const SelectBoardsMenu = () => {
); );
}; };
const anchorSelector = "a[href]:not([target='_blank'])";
const usePreventLeaveWithDirty = (isDirty: boolean) => { const usePreventLeaveWithDirty = (isDirty: boolean) => {
const t = useI18n(); const t = useI18n();
const { openConfirmModal } = useConfirmModal(); const { openConfirmModal } = useConfirmModal();
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
if (!isDirty) return; const handleClick = (event: MouseEvent<HTMLElement>) => {
const handleClick = (event: Event) => {
const target = (event.target as HTMLElement).closest("a"); const target = (event.target as HTMLElement).closest("a");
if (!target) { if (!target) return;
console.warn("No anchor element found for click event", event); if (!isDirty) return;
return;
}
event.preventDefault(); event.preventDefault();
@@ -235,29 +231,33 @@ const usePreventLeaveWithDirty = (isDirty: boolean) => {
}; };
const handlePopState = (event: Event) => { const handlePopState = (event: Event) => {
window.history.pushState(null, document.title, window.location.href); if (isDirty) {
event.preventDefault(); window.history.pushState(null, document.title, window.location.href);
event.preventDefault();
} else {
window.history.back();
}
}; };
const handleBeforeUnload = (event: BeforeUnloadEvent) => { const handleBeforeUnload = (event: BeforeUnloadEvent) => {
if (!isDirty) return;
if (env.NODE_ENV === "development") return; // Allow to reload in development if (env.NODE_ENV === "development") return; // Allow to reload in development
event.preventDefault(); event.preventDefault();
event.returnValue = true; event.returnValue = true;
}; };
const anchors = document.querySelectorAll(anchorSelector); document.querySelectorAll("a").forEach((link) => {
anchors.forEach((link) => { link.addEventListener("click", handleClick as never);
link.addEventListener("click", handleClick);
}); });
window.addEventListener("popstate", handlePopState); window.addEventListener("popstate", handlePopState);
window.addEventListener("beforeunload", handleBeforeUnload); window.addEventListener("beforeunload", handleBeforeUnload);
return () => { return () => {
anchors.forEach((link) => { document.querySelectorAll("a").forEach((link) => {
link.removeEventListener("click", handleClick); link.removeEventListener("click", handleClick as never);
window.removeEventListener("popstate", handlePopState);
}); });
window.removeEventListener("popstate", handlePopState);
window.removeEventListener("beforeunload", handleBeforeUnload); window.removeEventListener("beforeunload", handleBeforeUnload);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -1,4 +1,4 @@
import { IconGrid3x3, IconKey, IconMessage, IconPassword, IconServer, IconUser } from "@tabler/icons-react"; import { IconGrid3x3, IconKey, IconPassword, IconServer, IconUser } from "@tabler/icons-react";
import type { IntegrationSecretKind } from "@homarr/definitions"; import type { IntegrationSecretKind } from "@homarr/definitions";
import type { TablerIcon } from "@homarr/ui"; import type { TablerIcon } from "@homarr/ui";
@@ -9,5 +9,4 @@ export const integrationSecretIcons = {
password: IconPassword, password: IconPassword,
realm: IconServer, realm: IconServer,
tokenId: IconGrid3x3, tokenId: IconGrid3x3,
topic: IconMessage,
} satisfies Record<IntegrationSecretKind, TablerIcon>; } satisfies Record<IntegrationSecretKind, TablerIcon>;
@@ -30,8 +30,6 @@ export const CertificateErrorDetails = ({ error, url }: CertificateErrorDetailsP
const { mutateAsync: trustHostnameAsync } = clientApi.certificates.trustHostnameMismatch.useMutation(); const { mutateAsync: trustHostnameAsync } = clientApi.certificates.trustHostnameMismatch.useMutation();
const { mutateAsync: addCertificateAsync } = clientApi.certificates.addCertificate.useMutation(); const { mutateAsync: addCertificateAsync } = clientApi.certificates.addCertificate.useMutation();
const rootCertificate = getHeighestCertificate(error.data.certificate);
const handleTrustHostname = () => { const handleTrustHostname = () => {
const { hostname } = new URL(url); const { hostname } = new URL(url);
openConfirmModal({ openConfirmModal({
@@ -74,7 +72,7 @@ export const CertificateErrorDetails = ({ error, url }: CertificateErrorDetailsP
const formData = new FormData(); const formData = new FormData();
formData.append( formData.append(
"file", "file",
new File([rootCertificate.pem], `${hostname}-${createId()}.crt`, { new File([error.data.certificate.pem], `${hostname}-${createId()}.crt`, {
type: "application/x-x509-ca-cert", type: "application/x-x509-ca-cert",
}), }),
); );
@@ -112,11 +110,11 @@ export const CertificateErrorDetails = ({ error, url }: CertificateErrorDetailsP
<> <>
{description} {description}
<CertificateDetailsCard certificate={rootCertificate} /> <CertificateDetailsCard certificate={error.data.certificate} />
{error.data.reason === "hostnameMismatch" && <HostnameMismatchAlert />} {error.data.reason === "hostnameMismatch" && <HostnameMismatchAlert />}
{!rootCertificate.isSelfSigned && error.data.reason === "untrusted" && <CertificateExtractAlert />} {!error.data.certificate.isSelfSigned && error.data.reason === "untrusted" && <CertificateExtractAlert />}
{showRetryButton && ( {showRetryButton && (
<Button <Button
@@ -129,7 +127,7 @@ export const CertificateErrorDetails = ({ error, url }: CertificateErrorDetailsP
</Button> </Button>
)} )}
{(error.data.reason === "untrusted" && rootCertificate.isSelfSigned) || {(error.data.reason === "untrusted" && error.data.certificate.isSelfSigned) ||
error.data.reason === "hostnameMismatch" ? ( error.data.reason === "hostnameMismatch" ? (
<Button <Button
variant="default" variant="default"
@@ -139,7 +137,7 @@ export const CertificateErrorDetails = ({ error, url }: CertificateErrorDetailsP
{tError("certificate.action.trust.label")} {tError("certificate.action.trust.label")}
</Button> </Button>
) : null} ) : null}
{error.data.reason === "untrusted" && !rootCertificate.isSelfSigned ? ( {error.data.reason === "untrusted" && !error.data.certificate.isSelfSigned ? (
<Button <Button
variant="default" variant="default"
fullWidth fullWidth
@@ -327,8 +325,3 @@ const PemContentModal = createModal<{ content: string }>(({ actions, innerProps
}, },
size: "lg", size: "lg",
}); });
const getHeighestCertificate = (certificate: MappedCertificate): MappedCertificate => {
if (certificate.issuerCertificate) return getHeighestCertificate(certificate.issuerCertificate);
return certificate;
};
@@ -10,20 +10,15 @@ import { getIntegrationName, integrationKinds } from "@homarr/definitions";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { IntegrationAvatar } from "@homarr/ui"; import { IntegrationAvatar } from "@homarr/ui";
interface IntegrationCreateDropdownContentProps { export const IntegrationCreateDropdownContent = () => {
enableMockIntegration: boolean;
}
export const IntegrationCreateDropdownContent = ({ enableMockIntegration }: IntegrationCreateDropdownContentProps) => {
const t = useI18n(); const t = useI18n();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const filteredKinds = useMemo(() => { const filteredKinds = useMemo(() => {
return integrationKinds return integrationKinds.filter((kind) =>
.filter((kind) => enableMockIntegration || kind !== "mock") getIntegrationName(kind).toLowerCase().includes(search.toLowerCase().trim()),
.filter((kind) => getIntegrationName(kind).toLowerCase().includes(search.toLowerCase().trim())) );
.sort((kindA, kindB) => getIntegrationName(kindA).localeCompare(getIntegrationName(kindB))); }, [search]);
}, [search, enableMockIntegration]);
const handleSearch = React.useCallback( const handleSearch = React.useCallback(
(event: ChangeEvent<HTMLInputElement>) => setSearch(event.target.value), (event: ChangeEvent<HTMLInputElement>) => setSearch(event.target.value),
@@ -41,7 +41,6 @@ import { CountBadge, IntegrationAvatar } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container"; import { ManageContainer } from "~/components/manage/manage-container";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NoResults } from "~/components/no-results"; import { NoResults } from "~/components/no-results";
import { env } from "~/env";
import { ActiveTabAccordion } from "../../../../components/active-tab-accordion"; import { ActiveTabAccordion } from "../../../../components/active-tab-accordion";
import { DeleteIntegrationActionButton } from "./_integration-buttons"; import { DeleteIntegrationActionButton } from "./_integration-buttons";
import { IntegrationCreateDropdownContent } from "./new/_integration-new-dropdown"; import { IntegrationCreateDropdownContent } from "./new/_integration-new-dropdown";
@@ -115,7 +114,7 @@ const IntegrationSelectMenu = ({ children }: PropsWithChildren) => {
> >
{children} {children}
<MenuDropdown> <MenuDropdown>
<IntegrationCreateDropdownContent enableMockIntegration={env.UNSAFE_ENABLE_MOCK_INTEGRATION} /> <IntegrationCreateDropdownContent />
</MenuDropdown> </MenuDropdown>
</Menu> </Menu>
); );
@@ -1,216 +1,89 @@
"use client"; "use client";
import React, { useState, useTransition } from "react"; import React from "react";
import { ActionIcon, Badge, Button, Card, Group, Select, Stack, Text } from "@mantine/core"; import { ActionIcon, Badge, Card, Group, Stack, Text } from "@mantine/core";
import { useMap } from "@mantine/hooks"; import { useListState } from "@mantine/hooks";
import { IconPlayerPlay, IconPower, IconSettings } from "@tabler/icons-react"; import { IconPlayerPlay } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { getMantineColor, useTimeAgo } from "@homarr/common"; import { useTimeAgo } from "@homarr/common";
import type { TaskStatus } from "@homarr/cron-job-status"; import type { TaskStatus } from "@homarr/cron-job-status";
import { useForm } from "@homarr/form"; import type { TranslationKeys } from "@homarr/translation";
import { createModal, useModalAction } from "@homarr/modals"; import { useScopedI18n } from "@homarr/translation/client";
import { TranslationFunction } from "@homarr/translation";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { IconPowerOff } from "@homarr/ui/icons";
interface JobsListProps { interface JobsListProps {
initialJobs: RouterOutputs["cronJobs"]["getJobs"]; initialJobs: RouterOutputs["cronJobs"]["getJobs"];
} }
type JobName = RouterOutputs["cronJobs"]["getJobs"][number]["name"]; interface JobState {
job: JobsListProps["initialJobs"][number];
status: TaskStatus | null;
}
export const JobsList = ({ initialJobs }: JobsListProps) => { export const JobsList = ({ initialJobs }: JobsListProps) => {
const [jobs] = clientApi.cronJobs.getJobs.useSuspenseQuery(undefined, { const t = useScopedI18n("management.page.tool.tasks");
initialData: initialJobs, const [jobs, handlers] = useListState<JobState>(
refetchOnMount: false, initialJobs.map((job) => ({
}); job,
status: null,
const jobStatusMap = useMap<string, TaskStatus | null>(initialJobs.map(({ name }) => [name, null] as const)); })),
);
clientApi.cronJobs.subscribeToStatusUpdates.useSubscription(undefined, { clientApi.cronJobs.subscribeToStatusUpdates.useSubscription(undefined, {
onData: (data) => { onData: (data) => {
jobStatusMap.set(data.name, data); const jobByName = jobs.find((job) => job.job.name === data.name);
if (!jobByName) {
return;
}
handlers.applyWhere(
(job) => job.job.name === data.name,
(job) => ({ ...job, status: data }),
);
}, },
}); });
const { mutateAsync } = clientApi.cronJobs.triggerJob.useMutation();
const handleJobTrigger = React.useCallback(
async (job: JobState) => {
if (job.status?.status === "running") {
return;
}
await mutateAsync(job.job.name);
},
[mutateAsync],
);
return ( return (
<Stack> <Stack>
{jobs.map((job) => { {jobs.map((job) => (
const status = jobStatusMap.get(job.name); <Card key={job.job.name} withBorder>
<Group justify={"space-between"} gap={"md"}>
<Stack gap={0}>
<Group>
<Text>{t(`job.${job.job.name}.label` as TranslationKeys)}</Text>
{job.status?.status === "idle" && <Badge variant="default">{t("status.idle")}</Badge>}
{job.status?.status === "running" && <Badge color="green">{t("status.running")}</Badge>}
{job.status?.lastExecutionStatus === "error" && <Badge color="red">{t("status.error")}</Badge>}
</Group>
{job.status && <TimeAgo timestamp={job.status.lastExecutionTimestamp} />}
</Stack>
return <JobCard key={job.name} job={job} status={status ?? null} />; {!job.job.preventManualExecution && (
})} <ActionIcon
onClick={() => handleJobTrigger(job)}
disabled={job.status?.status === "running"}
variant={"default"}
size={"xl"}
radius={"xl"}
>
<IconPlayerPlay stroke={1.5} />
</ActionIcon>
)}
</Group>
</Card>
))}
</Stack> </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 = ({ timestamp }: { timestamp: string }) => {
const timeAgo = useTimeAgo(new Date(timestamp)); const timeAgo = useTimeAgo(new Date(timestamp));
@@ -220,65 +93,3 @@ const TimeAgo = ({ timestamp }: { timestamp: string }) => {
</Text> </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: "",
});
-9
View File
@@ -1,9 +0,0 @@
import { createEnv } from "@homarr/env";
import { createBooleanSchema } from "@homarr/env/schemas";
export const env = createEnv({
server: {
UNSAFE_ENABLE_MOCK_INTEGRATION: createBooleanSchema(false),
},
experimental__runtimeEnv: process.env,
});
+6 -17
View File
@@ -5,36 +5,25 @@ import SuperJSON from "superjson";
import type { AppRouter } from "@homarr/api"; import type { AppRouter } from "@homarr/api";
import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared"; import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
import { localeCookieKey } from "@homarr/definitions";
import type { SupportedLanguage } from "@homarr/translation";
import { supportedLanguages } from "@homarr/translation";
import { createI18nMiddleware } from "@homarr/translation/middleware"; import { createI18nMiddleware } from "@homarr/translation/middleware";
let isOnboardingFinished = false;
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
// fetch api does not work because window is not defined and we need to construct the url from the headers
// In next 15 we will be able to use node apis and such the db directly
const culture = await serverFetchApi.serverSettings.getCulture.query();
// Redirect to onboarding if it's not finished yet // Redirect to onboarding if it's not finished yet
const pathname = request.nextUrl.pathname; const pathname = request.nextUrl.pathname;
if (!pathname.endsWith("/init")) {
if (!isOnboardingFinished && !pathname.endsWith("/init")) {
const currentOnboardingStep = await serverFetchApi.onboard.currentStep.query(); const currentOnboardingStep = await serverFetchApi.onboard.currentStep.query();
if (currentOnboardingStep.current !== "finish") { if (currentOnboardingStep.current !== "finish") {
return NextResponse.redirect(new URL("/init", request.url)); return NextResponse.redirect(new URL("/init", request.url));
} }
isOnboardingFinished = true;
}
// Only run this if the user has not already configured their language
const currentLocale = request.cookies.get(localeCookieKey)?.value;
let defaultLocale: SupportedLanguage = "en";
if (!currentLocale || !supportedLanguages.includes(currentLocale as SupportedLanguage)) {
defaultLocale = await serverFetchApi.serverSettings.getCulture.query().then((culture) => culture.defaultLocale);
} }
// We don't want to fallback to accept-language header so we clear it // We don't want to fallback to accept-language header so we clear it
request.headers.set("accept-language", ""); request.headers.set("accept-language", "");
const next = createI18nMiddleware(defaultLocale); const next = createI18nMiddleware(culture.defaultLocale);
return next(request); return next(request);
} }
+8 -9
View File
@@ -22,7 +22,7 @@
"dependencies": { "dependencies": {
"@homarr/analytics": "workspace:^0.1.0", "@homarr/analytics": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/cron-job-api": "workspace:^0.1.0", "@homarr/cron-job-runner": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/cron-jobs-core": "workspace:^0.1.0", "@homarr/cron-jobs-core": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
@@ -36,21 +36,20 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0", "@homarr/widgets": "workspace:^0.1.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dotenv": "^17.0.1", "dotenv": "^16.5.0",
"fastify": "^5.4.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"undici": "7.11.0" "undici": "7.10.0"
}, },
"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": "^22.16.0", "@types/node": "^22.15.21",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"esbuild": "^0.25.5", "esbuild": "^0.25.4",
"eslint": "^9.30.1", "eslint": "^9.27.0",
"prettier": "^3.6.2", "prettier": "^3.5.3",
"tsx": "4.20.3", "tsx": "4.19.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
-109
View File
@@ -1,109 +0,0 @@
import { schedule, validate as validateCron } from "node-cron";
import type { IJobManager } from "@homarr/cron-job-api";
import type { jobGroup as cronJobGroup, JobGroupKeys } from "@homarr/cron-jobs";
import type { Database, InferInsertModel } from "@homarr/db";
import { eq } from "@homarr/db";
import { cronJobConfigurations } from "@homarr/db/schema";
import { logger } from "@homarr/log";
export class JobManager implements IJobManager {
constructor(
private db: Database,
private jobGroup: typeof cronJobGroup,
) {}
public async startAsync(name: JobGroupKeys): Promise<void> {
await this.jobGroup.startAsync(name);
}
public async triggerAsync(name: JobGroupKeys): Promise<void> {
await this.jobGroup.runManuallyAsync(name);
}
public async stopAsync(name: JobGroupKeys): Promise<void> {
await this.jobGroup.stopAsync(name);
}
public async updateIntervalAsync(name: JobGroupKeys, cron: string): Promise<void> {
logger.info(`Updating cron job interval name="${name}" expression="${cron}"`);
const job = this.jobGroup.getJobRegistry().get(name);
if (!job) throw new Error(`Job ${name} not found`);
if (job.cronExpression === "never") throw new Error(`Job ${name} cannot be updated as it is set to "never"`);
if (!validateCron(cron)) {
throw new Error(`Invalid cron expression: ${cron}`);
}
await this.updateConfigurationAsync(name, { cronExpression: cron });
await this.jobGroup.getTask(name)?.destroy();
this.jobGroup.setTask(
name,
schedule(cron, () => void job.executeAsync(), {
name,
}),
);
logger.info(`Cron job interval updated name="${name}" expression="${cron}"`);
}
public async disableAsync(name: JobGroupKeys): Promise<void> {
logger.info(`Disabling cron job name="${name}"`);
const job = this.jobGroup.getJobRegistry().get(name);
if (!job) throw new Error(`Job ${name} not found`);
if (job.cronExpression === "never") throw new Error(`Job ${name} cannot be disabled as it is set to "never"`);
await this.updateConfigurationAsync(name, { isEnabled: false });
await this.jobGroup.stopAsync(name);
logger.info(`Cron job disabled name="${name}"`);
}
public async enableAsync(name: JobGroupKeys): Promise<void> {
logger.info(`Enabling cron job name="${name}"`);
await this.updateConfigurationAsync(name, { isEnabled: true });
await this.jobGroup.startAsync(name);
logger.info(`Cron job enabled name="${name}"`);
}
private async updateConfigurationAsync(
name: JobGroupKeys,
configuration: Omit<Partial<InferInsertModel<typeof cronJobConfigurations>>, "name">,
) {
const existingConfig = await this.db.query.cronJobConfigurations.findFirst({
where: (table, { eq }) => eq(table.name, name),
});
logger.debug(
`Updating cron job configuration name="${name}" configuration="${JSON.stringify(configuration)}" exists="${Boolean(existingConfig)}"`,
);
if (existingConfig) {
await this.db
.update(cronJobConfigurations)
// prevent updating the name, as it is the primary key
.set({ ...configuration, name: undefined })
.where(eq(cronJobConfigurations.name, name));
logger.debug(`Cron job configuration updated name="${name}" configuration="${JSON.stringify(configuration)}"`);
return;
}
const job = this.jobGroup.getJobRegistry().get(name);
if (!job) throw new Error(`Job ${name} not found`);
await this.db.insert(cronJobConfigurations).values({
name,
cronExpression: configuration.cronExpression ?? job.cronExpression,
isEnabled: configuration.isEnabled ?? true,
});
logger.debug(`Cron job configuration updated name="${name}" configuration="${JSON.stringify(configuration)}"`);
}
public async getAllAsync(): Promise<
{ name: JobGroupKeys; cron: string; preventManualExecution: boolean; isEnabled: boolean }[]
> {
const configurations = await this.db.query.cronJobConfigurations.findMany();
return [...this.jobGroup.getJobRegistry().entries()].map(([name, job]) => {
const config = configurations.find((config) => config.name === name);
return {
name,
cron: config?.cronExpression ?? job.cronExpression,
preventManualExecution: job.preventManualExecution,
isEnabled: config?.isEnabled ?? true,
};
});
}
}
+2 -37
View File
@@ -1,45 +1,10 @@
// This import has to be the first import in the file so that the agent is overridden before any other modules are imported. // This import has to be the first import in the file so that the agent is overridden before any other modules are imported.
import "./undici-log-agent-override"; import "./undici-log-agent-override";
import type { FastifyTRPCPluginOptions } from "@trpc/server/adapters/fastify"; import { registerCronJobRunner } from "@homarr/cron-job-runner/register";
import { fastifyTRPCPlugin } from "@trpc/server/adapters/fastify";
import fastify from "fastify";
import type { JobRouter } from "@homarr/cron-job-api";
import { jobRouter } from "@homarr/cron-job-api";
import { CRON_JOB_API_KEY_HEADER, CRON_JOB_API_PATH, CRON_JOB_API_PORT } from "@homarr/cron-job-api/constants";
import { jobGroup } from "@homarr/cron-jobs"; import { jobGroup } from "@homarr/cron-jobs";
import { db } from "@homarr/db";
import { logger } from "@homarr/log";
import { JobManager } from "./job-manager";
const server = fastify({
maxParamLength: 5000,
});
server.register(fastifyTRPCPlugin, {
prefix: CRON_JOB_API_PATH,
trpcOptions: {
router: jobRouter,
createContext: ({ req }) => ({
manager: new JobManager(db, jobGroup),
apiKey: req.headers[CRON_JOB_API_KEY_HEADER] as string | undefined,
}),
onError({ path, error }) {
logger.error(new Error(`Error in tasks tRPC handler path="${path}"`, { cause: error }));
},
} satisfies FastifyTRPCPluginOptions<JobRouter>["trpcOptions"],
});
void (async () => { void (async () => {
await jobGroup.initializeAsync(); registerCronJobRunner();
await jobGroup.startAllAsync(); await jobGroup.startAllAsync();
try {
await server.listen({ port: CRON_JOB_API_PORT });
logger.info(`Tasks web server started successfully port="${CRON_JOB_API_PORT}"`);
} catch (err) {
logger.error(new Error(`Failed to start tasks web server port="${CRON_JOB_API_PORT}"`, { cause: err }));
process.exit(1);
}
})(); })();
+6 -6
View File
@@ -25,18 +25,18 @@
"@homarr/log": "workspace:^", "@homarr/log": "workspace:^",
"@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.0.1", "dotenv": "^16.5.0",
"tsx": "4.20.3", "tsx": "4.19.4",
"ws": "^8.18.3" "ws": "^8.18.2"
}, },
"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/ws": "^8.18.1", "@types/ws": "^8.18.1",
"esbuild": "^0.25.5", "esbuild": "^0.25.4",
"eslint": "^9.30.1", "eslint": "^9.27.0",
"prettier": "^3.6.2", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -3
View File
@@ -120,6 +120,4 @@ You can also support us by helping with [translating the entire project](https:/
Thanks to your generous sponsors we can continue to build Homarr. Check them out for high quality and easy to use development tools. Thanks to your generous sponsors we can continue to build Homarr. Check them out for high quality and easy to use development tools.
Feel free to contact us at homarr-labs@proton.me if you wish to become a sponsor. Feel free to contact us at homarr-labs@proton.me if you wish to become a sponsor.
[![Covered by Argos Visual Testing](https://argos-ci.com/badge-large.svg)](https://argos-ci.com?utm_source=%5Bhomarr%5D&utm_campaign=oss) \ [![Covered by Argos Visual Testing](https://argos-ci.com/badge-large.svg)](https://argos-ci.com?utm_source=%5Bhomarr%5D&utm_campaign=oss)
[![Supported by PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=homarr-v1)
+16 -16
View File
@@ -1,6 +1,6 @@
{ {
"name": "homarr", "name": "homarr",
"version": "1.27.0", "version": "1.21.0",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "cross-env CI=true turbo build", "build": "cross-env CI=true turbo build",
@@ -35,27 +35,27 @@
"@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.2",
"@semantic-release/npm": "^12.0.2", "@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.3", "@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.5.4", "@turbo/gen": "^2.5.3",
"@vitejs/plugin-react": "^4.6.0", "@vitejs/plugin-react": "^4.5.0",
"@vitest/coverage-v8": "^3.2.4", "@vitest/coverage-v8": "^3.1.4",
"@vitest/ui": "^3.2.4", "@vitest/ui": "^3.1.4",
"conventional-changelog-conventionalcommits": "^9.0.0", "conventional-changelog-conventionalcommits": "^8.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"jsdom": "^26.1.0", "jsdom": "^26.1.0",
"prettier": "^3.6.2", "prettier": "^3.5.3",
"semantic-release": "^24.2.6", "semantic-release": "^24.2.4",
"testcontainers": "^11.0.3", "testcontainers": "^10.28.0",
"turbo": "^2.5.4", "turbo": "^2.5.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4" "vitest": "^3.1.4"
}, },
"packageManager": "pnpm@10.12.4", "packageManager": "pnpm@10.11.0",
"engines": { "engines": {
"node": ">=22.17.0" "node": ">=22.16.0"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
@@ -70,7 +70,7 @@
"tree-sitter-json" "tree-sitter-json"
], ],
"overrides": { "overrides": {
"proxmox-api>undici": "7.11.0" "proxmox-api>undici": "7.10.0"
}, },
"allowUnusedPatches": true, "allowUnusedPatches": true,
"ignoredBuiltDependencies": [ "ignoredBuiltDependencies": [
+1 -1
View File
@@ -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.30.1", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+12 -12
View File
@@ -24,7 +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/cron-job-api": "workspace:^0.1.0", "@homarr/cron-job-runner": "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",
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
@@ -40,26 +40,26 @@
"@homarr/request-handler": "workspace:^0.1.0", "@homarr/request-handler": "workspace:^0.1.0",
"@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.2.0",
"@tanstack/react-query": "^5.81.5", "@tanstack/react-query": "^5.76.2",
"@trpc/client": "^11.4.3", "@trpc/client": "^11.1.2",
"@trpc/react-query": "^11.4.3", "@trpc/react-query": "^11.1.2",
"@trpc/server": "^11.4.3", "@trpc/server": "^11.1.2",
"@trpc/tanstack-react-query": "^11.4.3", "@trpc/tanstack-react-query": "^11.1.2",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"next": "15.3.5", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"trpc-to-openapi": "^2.3.2", "trpc-to-openapi": "^2.2.0",
"zod": "^3.25.74" "zod": "^3.25.23"
}, },
"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.30.1", "eslint": "^9.27.0",
"prettier": "^3.6.2", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+9 -42
View File
@@ -1,8 +1,7 @@
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import z from "zod/v4";
import { cronExpressionSchema, jobGroupKeys, jobNameSchema } from "@homarr/cron-job-api"; import { objectEntries } from "@homarr/common";
import { cronJobApi } from "@homarr/cron-job-api/client"; import { cronJobNames, cronJobs, jobNameSchema, triggerCronJobAsync } from "@homarr/cron-job-runner";
import type { TaskStatus } from "@homarr/cron-job-status"; import type { TaskStatus } from "@homarr/cron-job-status";
import { createCronJobStatusChannel } from "@homarr/cron-job-status"; import { createCronJobStatusChannel } from "@homarr/cron-job-status";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
@@ -14,51 +13,19 @@ export const cronJobsRouter = createTRPCRouter({
.requiresPermission("admin") .requiresPermission("admin")
.input(jobNameSchema) .input(jobNameSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
await cronJobApi.trigger.mutate(input); await triggerCronJobAsync(input);
}), }),
startJob: permissionRequiredProcedure getJobs: permissionRequiredProcedure.requiresPermission("admin").query(() => {
.requiresPermission("admin") return objectEntries(cronJobs).map(([name, options]) => ({
.input(jobNameSchema) name,
.mutation(async ({ input }) => { preventManualExecution: options.preventManualExecution,
await cronJobApi.start.mutate(input); }));
}),
stopJob: permissionRequiredProcedure
.requiresPermission("admin")
.input(jobNameSchema)
.mutation(async ({ input }) => {
await cronJobApi.stop.mutate(input);
}),
updateJobInterval: permissionRequiredProcedure
.requiresPermission("admin")
.input(
z.object({
name: jobNameSchema,
cron: cronExpressionSchema,
}),
)
.mutation(async ({ input }) => {
await cronJobApi.updateInterval.mutate(input);
}),
disableJob: permissionRequiredProcedure
.requiresPermission("admin")
.input(jobNameSchema)
.mutation(async ({ input }) => {
await cronJobApi.disable.mutate(input);
}),
enableJob: permissionRequiredProcedure
.requiresPermission("admin")
.input(jobNameSchema)
.mutation(async ({ input }) => {
await cronJobApi.enable.mutate(input);
}),
getJobs: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
return await cronJobApi.getAll.query();
}), }),
subscribeToStatusUpdates: permissionRequiredProcedure.requiresPermission("admin").subscription(() => { subscribeToStatusUpdates: permissionRequiredProcedure.requiresPermission("admin").subscription(() => {
return observable<TaskStatus>((emit) => { return observable<TaskStatus>((emit) => {
const unsubscribes: (() => void)[] = []; const unsubscribes: (() => void)[] = [];
for (const name of jobGroupKeys) { for (const name of cronJobNames) {
const channel = createCronJobStatusChannel(name); const channel = createCronJobStatusChannel(name);
const unsubscribe = channel.subscribe((data) => { const unsubscribe = channel.subscribe((data) => {
emit.next(data); emit.next(data);
@@ -36,7 +36,6 @@ const mapError = (error: Error): MappedError => {
export interface MappedCertificate { export interface MappedCertificate {
isSelfSigned: boolean; isSelfSigned: boolean;
issuer: string; issuer: string;
issuerCertificate?: MappedCertificate;
subject: string; subject: string;
serialNumber: string; serialNumber: string;
validFrom: Date; validFrom: Date;
@@ -48,7 +47,6 @@ export interface MappedCertificate {
const mapCertificate = (certificate: X509Certificate, code: RequestErrorCode): MappedCertificate => ({ const mapCertificate = (certificate: X509Certificate, code: RequestErrorCode): MappedCertificate => ({
isSelfSigned: certificate.ca || code === "DEPTH_ZERO_SELF_SIGNED_CERT", isSelfSigned: certificate.ca || code === "DEPTH_ZERO_SELF_SIGNED_CERT",
issuer: certificate.issuer, issuer: certificate.issuer,
issuerCertificate: certificate.issuerCertificate ? mapCertificate(certificate.issuerCertificate, code) : undefined,
subject: certificate.subject, subject: certificate.subject,
serialNumber: certificate.serialNumber, serialNumber: certificate.serialNumber,
validFrom: certificate.validFromDate, validFrom: certificate.validFromDate,
@@ -2,10 +2,9 @@ import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { asc, createId, eq, like } from "@homarr/db"; import { asc, createId, eq, like } from "@homarr/db";
import { getServerSettingByKeyAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries"; import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import { searchEngines, users } from "@homarr/db/schema"; import { searchEngines, users } from "@homarr/db/schema";
import { createIntegrationAsync } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations";
import { logger } from "@homarr/log";
import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common"; import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common";
import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine"; import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine";
import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request"; import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
@@ -97,35 +96,23 @@ export const searchEngineRouter = createTRPCRouter({
}); });
} }
const searchSettings = await getServerSettingByKeyAsync(ctx.db, "search"); const serverDefaultId = await getServerSettingByKeyAsync(ctx.db, "search").then(
(setting) => setting.defaultSearchEngineId,
);
if (!searchSettings.defaultSearchEngineId) return null; if (serverDefaultId) {
return await ctx.db.query.searchEngines.findFirst({
const serverDefault = await ctx.db.query.searchEngines.findFirst({ where: eq(searchEngines.id, serverDefaultId),
where: eq(searchEngines.id, searchSettings.defaultSearchEngineId), with: {
with: { integration: {
integration: { columns: {
columns: { kind: true,
kind: true, url: true,
url: true, id: true,
id: true, },
}, },
}, },
},
});
if (serverDefault) return serverDefault;
// Remove the default search engine ID from settings if it does not longer exist
try {
await updateServerSettingByKeyAsync(ctx.db, "search", {
...searchSettings,
defaultSearchEngineId: null,
}); });
} catch (error) {
logger.warn(
new Error("Failed to update search settings after default search engine not found", { cause: error }),
);
} }
return null; return null;
+4 -8
View File
@@ -19,11 +19,10 @@ const createDownloadClientIntegrationMiddleware = (action: IntegrationAction) =>
export const downloadsRouter = createTRPCRouter({ export const downloadsRouter = createTRPCRouter({
getJobsAndStatuses: publicProcedure getJobsAndStatuses: publicProcedure
.concat(createDownloadClientIntegrationMiddleware("query")) .concat(createDownloadClientIntegrationMiddleware("query"))
.input(z.object({ limitPerIntegration: z.number().default(50) })) .query(async ({ ctx }) => {
.query(async ({ ctx, input }) => {
return await Promise.all( return await Promise.all(
ctx.integrations.map(async (integration) => { ctx.integrations.map(async (integration) => {
const innerHandler = downloadClientRequestHandler.handler(integration, { limit: input.limitPerIntegration }); const innerHandler = downloadClientRequestHandler.handler(integration, {});
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
@@ -41,8 +40,7 @@ export const downloadsRouter = createTRPCRouter({
}), }),
subscribeToJobsAndStatuses: publicProcedure subscribeToJobsAndStatuses: publicProcedure
.concat(createDownloadClientIntegrationMiddleware("query")) .concat(createDownloadClientIntegrationMiddleware("query"))
.input(z.object({ limitPerIntegration: z.number().default(50) })) .subscription(({ ctx }) => {
.subscription(({ ctx, input }) => {
return observable<{ return observable<{
integration: Modify<Integration, { kind: IntegrationKindByCategory<"downloadClient"> }>; integration: Modify<Integration, { kind: IntegrationKindByCategory<"downloadClient"> }>;
data: DownloadClientJobsAndStatus; data: DownloadClientJobsAndStatus;
@@ -50,9 +48,7 @@ export const downloadsRouter = createTRPCRouter({
const unsubscribes: (() => void)[] = []; const unsubscribes: (() => void)[] = [];
for (const integrationWithSecrets of ctx.integrations) { for (const integrationWithSecrets of ctx.integrations) {
const { decryptedSecrets: _, ...integration } = integrationWithSecrets; const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
const innerHandler = downloadClientRequestHandler.handler(integrationWithSecrets, { const innerHandler = downloadClientRequestHandler.handler(integrationWithSecrets, {});
limit: input.limitPerIntegration,
});
const unsubscribe = innerHandler.subscribe((data) => { const unsubscribe = innerHandler.subscribe((data) => {
emit.next({ emit.next({
integration, integration,
@@ -1,6 +1,6 @@
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import type { SystemHealthMonitoring } from "@homarr/integrations"; import type { HealthMonitoring } from "@homarr/integrations";
import type { ProxmoxClusterInfo } from "@homarr/integrations/types"; import type { ProxmoxClusterInfo } from "@homarr/integrations/types";
import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring"; import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
@@ -9,7 +9,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
export const healthMonitoringRouter = createTRPCRouter({ export const healthMonitoringRouter = createTRPCRouter({
getSystemHealthStatus: publicProcedure getSystemHealthStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "mock")) .concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
.query(async ({ ctx }) => { .query(async ({ ctx }) => {
return await Promise.all( return await Promise.all(
ctx.integrations.map(async (integration) => { ctx.integrations.map(async (integration) => {
@@ -26,9 +26,9 @@ export const healthMonitoringRouter = createTRPCRouter({
); );
}), }),
subscribeSystemHealthStatus: publicProcedure subscribeSystemHealthStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "mock")) .concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
.subscription(({ ctx }) => { .subscription(({ ctx }) => {
return observable<{ integrationId: string; healthInfo: SystemHealthMonitoring; timestamp: Date }>((emit) => { return observable<{ integrationId: string; healthInfo: HealthMonitoring; timestamp: Date }>((emit) => {
const unsubscribes: (() => void)[] = []; const unsubscribes: (() => void)[] = [];
for (const integration of ctx.integrations) { for (const integration of ctx.integrations) {
const innerHandler = systemInfoRequestHandler.handler(integration, {}); const innerHandler = systemInfoRequestHandler.handler(integration, {});
@@ -49,14 +49,14 @@ export const healthMonitoringRouter = createTRPCRouter({
}); });
}), }),
getClusterHealthStatus: publicProcedure getClusterHealthStatus: publicProcedure
.concat(createOneIntegrationMiddleware("query", "proxmox", "mock")) .concat(createOneIntegrationMiddleware("query", "proxmox"))
.query(async ({ ctx }) => { .query(async ({ ctx }) => {
const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {}); const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {});
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return data; return data;
}), }),
subscribeClusterHealthStatus: publicProcedure subscribeClusterHealthStatus: publicProcedure
.concat(createOneIntegrationMiddleware("query", "proxmox", "mock")) .concat(createOneIntegrationMiddleware("query", "proxmox"))
.subscription(({ ctx }) => { .subscription(({ ctx }) => {
return observable<ProxmoxClusterInfo>((emit) => { return observable<ProxmoxClusterInfo>((emit) => {
const unsubscribes: (() => void)[] = []; const unsubscribes: (() => void)[] = [];
-2
View File
@@ -11,7 +11,6 @@ import { mediaTranscodingRouter } from "./media-transcoding";
import { minecraftRouter } from "./minecraft"; import { minecraftRouter } from "./minecraft";
import { networkControllerRouter } from "./network-controller"; import { networkControllerRouter } from "./network-controller";
import { notebookRouter } from "./notebook"; import { notebookRouter } from "./notebook";
import { notificationsRouter } from "./notifications";
import { optionsRouter } from "./options"; import { optionsRouter } from "./options";
import { releasesRouter } from "./releases"; import { releasesRouter } from "./releases";
import { rssFeedRouter } from "./rssFeed"; import { rssFeedRouter } from "./rssFeed";
@@ -38,5 +37,4 @@ export const widgetRouter = createTRPCRouter({
options: optionsRouter, options: optionsRouter,
releases: releasesRouter, releases: releasesRouter,
networkController: networkControllerRouter, networkController: networkControllerRouter,
notifications: notificationsRouter,
}); });
@@ -1,64 +0,0 @@
import { observable } from "@trpc/server/observable";
import type { Modify } from "@homarr/common/types";
import type { Integration } from "@homarr/db/schema";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import type { Notification } from "@homarr/integrations";
import { notificationsRequestHandler } from "@homarr/request-handler/notifications";
import type { IntegrationAction } from "../../middlewares/integration";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
const createNotificationsIntegrationMiddleware = (action: IntegrationAction) =>
createManyIntegrationMiddleware(action, ...getIntegrationKindsByCategory("notifications"));
export const notificationsRouter = createTRPCRouter({
getNotifications: publicProcedure
.unstable_concat(createNotificationsIntegrationMiddleware("query"))
.query(async ({ ctx }) => {
return await Promise.all(
ctx.integrations.map(async (integration) => {
const innerHandler = notificationsRequestHandler.handler(integration, {});
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return {
integration: {
id: integration.id,
name: integration.name,
kind: integration.kind,
updatedAt: timestamp,
},
data,
};
}),
);
}),
subscribeNotifications: publicProcedure
.unstable_concat(createNotificationsIntegrationMiddleware("query"))
.subscription(({ ctx }) => {
return observable<{
integration: Modify<Integration, { kind: IntegrationKindByCategory<"notifications"> }>;
data: Notification[];
}>((emit) => {
const unsubscribes: (() => void)[] = [];
for (const integrationWithSecrets of ctx.integrations) {
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
const innerHandler = notificationsRequestHandler.handler(integrationWithSecrets, {});
const unsubscribe = innerHandler.subscribe((data) => {
emit.next({
integration,
data,
});
});
unsubscribes.push(unsubscribe);
}
return () => {
unsubscribes.forEach((unsubscribe) => {
unsubscribe();
});
};
});
}),
});
+9 -9
View File
@@ -23,8 +23,8 @@
}, },
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@auth/core": "^0.40.0", "@auth/core": "^0.39.1",
"@auth/drizzle-adapter": "^1.10.0", "@auth/drizzle-adapter": "^1.9.1",
"@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/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
@@ -34,21 +34,21 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"cookies": "^0.9.1", "cookies": "^0.9.1",
"ldapts": "8.0.4", "ldapts": "8.0.0",
"next": "15.3.5", "next": "15.3.2",
"next-auth": "5.0.0-beta.29", "next-auth": "5.0.0-beta.28",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"zod": "^3.25.74" "zod": "^3.25.23"
}, },
"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/bcrypt": "5.0.2", "@types/bcrypt": "5.0.2",
"@types/cookies": "0.9.1", "@types/cookies": "0.9.0",
"eslint": "^9.30.1", "eslint": "^9.27.0",
"prettier": "^3.6.2", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
@@ -23,22 +23,21 @@ export type BoardPermissionsProps = (
export const constructBoardPermissions = (board: BoardPermissionsProps, session: Session | null) => { export const constructBoardPermissions = (board: BoardPermissionsProps, session: Session | null) => {
const creatorId = "creator" in board ? board.creator?.id : board.creatorId; const creatorId = "creator" in board ? board.creator?.id : board.creatorId;
const isCreator = session !== null && session.user.id === creatorId;
return { return {
hasFullAccess: hasFullAccess:
isCreator || session?.user.id === creatorId ||
board.userPermissions.some(({ permission }) => permission === "full") || board.userPermissions.some(({ permission }) => permission === "full") ||
board.groupPermissions.some(({ permission }) => permission === "full") || board.groupPermissions.some(({ permission }) => permission === "full") ||
(session?.user.permissions.includes("board-full-all") ?? false), (session?.user.permissions.includes("board-full-all") ?? false),
hasChangeAccess: hasChangeAccess:
isCreator || session?.user.id === creatorId ||
board.userPermissions.some(({ permission }) => permission === "modify" || permission === "full") || board.userPermissions.some(({ permission }) => permission === "modify" || permission === "full") ||
board.groupPermissions.some(({ permission }) => permission === "modify" || permission === "full") || board.groupPermissions.some(({ permission }) => permission === "modify" || permission === "full") ||
(session?.user.permissions.includes("board-modify-all") ?? false) || (session?.user.permissions.includes("board-modify-all") ?? false) ||
(session?.user.permissions.includes("board-full-all") ?? false), (session?.user.permissions.includes("board-full-all") ?? false),
hasViewAccess: hasViewAccess:
isCreator || session?.user.id === creatorId ||
board.userPermissions.length >= 1 || board.userPermissions.length >= 1 ||
board.groupPermissions.length >= 1 || board.groupPermissions.length >= 1 ||
board.isPublic || board.isPublic ||
@@ -286,22 +286,4 @@ describe("constructBoardPermissions", () => {
expect(result.hasChangeAccess).toBe(false); expect(result.hasChangeAccess).toBe(false);
expect(result.hasViewAccess).toBe(true); expect(result.hasViewAccess).toBe(true);
}); });
test("should return all false when creator is null and session is null", () => {
// Arrange
const board = {
creator: null,
userPermissions: [],
groupPermissions: [],
isPublic: false,
};
const session = null;
// Act
const result = constructBoardPermissions(board, session);
// Assert
expect(result.hasFullAccess).toBe(false);
expect(result.hasChangeAccess).toBe(false);
expect(result.hasViewAccess).toBe(false);
});
}); });
+1 -1
View File
@@ -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.30.1", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+2 -2
View File
@@ -24,13 +24,13 @@
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
"undici": "7.11.0" "undici": "7.10.0"
}, },
"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.30.1", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+3 -3
View File
@@ -28,14 +28,14 @@
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"dotenv": "^17.0.1" "dotenv": "^16.5.0"
}, },
"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",
"esbuild": "^0.25.5", "esbuild": "^0.25.4",
"eslint": "^9.30.1", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+5 -5
View File
@@ -30,18 +30,18 @@
"@homarr/env": "workspace:^0.1.0", "@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"next": "15.3.5", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"undici": "7.11.0", "undici": "7.10.0",
"zod": "^3.25.74", "zod": "^3.25.23",
"zod-validation-error": "^3.5.2" "zod-validation-error": "^3.4.1"
}, },
"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.30.1", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+2 -2
View File
@@ -1,10 +1,10 @@
import type { SerializeOptions } from "cookie"; import type { CookieSerializeOptions } from "cookie";
import { parse, serialize } from "cookie"; import { parse, serialize } from "cookie";
export function parseCookies(cookieString: string) { export function parseCookies(cookieString: string) {
return parse(cookieString); return parse(cookieString);
} }
export function setClientCookie(name: string, value: string, options: SerializeOptions = {}) { export function setClientCookie(name: string, value: string, options: CookieSerializeOptions = {}) {
document.cookie = serialize(name, value, options); document.cookie = serialize(name, value, options);
} }
@@ -36,12 +36,7 @@ export const requestErrorMap = {
expired: ["CERT_HAS_EXPIRED"], expired: ["CERT_HAS_EXPIRED"],
hostnameMismatch: ["ERR_TLS_CERT_ALTNAME_INVALID", "CERT_COMMON_NAME_INVALID"], hostnameMismatch: ["ERR_TLS_CERT_ALTNAME_INVALID", "CERT_COMMON_NAME_INVALID"],
notYetValid: ["CERT_NOT_YET_VALID"], notYetValid: ["CERT_NOT_YET_VALID"],
untrusted: [ untrusted: ["DEPTH_ZERO_SELF_SIGNED_CERT", "UNABLE_TO_VERIFY_LEAF_SIGNATURE", "UNABLE_TO_GET_ISSUER_CERT_LOCALLY"],
"DEPTH_ZERO_SELF_SIGNED_CERT",
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
"SELF_SIGNED_CERT_IN_CHAIN",
],
}, },
connection: { connection: {
hostUnreachable: "EHOSTUNREACH", hostUnreachable: "EHOSTUNREACH",
+1 -1
View File
@@ -24,7 +24,7 @@ export class LoggingAgent extends Agent {
url.searchParams.set(key, "REDACTED"); url.searchParams.set(key, "REDACTED");
}); });
logger.debug( logger.info(
`Dispatching request ${url.toString().replaceAll("=&", "&")} (${Object.keys(options.headers ?? {}).length} headers)`, `Dispatching request ${url.toString().replaceAll("=&", "&")} (${Object.keys(options.headers ?? {}).length} headers)`,
); );
return super.dispatch(options, handler); return super.dispatch(options, handler);
+2 -2
View File
@@ -10,11 +10,11 @@ const calculateTimeAgo = (timestamp: Date) => {
return dayjs().to(timestamp); return dayjs().to(timestamp);
}; };
export const useTimeAgo = (timestamp: Date, updateFrequency = 1000) => { export const useTimeAgo = (timestamp: Date) => {
const [timeAgo, setTimeAgo] = useState(calculateTimeAgo(timestamp)); const [timeAgo, setTimeAgo] = useState(calculateTimeAgo(timestamp));
useEffect(() => { useEffect(() => {
const intervalId = setInterval(() => setTimeAgo(calculateTimeAgo(timestamp)), updateFrequency); const intervalId = setInterval(() => setTimeAgo(calculateTimeAgo(timestamp)), 1000); // update every second
return () => clearInterval(intervalId); // clear interval on hook unmount return () => clearInterval(intervalId); // clear interval on hook unmount
}, [timestamp]); }, [timestamp]);
+4 -4
View File
@@ -21,7 +21,7 @@ const REDACTED = "REDACTED";
describe("LoggingAgent should log all requests", () => { describe("LoggingAgent should log all requests", () => {
test("should log all requests", () => { test("should log all requests", () => {
// Arrange // Arrange
const infoLogSpy = vi.spyOn(logger, "debug"); const infoLogSpy = vi.spyOn(logger, "info");
const agent = new LoggingAgent(); const agent = new LoggingAgent();
// Act // Act
@@ -33,7 +33,7 @@ describe("LoggingAgent should log all requests", () => {
test("should show amount of headers", () => { test("should show amount of headers", () => {
// Arrange // Arrange
const infoLogSpy = vi.spyOn(logger, "debug"); const infoLogSpy = vi.spyOn(logger, "info");
const agent = new LoggingAgent(); const agent = new LoggingAgent();
// Act // Act
@@ -68,7 +68,7 @@ describe("LoggingAgent should log all requests", () => {
[`/?stringWith13Chars=${"a".repeat(13)}`, `/?stringWith13Chars=${REDACTED}`], [`/?stringWith13Chars=${"a".repeat(13)}`, `/?stringWith13Chars=${REDACTED}`],
])("should redact sensitive data in url https://homarr.dev%s", (path, expected) => { ])("should redact sensitive data in url https://homarr.dev%s", (path, expected) => {
// Arrange // Arrange
const infoLogSpy = vi.spyOn(logger, "debug"); const infoLogSpy = vi.spyOn(logger, "info");
const agent = new LoggingAgent(); const agent = new LoggingAgent();
// Act // Act
@@ -87,7 +87,7 @@ describe("LoggingAgent should log all requests", () => {
["date times", "/?datetime=2022-01-01T00:00:00.000Z"], ["date times", "/?datetime=2022-01-01T00:00:00.000Z"],
])("should not redact values that are %s", (_reason, path) => { ])("should not redact values that are %s", (_reason, path) => {
// Arrange // Arrange
const infoLogSpy = vi.spyOn(logger, "debug"); const infoLogSpy = vi.spyOn(logger, "info");
const agent = new LoggingAgent(); const agent = new LoggingAgent();
// Act // Act
-20
View File
@@ -1,20 +0,0 @@
import { createTRPCClient, httpLink } from "@trpc/client";
import type { JobRouter } from ".";
import { CRON_JOB_API_KEY_HEADER, CRON_JOB_API_PATH, CRON_JOB_API_PORT } from "./constants";
import { env } from "./env";
export const cronJobApi = createTRPCClient<JobRouter>({
links: [
httpLink({
url: `${getBaseUrl()}${CRON_JOB_API_PATH}`,
headers: {
[CRON_JOB_API_KEY_HEADER]: env.CRON_JOB_API_KEY,
},
}),
],
});
function getBaseUrl() {
return `http://localhost:${CRON_JOB_API_PORT}`;
}
-3
View File
@@ -1,3 +0,0 @@
export const CRON_JOB_API_PORT = 3002;
export const CRON_JOB_API_PATH = "/trpc";
export const CRON_JOB_API_KEY_HEADER = "cron-job-api-key";
-11
View File
@@ -1,11 +0,0 @@
import { z } from "zod/v4";
import { env as commonEnv } from "@homarr/common/env";
import { createEnv } from "@homarr/env";
export const env = createEnv({
server: {
CRON_JOB_API_KEY: commonEnv.NODE_ENV === "development" ? z.string().default("test") : z.string(),
},
experimental__runtimeEnv: process.env,
});
-82
View File
@@ -1,82 +0,0 @@
import { initTRPC, TRPCError } from "@trpc/server";
import { validate } from "node-cron";
import { z } from "zod/v4";
import type { JobGroupKeys } from "@homarr/cron-jobs";
import { jobGroup } from "@homarr/cron-jobs";
import { env } from "./env";
export const jobGroupKeys = jobGroup.getKeys();
export const jobNameSchema = z.enum(jobGroup.getKeys());
export interface IJobManager {
startAsync(name: JobGroupKeys): Promise<void>;
triggerAsync(name: JobGroupKeys): Promise<void>;
stopAsync(name: JobGroupKeys): Promise<void>;
updateIntervalAsync(name: JobGroupKeys, cron: string): Promise<void>;
disableAsync(name: JobGroupKeys): Promise<void>;
enableAsync(name: JobGroupKeys): Promise<void>;
getAllAsync(): Promise<{ name: JobGroupKeys; cron: string; preventManualExecution: boolean; isEnabled: boolean }[]>;
}
const t = initTRPC
.context<{
manager: IJobManager;
apiKey?: string;
}>()
.create();
const createTrpcRouter = t.router;
const apiKeyProcedure = t.procedure.use(({ ctx, next }) => {
if (ctx.apiKey !== env.CRON_JOB_API_KEY) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Missing or invalid API key",
});
}
return next({
ctx: {
...ctx,
apiKey: undefined, // Clear the API key after checking
},
});
});
export const cronExpressionSchema = z.string().refine((expression) => validate(expression), {
error: "Invalid cron expression",
});
export const jobRouter = createTrpcRouter({
start: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
await ctx.manager.startAsync(input);
}),
trigger: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
await ctx.manager.triggerAsync(input);
}),
stop: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
await ctx.manager.stopAsync(input);
}),
updateInterval: apiKeyProcedure
.input(
z.object({
name: jobNameSchema,
cron: cronExpressionSchema,
}),
)
.mutation(async ({ input, ctx }) => {
await ctx.manager.updateIntervalAsync(input.name, input.cron);
}),
disable: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
await ctx.manager.disableAsync(input);
}),
enable: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
await ctx.manager.enableAsync(input);
}),
getAll: apiKeyProcedure.query(({ ctx }) => {
return ctx.manager.getAllAsync();
}),
});
export type JobRouter = typeof jobRouter;
+1
View File
@@ -0,0 +1 @@
export * from "./src";
@@ -1,14 +1,12 @@
{ {
"name": "@homarr/cron-job-api", "name": "@homarr/cron-job-runner",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.ts", ".": "./index.ts",
"./env": "./src/env.ts", "./register": "./src/register.ts"
"./constants": "./src/constants.ts",
"./client": "./src/client.ts"
}, },
"typesVersions": { "typesVersions": {
"*": { "*": {
@@ -27,23 +25,15 @@
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"@tanstack/react-query": "^5.81.5", "@homarr/redis": "workspace:^0.1.0",
"@trpc/client": "^11.4.3", "@homarr/validation": "workspace:^0.1.0"
"@trpc/server": "^11.4.3",
"@trpc/tanstack-react-query": "^11.4.3",
"node-cron": "^4.2.0",
"react": "19.1.0",
"zod": "^3.25.74"
}, },
"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", "eslint": "^9.27.0",
"@types/react": "19.1.8",
"eslint": "^9.30.1",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+44
View File
@@ -0,0 +1,44 @@
import { objectKeys } from "@homarr/common";
import type { JobGroupKeys } from "@homarr/cron-jobs";
import { createSubPubChannel } from "@homarr/redis";
import { zodEnumFromArray } from "@homarr/validation/enums";
export const cronJobRunnerChannel = createSubPubChannel<JobGroupKeys>("cron-job-runner", { persist: false });
export const cronJobs = {
analytics: { preventManualExecution: true },
iconsUpdater: { preventManualExecution: false },
ping: { preventManualExecution: false },
smartHomeEntityState: { preventManualExecution: false },
mediaServer: { preventManualExecution: false },
mediaOrganizer: { preventManualExecution: false },
downloads: { preventManualExecution: false },
dnsHole: { preventManualExecution: false },
mediaRequestStats: { preventManualExecution: false },
mediaRequestList: { preventManualExecution: false },
rssFeeds: { preventManualExecution: false },
indexerManager: { preventManualExecution: false },
healthMonitoring: { preventManualExecution: false },
sessionCleanup: { preventManualExecution: false },
updateChecker: { preventManualExecution: false },
mediaTranscoding: { preventManualExecution: false },
minecraftServerStatus: { preventManualExecution: false },
networkController: { preventManualExecution: false },
dockerContainers: { preventManualExecution: false },
} satisfies Record<JobGroupKeys, { preventManualExecution?: boolean }>;
/**
* Triggers a cron job to run immediately.
* This works over the Redis PubSub channel.
* @param jobName name of the job to be triggered
*/
export const triggerCronJobAsync = async (jobName: JobGroupKeys) => {
if (cronJobs[jobName].preventManualExecution) {
throw new Error(`The job "${jobName}" can not be executed manually`);
}
await cronJobRunnerChannel.publishAsync(jobName);
};
export const cronJobNames = objectKeys(cronJobs);
export const jobNameSchema = zodEnumFromArray(cronJobNames);
+12
View File
@@ -0,0 +1,12 @@
import { jobGroup } from "@homarr/cron-jobs";
import { cronJobRunnerChannel } from ".";
/**
* Registers the cron job runner to listen to the Redis PubSub channel.
*/
export const registerCronJobRunner = () => {
cronJobRunnerChannel.subscribe((jobName) => {
jobGroup.runManually(jobName);
});
};
File renamed without changes.
+1 -1
View File
@@ -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.30.1", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+2 -3
View File
@@ -25,15 +25,14 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0", "node-cron": "^3.0.3"
"node-cron": "^4.2.0"
}, },
"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",
"eslint": "^9.30.1", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+27 -37
View File
@@ -1,9 +1,8 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { createTask, validate } from "node-cron"; import cron from "node-cron";
import { Stopwatch } from "@homarr/common"; import { Stopwatch } from "@homarr/common";
import type { MaybePromise } from "@homarr/common/types"; import type { MaybePromise } from "@homarr/common/types";
import { db } from "@homarr/db";
import type { Logger } from "./logger"; import type { Logger } from "./logger";
import type { ValidateCron } from "./validation"; import type { ValidateCron } from "./validation";
@@ -18,18 +17,17 @@ export interface CreateCronJobCreatorOptions<TAllowedNames extends string> {
interface CreateCronJobOptions { interface CreateCronJobOptions {
runOnStart?: boolean; runOnStart?: boolean;
preventManualExecution?: boolean;
expectedMaximumDurationInMillis?: number; expectedMaximumDurationInMillis?: number;
beforeStart?: () => MaybePromise<void>; beforeStart?: () => MaybePromise<void>;
} }
const createCallback = <TAllowedNames extends string, TName extends TAllowedNames>( const createCallback = <TAllowedNames extends string, TName extends TAllowedNames>(
name: TName, name: TName,
defaultCronExpression: string, cronExpression: string,
options: CreateCronJobOptions, options: CreateCronJobOptions,
creatorOptions: CreateCronJobCreatorOptions<TAllowedNames>, creatorOptions: CreateCronJobCreatorOptions<TAllowedNames>,
) => { ) => {
const expectedMaximumDurationInMillis = options.expectedMaximumDurationInMillis ?? 2500; const expectedMaximumDurationInMillis = options.expectedMaximumDurationInMillis ?? 1000;
return (callback: () => MaybePromise<void>) => { return (callback: () => MaybePromise<void>) => {
const catchingCallbackAsync = async () => { const catchingCallbackAsync = async () => {
try { try {
@@ -64,30 +62,26 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
} }
}; };
/**
* We are not using the runOnInit method as we want to run the job only once we start the cron job schedule manually.
* This allows us to always run it once we start it. Additionally it will not run the callback if only the cron job file is imported.
*/
let scheduledTask: cron.ScheduledTask | null = null;
if (cronExpression !== "never") {
scheduledTask = cron.schedule(cronExpression, () => void catchingCallbackAsync(), {
scheduled: false,
name,
timezone: creatorOptions.timezone,
});
creatorOptions.logger.logDebug(
`The cron job '${name}' was created with expression ${cronExpression} in timezone ${creatorOptions.timezone} and runOnStart ${options.runOnStart}`,
);
}
return { return {
name, name,
cronExpression: defaultCronExpression, cronExpression,
async createTaskAsync() { scheduledTask,
const configuration = await db.query.cronJobConfigurations.findFirst({
where: (cronJobConfigurations, { eq }) => eq(cronJobConfigurations.name, name),
});
if (defaultCronExpression === "never") return null;
const scheduledTask = createTask(
configuration?.cronExpression ?? defaultCronExpression,
() => void catchingCallbackAsync(),
{
name,
timezone: creatorOptions.timezone,
},
);
creatorOptions.logger.logDebug(
`The cron job '${name}' was created with expression ${defaultCronExpression} in timezone ${creatorOptions.timezone} and runOnStart ${options.runOnStart}`,
);
return scheduledTask;
},
async onStartAsync() { async onStartAsync() {
if (options.beforeStart) { if (options.beforeStart) {
creatorOptions.logger.logDebug(`Running beforeStart for job: ${name}`); creatorOptions.logger.logDebug(`Running beforeStart for job: ${name}`);
@@ -99,10 +93,6 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
creatorOptions.logger.logDebug(`The cron job '${name}' is running because runOnStart is set to true`); creatorOptions.logger.logDebug(`The cron job '${name}' is running because runOnStart is set to true`);
await catchingCallbackAsync(); await catchingCallbackAsync();
}, },
async executeAsync() {
await catchingCallbackAsync();
},
preventManualExecution: options.preventManualExecution ?? false,
}; };
}; };
}; };
@@ -116,17 +106,17 @@ export const createCronJobCreator = <TAllowedNames extends string = string>(
) => { ) => {
return <TName extends TAllowedNames, TExpression extends string>( return <TName extends TAllowedNames, TExpression extends string>(
name: TName, name: TName,
defaultCronExpression: TExpression, cronExpression: TExpression,
options: CreateCronJobOptions = { runOnStart: false }, options: CreateCronJobOptions = { runOnStart: false },
) => { ) => {
creatorOptions.logger.logDebug(`Validating cron expression '${defaultCronExpression}' for job: ${name}`); creatorOptions.logger.logDebug(`Validating cron expression '${cronExpression}' for job: ${name}`);
if (defaultCronExpression !== "never" && !validate(defaultCronExpression)) { if (cronExpression !== "never" && !cron.validate(cronExpression)) {
throw new Error(`Invalid cron expression '${defaultCronExpression}' for job '${name}'`); throw new Error(`Invalid cron expression '${cronExpression}' for job '${name}'`);
} }
creatorOptions.logger.logDebug(`Cron job expression '${defaultCronExpression}' for job ${name} is valid`); creatorOptions.logger.logDebug(`Cron job expression '${cronExpression}' for job ${name} is valid`);
const returnValue = { const returnValue = {
withCallback: createCallback<TAllowedNames, TName>(name, defaultCronExpression, options, creatorOptions), withCallback: createCallback<TAllowedNames, TName>(name, cronExpression, options, creatorOptions),
}; };
// This is a type guard to check if the cron expression is valid and give the user a type hint // This is a type guard to check if the cron expression is valid and give the user a type hint
+8 -44
View File
@@ -1,7 +1,4 @@
import type { ScheduledTask } from "node-cron";
import { objectEntries, objectKeys } from "@homarr/common"; import { objectEntries, objectKeys } from "@homarr/common";
import { db } from "@homarr/db";
import type { JobCallback } from "./creator"; import type { JobCallback } from "./creator";
import type { Logger } from "./logger"; import type { Logger } from "./logger";
@@ -30,78 +27,45 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
}); });
} }
const tasks = new Map<string, ScheduledTask>();
return { return {
initializeAsync: async () => {
const configurations = await db.query.cronJobConfigurations.findMany();
for (const job of jobRegistry.values()) {
const configuration = configurations.find(({ name }) => name === job.name);
if (configuration?.isEnabled === false) {
continue;
}
if (tasks.has(job.name)) {
continue;
}
const scheduledTask = await job.createTaskAsync();
if (!scheduledTask) continue;
tasks.set(job.name, scheduledTask);
}
},
startAsync: async (name: keyof TJobs) => { startAsync: async (name: keyof TJobs) => {
const job = jobRegistry.get(name as string); const job = jobRegistry.get(name as string);
if (!job) return; if (!job) return;
if (!tasks.has(job.name)) return;
options.logger.logInfo(`Starting schedule cron job ${job.name}.`); options.logger.logInfo(`Starting schedule cron job ${job.name}.`);
await job.onStartAsync(); await job.onStartAsync();
await tasks.get(name as string)?.start(); job.scheduledTask?.start();
}, },
startAllAsync: async () => { startAllAsync: async () => {
for (const job of jobRegistry.values()) { for (const job of jobRegistry.values()) {
if (!tasks.has(job.name)) {
continue;
}
options.logger.logInfo(`Starting schedule of cron job ${job.name}.`); options.logger.logInfo(`Starting schedule of cron job ${job.name}.`);
await job.onStartAsync(); await job.onStartAsync();
await tasks.get(job.name)?.start(); job.scheduledTask?.start();
} }
}, },
runManuallyAsync: async (name: keyof TJobs) => { runManually: (name: keyof TJobs) => {
const job = jobRegistry.get(name as string); const job = jobRegistry.get(name as string);
if (!job) return; if (!job) return;
if (job.preventManualExecution) {
throw new Error(`The job "${job.name}" can not be executed manually.`);
}
options.logger.logInfo(`Running schedule cron job ${job.name} manually.`); options.logger.logInfo(`Running schedule cron job ${job.name} manually.`);
await tasks.get(name as string)?.execute(); job.scheduledTask?.now();
}, },
stopAsync: async (name: keyof TJobs) => { stop: (name: keyof TJobs) => {
const job = jobRegistry.get(name as string); const job = jobRegistry.get(name as string);
if (!job) return; if (!job) return;
options.logger.logInfo(`Stopping schedule cron job ${job.name}.`); options.logger.logInfo(`Stopping schedule cron job ${job.name}.`);
await tasks.get(name as string)?.stop(); job.scheduledTask?.stop();
}, },
stopAllAsync: async () => { stopAll: () => {
for (const job of jobRegistry.values()) { for (const job of jobRegistry.values()) {
options.logger.logInfo(`Stopping schedule cron job ${job.name}.`); options.logger.logInfo(`Stopping schedule cron job ${job.name}.`);
await tasks.get(job.name)?.stop(); job.scheduledTask?.stop();
} }
}, },
getJobRegistry() { getJobRegistry() {
return jobRegistry as Map<TAllowedNames, ReturnType<JobCallback<TAllowedNames, TAllowedNames>>>; return jobRegistry as Map<TAllowedNames, ReturnType<JobCallback<TAllowedNames, TAllowedNames>>>;
}, },
getTask(name: keyof TJobs) {
return tasks.get(name as string) ?? null;
},
setTask(name: keyof TJobs, task: ScheduledTask) {
tasks.set(name as string, task);
},
getKeys() { getKeys() {
return objectKeys(jobs); return objectKeys(jobs);
}, },
+1 -1
View File
@@ -44,7 +44,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.30.1", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
-2
View File
@@ -11,7 +11,6 @@ import { mediaRequestListJob, mediaRequestStatsJob } from "./jobs/integrations/m
import { mediaServerJob } from "./jobs/integrations/media-server"; import { mediaServerJob } from "./jobs/integrations/media-server";
import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding"; import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding";
import { networkControllerJob } from "./jobs/integrations/network-controller"; import { networkControllerJob } from "./jobs/integrations/network-controller";
import { refreshNotificationsJob } from "./jobs/integrations/notifications";
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status"; import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
import { pingJob } from "./jobs/ping"; import { pingJob } from "./jobs/ping";
import { rssFeedsJob } from "./jobs/rss-feeds"; import { rssFeedsJob } from "./jobs/rss-feeds";
@@ -39,7 +38,6 @@ export const jobGroup = createCronJobGroup({
minecraftServerStatus: minecraftServerStatusJob, minecraftServerStatus: minecraftServerStatusJob,
dockerContainers: dockerContainersJob, dockerContainers: dockerContainersJob,
networkController: networkControllerJob, networkController: networkControllerJob,
refreshNotifications: refreshNotificationsJob,
}); });
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number]; export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
-1
View File
@@ -7,7 +7,6 @@ import { createCronJob } from "../lib";
export const analyticsJob = createCronJob("analytics", EVERY_WEEK, { export const analyticsJob = createCronJob("analytics", EVERY_WEEK, {
runOnStart: true, runOnStart: true,
preventManualExecution: true,
}).withCallback(async () => { }).withCallback(async () => {
const analyticSetting = await getServerSettingByKeyAsync(db, "analytics"); const analyticSetting = await getServerSettingByKeyAsync(db, "analytics");
+8 -7
View File
@@ -38,7 +38,6 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
const newIconRepositories: InferInsertModel<typeof iconRepositories>[] = []; const newIconRepositories: InferInsertModel<typeof iconRepositories>[] = [];
const newIcons: InferInsertModel<typeof icons>[] = []; const newIcons: InferInsertModel<typeof icons>[] = [];
const allDbIcons = databaseIconRepositories.flatMap((group) => group.icons);
for (const repositoryIconGroup of repositoryIconGroups) { for (const repositoryIconGroup of repositoryIconGroups) {
if (!repositoryIconGroup.success) { if (!repositoryIconGroup.success) {
@@ -56,10 +55,12 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
}); });
} }
const dbIconsInRepository = allDbIcons.filter((icon) => icon.iconRepositoryId === iconRepositoryId);
for (const icon of repositoryIconGroup.icons) { for (const icon of repositoryIconGroup.icons) {
if (dbIconsInRepository.some((dbIcon) => dbIcon.checksum === icon.checksum)) { if (
databaseIconRepositories
.flatMap((repository) => repository.icons)
.some((dbIcon) => dbIcon.checksum === icon.checksum && dbIcon.iconRepositoryId === iconRepositoryId)
) {
skippedChecksums.push(`${iconRepositoryId}.${icon.checksum}`); skippedChecksums.push(`${iconRepositoryId}.${icon.checksum}`);
continue; continue;
} }
@@ -75,9 +76,9 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
} }
} }
const deadIcons = allDbIcons.filter( const deadIcons = databaseIconRepositories
(icon) => !skippedChecksums.includes(`${icon.iconRepositoryId}.${icon.checksum}`), .flatMap((repository) => repository.icons)
); .filter((icon) => !skippedChecksums.includes(`${icon.iconRepositoryId}.${icon.checksum}`));
const deadIconRepositories = databaseIconRepositories.filter( const deadIconRepositories = databaseIconRepositories.filter(
(iconRepository) => !repositoryIconGroups.some((group) => group.slug === iconRepository.slug), (iconRepository) => !repositoryIconGroups.some((group) => group.slug === iconRepository.slug),
@@ -1,10 +1,10 @@
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions";
import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole"; import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole";
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
import { createCronJob } from "../../lib"; import { createCronJob } from "../../lib";
export const dnsHoleJob = createCronJob("dnsHole", EVERY_MINUTE).withCallback( export const dnsHoleJob = createCronJob("dnsHole", EVERY_5_SECONDS).withCallback(
createRequestIntegrationJobHandler(dnsHoleRequestHandler.handler, { createRequestIntegrationJobHandler(dnsHoleRequestHandler.handler, {
widgetKinds: ["dnsHoleSummary", "dnsHoleControls"], widgetKinds: ["dnsHoleSummary", "dnsHoleControls"],
getInput: { getInput: {
@@ -8,9 +8,7 @@ export const downloadsJob = createCronJob("downloads", EVERY_5_SECONDS).withCall
createRequestIntegrationJobHandler(downloadClientRequestHandler.handler, { createRequestIntegrationJobHandler(downloadClientRequestHandler.handler, {
widgetKinds: ["downloads"], widgetKinds: ["downloads"],
getInput: { getInput: {
downloads: (options) => ({ downloads: () => ({}),
limit: options.limitPerIntegration,
}),
}, },
}), }),
); );
@@ -8,8 +8,7 @@ export const healthMonitoringJob = createCronJob("healthMonitoring", EVERY_5_SEC
createRequestIntegrationJobHandler( createRequestIntegrationJobHandler(
(integration, itemOptions: Record<string, never>) => { (integration, itemOptions: Record<string, never>) => {
const { kind } = integration; const { kind } = integration;
if (kind !== "proxmox") {
if (kind !== "proxmox" && kind !== "mock") {
return systemInfoRequestHandler.handler({ ...integration, kind }, itemOptions); return systemInfoRequestHandler.handler({ ...integration, kind }, itemOptions);
} }
return clusterInfoRequestHandler.handler({ ...integration, kind }, itemOptions); return clusterInfoRequestHandler.handler({ ...integration, kind }, itemOptions);
@@ -1,14 +0,0 @@
import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions";
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
import { notificationsRequestHandler } from "@homarr/request-handler/notifications";
import { createCronJob } from "../../lib";
export const refreshNotificationsJob = createCronJob("refreshNotifications", EVERY_5_MINUTES).withCallback(
createRequestIntegrationJobHandler(notificationsRequestHandler.handler, {
widgetKinds: ["notifications"],
getInput: {
notifications: (options) => options,
},
}),
);
+2 -2
View File
@@ -1,6 +1,6 @@
import SuperJSON from "superjson"; import SuperJSON from "superjson";
import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions"; import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions";
import { db, eq } from "@homarr/db"; import { db, eq } from "@homarr/db";
import { items } from "@homarr/db/schema"; import { items } from "@homarr/db/schema";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
@@ -10,7 +10,7 @@ import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
import type { WidgetComponentProps } from "../../../widgets"; import type { WidgetComponentProps } from "../../../widgets";
import { createCronJob } from "../lib"; import { createCronJob } from "../lib";
export const rssFeedsJob = createCronJob("rssFeeds", EVERY_10_MINUTES).withCallback(async () => { export const rssFeedsJob = createCronJob("rssFeeds", EVERY_5_MINUTES).withCallback(async () => {
const rssItems = await db.query.items.findMany({ const rssItems = await db.query.items.findMany({
where: eq(items.kind, "rssFeed"), where: eq(items.kind, "rssFeed"),
}); });
@@ -1,6 +0,0 @@
CREATE TABLE `cron_job_configuration` (
`name` varchar(256) NOT NULL,
`cron_expression` varchar(32) NOT NULL,
`is_enabled` boolean NOT NULL DEFAULT true,
CONSTRAINT `cron_job_configuration_name` PRIMARY KEY(`name`)
);
File diff suppressed because it is too large. Load diff
@@ -232,13 +232,6 @@
"when": 1746821770071, "when": 1746821770071,
"tag": "0032_add_trusted_certificate_hostnames", "tag": "0032_add_trusted_certificate_hostnames",
"breakpoints": true "breakpoints": true
},
{
"idx": 33,
"version": "5",
"when": 1750013953833,
"tag": "0033_add_cron_job_configuration",
"breakpoints": true
} }
] ]
} }
@@ -1,5 +0,0 @@
CREATE TABLE `cron_job_configuration` (
`name` text PRIMARY KEY NOT NULL,
`cron_expression` text NOT NULL,
`is_enabled` integer DEFAULT true NOT NULL
);
File diff suppressed because it is too large. Load diff
@@ -232,13 +232,6 @@
"when": 1746821779051, "when": 1746821779051,
"tag": "0032_add_trusted_certificate_hostnames", "tag": "0032_add_trusted_certificate_hostnames",
"breakpoints": true "breakpoints": true
},
{
"idx": 33,
"version": "6",
"when": 1750014001941,
"tag": "0033_add_cron_job_configuration",
"breakpoints": true
} }
] ]
} }
+11 -11
View File
@@ -38,19 +38,19 @@
}, },
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@auth/core": "^0.40.0", "@auth/core": "^0.39.1",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0", "@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^8.1.2", "@mantine/core": "^8.0.1",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^11.0.3", "@testcontainers/mysql": "^10.28.0",
"better-sqlite3": "^12.2.0", "better-sqlite3": "^11.10.0",
"dotenv": "^17.0.1", "dotenv": "^16.5.0",
"drizzle-kit": "^0.31.4", "drizzle-kit": "^0.31.1",
"drizzle-orm": "^0.44.2", "drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.7.1", "drizzle-zod": "^0.7.1",
"mysql2": "3.14.1" "mysql2": "3.14.1"
}, },
@@ -60,10 +60,10 @@
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"esbuild": "^0.25.5", "esbuild": "^0.25.4",
"eslint": "^9.30.1", "eslint": "^9.27.0",
"prettier": "^3.6.2", "prettier": "^3.5.3",
"tsx": "4.20.3", "tsx": "4.19.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
-1
View File
@@ -40,7 +40,6 @@ export const {
itemLayouts, itemLayouts,
sectionLayouts, sectionLayouts,
trustedCertificateHostnames, trustedCertificateHostnames,
cronJobConfigurations,
} = schema; } = schema;
export type User = InferSelectModel<typeof schema.users>; export type User = InferSelectModel<typeof schema.users>;
-6
View File
@@ -508,12 +508,6 @@ export const trustedCertificateHostnames = mysqlTable(
}), }),
); );
export const cronJobConfigurations = mysqlTable("cron_job_configuration", {
name: varchar({ length: 256 }).notNull().primaryKey(),
cronExpression: varchar({ length: 32 }).notNull(),
isEnabled: boolean().default(true).notNull(),
});
export const accountRelations = relations(accounts, ({ one }) => ({ export const accountRelations = relations(accounts, ({ one }) => ({
user: one(users, { user: one(users, {
fields: [accounts.userId], fields: [accounts.userId],
-6
View File
@@ -493,12 +493,6 @@ export const trustedCertificateHostnames = sqliteTable(
}), }),
); );
export const cronJobConfigurations = sqliteTable("cron_job_configuration", {
name: text().notNull().primaryKey(),
cronExpression: text().notNull(),
isEnabled: int({ mode: "boolean" }).default(true).notNull(),
});
export const accountRelations = relations(accounts, ({ one }) => ({ export const accountRelations = relations(accounts, ({ one }) => ({
user: one(users, { user: one(users, {
fields: [accounts.userId], fields: [accounts.userId],
+1 -1
View File
@@ -9,7 +9,7 @@ import * as mysqlSchema from "../schema/mysql";
describe("Mysql Migration", () => { describe("Mysql Migration", () => {
test("should add all tables and keys specified in migration files", async () => { test("should add all tables and keys specified in migration files", async () => {
const mysqlContainer = await new MySqlContainer("mysql:latest").start(); const mysqlContainer = await new MySqlContainer().start();
const connection = mysql.createConnection({ const connection = mysql.createConnection({
host: mysqlContainer.getHost(), host: mysqlContainer.getHost(),
+4 -4
View File
@@ -24,15 +24,15 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "^5.2.5", "fast-xml-parser": "^5.2.3",
"zod": "^3.25.74" "zod": "^3.25.23"
}, },
"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.30.1", "eslint": "^9.27.0",
"tsx": "4.20.3", "tsx": "4.19.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -28
View File
@@ -7,7 +7,6 @@ export const integrationSecretKindObject = {
password: { isPublic: false }, password: { isPublic: false },
tokenId: { isPublic: true }, tokenId: { isPublic: true },
realm: { isPublic: true }, realm: { isPublic: true },
topic: { isPublic: true },
} satisfies Record<string, { isPublic: boolean }>; } satisfies Record<string, { isPublic: boolean }>;
export const integrationSecretKinds = objectKeys(integrationSecretKindObject); export const integrationSecretKinds = objectKeys(integrationSecretKindObject);
@@ -170,31 +169,6 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png", iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png",
category: ["networkController"], category: ["networkController"],
}, },
ntfy: {
name: "ntfy",
secretKinds: [["topic"], ["topic", "apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/ntfy.svg",
category: ["notifications"],
},
// This integration only returns mock data, it is used during development (but can also be used in production by directly going to the create page)
mock: {
name: "Mock",
secretKinds: [[]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/vitest.svg",
category: [
"calendar",
"dnsHole",
"downloadClient",
"healthMonitoring",
"indexerManager",
"mediaRequest",
"mediaService",
"mediaTranscoding",
"networkController",
"notifications",
"smartHomeServer",
],
},
} as const satisfies Record<string, integrationDefinition>; } as const satisfies Record<string, integrationDefinition>;
export const integrationKinds = objectKeys(integrationDefs) as AtLeastOneOf<IntegrationKind>; export const integrationKinds = objectKeys(integrationDefs) as AtLeastOneOf<IntegrationKind>;
@@ -249,5 +223,4 @@ export type IntegrationCategory =
| "healthMonitoring" | "healthMonitoring"
| "search" | "search"
| "mediaTranscoding" | "mediaTranscoding"
| "networkController" | "networkController";
| "notifications";
-1
View File
@@ -25,6 +25,5 @@ export const widgetKinds = [
"healthMonitoring", "healthMonitoring",
"releases", "releases",
"dockerContainers", "dockerContainers",
"notifications",
] as const; ] as const;
export type WidgetKind = (typeof widgetKinds)[number]; export type WidgetKind = (typeof widgetKinds)[number];
+3 -3
View File
@@ -26,14 +26,14 @@
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0", "@homarr/env": "workspace:^0.1.0",
"dockerode": "^4.0.7" "dockerode": "^4.0.6"
}, },
"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/dockerode": "^3.3.42", "@types/dockerode": "^3.3.39",
"eslint": "^9.30.1", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+3 -3
View File
@@ -23,14 +23,14 @@
}, },
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@t3-oss/env-nextjs": "^0.13.8", "@t3-oss/env-nextjs": "^0.13.4",
"zod": "^3.25.74" "zod": "^3.25.23"
}, },
"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.30.1", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+3 -3
View File
@@ -26,14 +26,14 @@
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^8.1.2", "@mantine/form": "^8.0.1",
"zod": "^3.25.74" "zod": "^3.25.23"
}, },
"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.30.1", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+3 -3
View File
@@ -29,15 +29,15 @@
"@homarr/notifications": "workspace:^0.1.0", "@homarr/notifications": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.1.2", "@mantine/core": "^8.0.1",
"react": "19.1.0", "react": "19.1.0",
"zod": "^3.25.74" "zod": "^3.25.23"
}, },
"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.30.1", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -1
View File
@@ -31,7 +31,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.30.1", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+5 -5
View File
@@ -38,13 +38,13 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@jellyfin/sdk": "^0.11.0", "@jellyfin/sdk": "^0.11.0",
"maria2": "^0.4.1", "maria2": "^0.4.0",
"node-ical": "^0.20.1", "node-ical": "^0.20.1",
"proxmox-api": "1.1.1", "proxmox-api": "1.1.1",
"tsdav": "^2.1.5", "tsdav": "^2.1.4",
"undici": "7.11.0", "undici": "7.10.0",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"zod": "^3.25.74" "zod": "^3.25.23"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
@@ -52,7 +52,7 @@
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-unifi": "^2.5.1", "@types/node-unifi": "^2.5.1",
"@types/xml2js": "^0.4.14", "@types/xml2js": "^0.4.14",
"eslint": "^9.30.1", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
@@ -20,9 +20,7 @@ import { RadarrIntegration } from "../media-organizer/radarr/radarr-integration"
import { ReadarrIntegration } from "../media-organizer/readarr/readarr-integration"; import { ReadarrIntegration } from "../media-organizer/readarr/readarr-integration";
import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration"; import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration";
import { TdarrIntegration } from "../media-transcoding/tdarr-integration"; import { TdarrIntegration } from "../media-transcoding/tdarr-integration";
import { MockIntegration } from "../mock/mock-integration";
import { NextcloudIntegration } from "../nextcloud/nextcloud.integration"; import { NextcloudIntegration } from "../nextcloud/nextcloud.integration";
import { NTFYIntegration } from "../ntfy/ntfy-integration";
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration"; import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
import { OverseerrIntegration } from "../overseerr/overseerr-integration"; import { OverseerrIntegration } from "../overseerr/overseerr-integration";
import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-factory"; import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-factory";
@@ -94,8 +92,6 @@ export const integrationCreators = {
emby: EmbyIntegration, emby: EmbyIntegration,
nextcloud: NextcloudIntegration, nextcloud: NextcloudIntegration,
unifiController: UnifiControllerIntegration, unifiController: UnifiControllerIntegration,
ntfy: NTFYIntegration,
mock: MockIntegration,
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>; } satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = { type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
@@ -12,10 +12,9 @@ import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration"; import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error"; import { TestConnectionError } from "../base/test-connection/test-connection-error";
import type { TestingResult } from "../base/test-connection/test-connection-service"; import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-monitoring/health-monitoring-integration"; import type { HealthMonitoring } from "../types";
import type { SystemHealthMonitoring } from "../interfaces/health-monitoring/health-monitoring-types";
export class DashDotIntegration extends Integration implements ISystemHealthMonitoringIntegration { export class DashDotIntegration extends Integration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> { protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/info")); const response = await input.fetchAsync(this.url("/info"));
if (!response.ok) return TestConnectionError.StatusResult(response); if (!response.ok) return TestConnectionError.StatusResult(response);
@@ -27,7 +26,7 @@ export class DashDotIntegration extends Integration implements ISystemHealthMoni
}; };
} }
public async getSystemInfoAsync(): Promise<SystemHealthMonitoring> { public async getSystemInfoAsync(): Promise<HealthMonitoring> {
const info = await this.getInfoAsync(); const info = await this.getInfoAsync();
const cpuLoad = await this.getCurrentCpuLoadAsync(); const cpuLoad = await this.getCurrentCpuLoadAsync();
const memoryLoad = await this.getCurrentMemoryLoadAsync(); const memoryLoad = await this.getCurrentMemoryLoadAsync();
@@ -4,16 +4,15 @@ import type { fetch as undiciFetch } from "undici";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { ResponseError } from "@homarr/common/server"; import { ResponseError } from "@homarr/common/server";
import { Integration } from "../../base/integration";
import type { IntegrationTestingInput } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration";
import type { TestingResult } from "../../base/test-connection/test-connection-service"; import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data"; import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items"; import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
import type { Aria2Download, Aria2GetClient } from "./aria2-types"; import type { Aria2Download, Aria2GetClient } from "./aria2-types";
export class Aria2Integration extends Integration implements IDownloadClientIntegration { export class Aria2Integration extends DownloadClientIntegration {
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> { public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
const client = this.getClient(); const client = this.getClient();
const keys: (keyof Aria2Download)[] = [ const keys: (keyof Aria2Download)[] = [
"bittorrent", "bittorrent",
@@ -28,12 +27,12 @@ export class Aria2Integration extends Integration implements IDownloadClientInte
]; ];
const [activeDownloads, waitingDownloads, stoppedDownloads, globalStats] = await Promise.all([ const [activeDownloads, waitingDownloads, stoppedDownloads, globalStats] = await Promise.all([
client.tellActive(), client.tellActive(),
client.tellWaiting(0, input.limit, keys), client.tellWaiting(0, 1000, keys),
client.tellStopped(0, input.limit, keys), client.tellStopped(0, 1000, keys),
client.getGlobalStat(), client.getGlobalStat(),
]); ]);
const downloads = [...activeDownloads, ...waitingDownloads, ...stoppedDownloads].slice(0, input.limit); const downloads = [...activeDownloads, ...waitingDownloads, ...stoppedDownloads];
const allPaused = downloads.every((download) => download.status === "paused"); const allPaused = downloads.every((download) => download.status === "paused");
return { return {
@@ -6,17 +6,16 @@ import { createCertificateAgentAsync } from "@homarr/certificates/server";
import { HandleIntegrationErrors } from "../../base/errors/decorator"; import { HandleIntegrationErrors } from "../../base/errors/decorator";
import { integrationOFetchHttpErrorHandler } from "../../base/errors/http"; import { integrationOFetchHttpErrorHandler } from "../../base/errors/http";
import { Integration } from "../../base/integration";
import type { IntegrationTestingInput } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration";
import { TestConnectionError } from "../../base/test-connection/test-connection-error"; import { TestConnectionError } from "../../base/test-connection/test-connection-error";
import type { TestingResult } from "../../base/test-connection/test-connection-service"; import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data"; import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items"; import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status"; import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
@HandleIntegrationErrors([integrationOFetchHttpErrorHandler]) @HandleIntegrationErrors([integrationOFetchHttpErrorHandler])
export class DelugeIntegration extends Integration implements IDownloadClientIntegration { export class DelugeIntegration extends DownloadClientIntegration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> { protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const client = await this.getClientAsync(input.dispatcher); const client = await this.getClientAsync(input.dispatcher);
const isSuccess = await client.login(); const isSuccess = await client.login();
@@ -30,10 +29,9 @@ export class DelugeIntegration extends Integration implements IDownloadClientInt
}; };
} }
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> { public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
const type = "torrent"; const type = "torrent";
const client = await this.getClientAsync(); const client = await this.getClientAsync();
// Currently there is no way to limit the number of returned torrents
const { const {
stats: { download_rate, upload_rate }, stats: { download_rate, upload_rate },
torrents: rawTorrents, torrents: rawTorrents,
@@ -51,29 +49,27 @@ export class DelugeIntegration extends Integration implements IDownloadClientInt
}, },
types: [type], types: [type],
}; };
const items = torrents const items = torrents.map((torrent): DownloadClientItem => {
.map((torrent): DownloadClientItem => { const state = DelugeIntegration.getTorrentState(torrent.state);
const state = DelugeIntegration.getTorrentState(torrent.state); return {
return { type,
type, id: torrent.id,
id: torrent.id, index: torrent.queue,
index: torrent.queue, name: torrent.name,
name: torrent.name, size: torrent.total_wanted,
size: torrent.total_wanted, sent: torrent.total_uploaded,
sent: torrent.total_uploaded, downSpeed: torrent.progress !== 100 ? torrent.download_payload_rate : undefined,
downSpeed: torrent.progress !== 100 ? torrent.download_payload_rate : undefined, upSpeed: torrent.upload_payload_rate,
upSpeed: torrent.upload_payload_rate, time:
time: torrent.progress === 100
torrent.progress === 100 ? Math.min((torrent.completed_time - dayjs().unix()) * 1000, -1)
? Math.min((torrent.completed_time - dayjs().unix()) * 1000, -1) : Math.max(torrent.eta * 1000, 0),
: Math.max(torrent.eta * 1000, 0), added: torrent.time_added * 1000,
added: torrent.time_added * 1000, state,
state, progress: torrent.progress / 100,
progress: torrent.progress / 100, category: torrent.label,
category: torrent.label, };
}; });
})
.slice(0, input.limit);
return { status, items }; return { status, items };
} }
@@ -4,16 +4,15 @@ import type { fetch as undiciFetch } from "undici";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { ResponseError } from "@homarr/common/server"; import { ResponseError } from "@homarr/common/server";
import { Integration } from "../../base/integration";
import type { IntegrationTestingInput } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration";
import type { TestingResult } from "../../base/test-connection/test-connection-service"; import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data"; import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items"; import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status"; import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
import type { NzbGetClient } from "./nzbget-types"; import type { NzbGetClient } from "./nzbget-types";
export class NzbGetIntegration extends Integration implements IDownloadClientIntegration { export class NzbGetIntegration extends DownloadClientIntegration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> { protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
await this.nzbGetApiCallWithCustomFetchAsync(input.fetchAsync, "version"); await this.nzbGetApiCallWithCustomFetchAsync(input.fetchAsync, "version");
return { return {
@@ -21,7 +20,7 @@ export class NzbGetIntegration extends Integration implements IDownloadClientInt
}; };
} }
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> { public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
const type = "usenet"; const type = "usenet";
const queue = await this.nzbGetApiCallAsync("listgroups"); const queue = await this.nzbGetApiCallAsync("listgroups");
const history = await this.nzbGetApiCallAsync("history"); const history = await this.nzbGetApiCallAsync("history");
@@ -66,8 +65,7 @@ export class NzbGetIntegration extends Integration implements IDownloadClientInt
category: file.Category, category: file.Category,
}; };
}), }),
) );
.slice(0, input.limit);
return { status, items }; return { status, items };
} }
@@ -6,17 +6,16 @@ import { createCertificateAgentAsync } from "@homarr/certificates/server";
import { HandleIntegrationErrors } from "../../base/errors/decorator"; import { HandleIntegrationErrors } from "../../base/errors/decorator";
import { integrationOFetchHttpErrorHandler } from "../../base/errors/http"; import { integrationOFetchHttpErrorHandler } from "../../base/errors/http";
import { Integration } from "../../base/integration";
import type { IntegrationTestingInput } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration";
import { TestConnectionError } from "../../base/test-connection/test-connection-error"; import { TestConnectionError } from "../../base/test-connection/test-connection-error";
import type { TestingResult } from "../../base/test-connection/test-connection-service"; import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data"; import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items"; import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status"; import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
@HandleIntegrationErrors([integrationOFetchHttpErrorHandler]) @HandleIntegrationErrors([integrationOFetchHttpErrorHandler])
export class QBitTorrentIntegration extends Integration implements IDownloadClientIntegration { export class QBitTorrentIntegration extends DownloadClientIntegration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> { protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const client = await this.getClientAsync(input.dispatcher); const client = await this.getClientAsync(input.dispatcher);
const isSuccess = await client.login(); const isSuccess = await client.login();
@@ -27,10 +26,10 @@ export class QBitTorrentIntegration extends Integration implements IDownloadClie
}; };
} }
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> { public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
const type = "torrent"; const type = "torrent";
const client = await this.getClientAsync(); const client = await this.getClientAsync();
const torrents = await client.listTorrents({ limit: input.limit }); const torrents = await client.listTorrents();
const rates = torrents.reduce( const rates = torrents.reduce(
({ down, up }, { dlspeed, upspeed }) => ({ down: down + dlspeed, up: up + upspeed }), ({ down, up }, { dlspeed, upspeed }) => ({ down: down + dlspeed, up: up + upspeed }),
{ down: 0, up: 0 }, { down: 0, up: 0 },
@@ -5,32 +5,27 @@ import type { fetch as undiciFetch } from "undici";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { ResponseError } from "@homarr/common/server"; import { ResponseError } from "@homarr/common/server";
import { Integration } from "../../base/integration";
import type { IntegrationTestingInput } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration";
import type { TestingResult } from "../../base/test-connection/test-connection-service"; import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data"; import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items"; import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status"; import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
import { historySchema, queueSchema } from "./sabnzbd-schema"; import { historySchema, queueSchema } from "./sabnzbd-schema";
dayjs.extend(duration); dayjs.extend(duration);
export class SabnzbdIntegration extends Integration implements IDownloadClientIntegration { export class SabnzbdIntegration extends DownloadClientIntegration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> { protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
//This is the one call that uses the least amount of data while requiring the api key //This is the one call that uses the least amount of data while requiring the api key
await this.sabNzbApiCallWithCustomFetchAsync(input.fetchAsync, "translate", { value: "ping" }); await this.sabNzbApiCallWithCustomFetchAsync(input.fetchAsync, "translate", { value: "ping" });
return { success: true }; return { success: true };
} }
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> { public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
const type = "usenet"; const type = "usenet";
const { queue } = await queueSchema.parseAsync( const { queue } = await queueSchema.parseAsync(await this.sabNzbApiCallAsync("queue"));
await this.sabNzbApiCallAsync("queue", { limit: input.limit.toString() }), const { history } = await historySchema.parseAsync(await this.sabNzbApiCallAsync("history"));
);
const { history } = await historySchema.parseAsync(
await this.sabNzbApiCallAsync("history", { limit: input.limit.toString() }),
);
const status: DownloadClientStatus = { const status: DownloadClientStatus = {
paused: queue.paused, paused: queue.paused,
rates: { down: Math.floor(Number(queue.kbpersec) * 1024) }, //Actually rounded kiBps () rates: { down: Math.floor(Number(queue.kbpersec) * 1024) }, //Actually rounded kiBps ()
@@ -78,8 +73,7 @@ export class SabnzbdIntegration extends Integration implements IDownloadClientIn
category: slot.category, category: slot.category,
}; };
}), }),
) );
.slice(0, input.limit);
return { status, items }; return { status, items };
} }
@@ -6,16 +6,15 @@ import { createCertificateAgentAsync } from "@homarr/certificates/server";
import { HandleIntegrationErrors } from "../../base/errors/decorator"; import { HandleIntegrationErrors } from "../../base/errors/decorator";
import { integrationOFetchHttpErrorHandler } from "../../base/errors/http"; import { integrationOFetchHttpErrorHandler } from "../../base/errors/http";
import { Integration } from "../../base/integration";
import type { IntegrationTestingInput } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration";
import type { TestingResult } from "../../base/test-connection/test-connection-service"; import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data"; import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items"; import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status"; import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
@HandleIntegrationErrors([integrationOFetchHttpErrorHandler]) @HandleIntegrationErrors([integrationOFetchHttpErrorHandler])
export class TransmissionIntegration extends Integration implements IDownloadClientIntegration { export class TransmissionIntegration extends DownloadClientIntegration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> { protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const client = await this.getClientAsync(input.dispatcher); const client = await this.getClientAsync(input.dispatcher);
await client.getSession(); await client.getSession();
@@ -24,10 +23,9 @@ export class TransmissionIntegration extends Integration implements IDownloadCli
}; };
} }
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> { public async getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus> {
const type = "torrent"; const type = "torrent";
const client = await this.getClientAsync(); const client = await this.getClientAsync();
// Currently there is no way to limit the number of returned torrents
const { torrents } = (await client.listTorrents()).arguments; const { torrents } = (await client.listTorrents()).arguments;
const rates = torrents.reduce( const rates = torrents.reduce(
({ down, up }, { rateDownload, rateUpload }) => ({ down: down + rateDownload, up: up + rateUpload }), ({ down, up }, { rateDownload, rateUpload }) => ({ down: down + rateDownload, up: up + rateUpload }),
@@ -36,29 +34,27 @@ export class TransmissionIntegration extends Integration implements IDownloadCli
const paused = const paused =
torrents.find(({ status }) => TransmissionIntegration.getTorrentState(status) !== "paused") === undefined; torrents.find(({ status }) => TransmissionIntegration.getTorrentState(status) !== "paused") === undefined;
const status: DownloadClientStatus = { paused, rates, types: [type] }; const status: DownloadClientStatus = { paused, rates, types: [type] };
const items = torrents const items = torrents.map((torrent): DownloadClientItem => {
.map((torrent): DownloadClientItem => { const state = TransmissionIntegration.getTorrentState(torrent.status);
const state = TransmissionIntegration.getTorrentState(torrent.status); return {
return { type,
type, id: torrent.hashString,
id: torrent.hashString, index: torrent.queuePosition,
index: torrent.queuePosition, name: torrent.name,
name: torrent.name, size: torrent.totalSize,
size: torrent.totalSize, sent: torrent.uploadedEver,
sent: torrent.uploadedEver, downSpeed: torrent.percentDone !== 1 ? torrent.rateDownload : undefined,
downSpeed: torrent.percentDone !== 1 ? torrent.rateDownload : undefined, upSpeed: torrent.rateUpload,
upSpeed: torrent.rateUpload, time:
time: torrent.percentDone === 1
torrent.percentDone === 1 ? Math.min(torrent.doneDate * 1000 - dayjs().valueOf(), -1)
? Math.min(torrent.doneDate * 1000 - dayjs().valueOf(), -1) : Math.max(torrent.eta * 1000, 0),
: Math.max(torrent.eta * 1000, 0), added: torrent.addedDate * 1000,
added: torrent.addedDate * 1000, state,
state, progress: torrent.percentDone,
progress: torrent.percentDone, category: torrent.labels,
category: torrent.labels, };
}; });
})
.slice(0, input.limit);
return { status, items }; return { status, items };
} }
@@ -7,8 +7,7 @@ import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration"; import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error"; import { TestConnectionError } from "../base/test-connection/test-connection-error";
import type { TestingResult } from "../base/test-connection/test-connection-service"; import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration"; import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session";
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/media-server-types";
import { convertJellyfinType } from "../jellyfin/jellyfin-integration"; import { convertJellyfinType } from "../jellyfin/jellyfin-integration";
const sessionSchema = z.object({ const sessionSchema = z.object({
@@ -31,7 +30,7 @@ const sessionSchema = z.object({
UserName: z.string().nullish(), UserName: z.string().nullish(),
}); });
export class EmbyIntegration extends Integration implements IMediaServerIntegration { export class EmbyIntegration extends Integration {
private static readonly apiKeyHeader = "X-Emby-Token"; private static readonly apiKeyHeader = "X-Emby-Token";
private static readonly deviceId = "homarr-emby-integration"; private static readonly deviceId = "homarr-emby-integration";
private static readonly authorizationHeaderValue = `Emby Client="Dashboard", Device="Homarr", DeviceId="${EmbyIntegration.deviceId}", Version="0.0.1"`; private static readonly authorizationHeaderValue = `Emby Client="Dashboard", Device="Homarr", DeviceId="${EmbyIntegration.deviceId}", Version="0.0.1"`;
@@ -5,10 +5,9 @@ import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration"; import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error"; import { TestConnectionError } from "../base/test-connection/test-connection-error";
import type { TestingResult } from "../base/test-connection/test-connection-service"; import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { ISmartHomeIntegration } from "../interfaces/smart-home/smart-home-integration";
import { entityStateSchema } from "./homeassistant-types"; import { entityStateSchema } from "./homeassistant-types";
export class HomeAssistantIntegration extends Integration implements ISmartHomeIntegration { export class HomeAssistantIntegration extends Integration {
public async getEntityStateAsync(entityId: string) { public async getEntityStateAsync(entityId: string) {
try { try {
const response = await this.getAsync(`/api/states/${entityId}`); const response = await this.getAsync(`/api/states/${entityId}`);
@@ -16,7 +15,6 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI
if (!response.ok) { if (!response.ok) {
logger.warn(`Response did not indicate success`); logger.warn(`Response did not indicate success`);
return { return {
success: false as const,
error: "Response did not indicate success", error: "Response did not indicate success",
}; };
} }
+12 -16
View File
@@ -1,43 +1,39 @@
// General integrations // General integrations
export { AdGuardHomeIntegration } from "./adguard-home/adguard-home-integration"; export { AdGuardHomeIntegration } from "./adguard-home/adguard-home-integration";
export { Aria2Integration } from "./download-client/aria2/aria2-integration";
export { DelugeIntegration } from "./download-client/deluge/deluge-integration"; export { DelugeIntegration } from "./download-client/deluge/deluge-integration";
export { NzbGetIntegration } from "./download-client/nzbget/nzbget-integration"; export { NzbGetIntegration } from "./download-client/nzbget/nzbget-integration";
export { QBitTorrentIntegration } from "./download-client/qbittorrent/qbittorrent-integration"; export { QBitTorrentIntegration } from "./download-client/qbittorrent/qbittorrent-integration";
export { SabnzbdIntegration } from "./download-client/sabnzbd/sabnzbd-integration"; export { SabnzbdIntegration } from "./download-client/sabnzbd/sabnzbd-integration";
export { TransmissionIntegration } from "./download-client/transmission/transmission-integration"; export { TransmissionIntegration } from "./download-client/transmission/transmission-integration";
export { Aria2Integration } from "./download-client/aria2/aria2-integration";
export { HomeAssistantIntegration } from "./homeassistant/homeassistant-integration"; export { HomeAssistantIntegration } from "./homeassistant/homeassistant-integration";
export { DownloadClientIntegration } from "./interfaces/downloads/download-client-integration";
export { JellyfinIntegration } from "./jellyfin/jellyfin-integration"; export { JellyfinIntegration } from "./jellyfin/jellyfin-integration";
export { JellyseerrIntegration } from "./jellyseerr/jellyseerr-integration"; export { JellyseerrIntegration } from "./jellyseerr/jellyseerr-integration";
export { LidarrIntegration } from "./media-organizer/lidarr/lidarr-integration";
export { RadarrIntegration } from "./media-organizer/radarr/radarr-integration"; export { RadarrIntegration } from "./media-organizer/radarr/radarr-integration";
export { ReadarrIntegration } from "./media-organizer/readarr/readarr-integration";
export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration"; export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration";
export { NextcloudIntegration } from "./nextcloud/nextcloud.integration";
export { NTFYIntegration } from "./ntfy/ntfy-integration";
export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration"; export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration";
export { OverseerrIntegration } from "./overseerr/overseerr-integration"; export { OverseerrIntegration } from "./overseerr/overseerr-integration";
export { PiHoleIntegrationV5 } from "./pi-hole/v5/pi-hole-integration-v5"; export { PiHoleIntegrationV5 } from "./pi-hole/v5/pi-hole-integration-v5";
export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6"; export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6";
export { PlexIntegration } from "./plex/plex-integration"; export { PlexIntegration } from "./plex/plex-integration";
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration"; export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
export { LidarrIntegration } from "./media-organizer/lidarr/lidarr-integration";
export { ReadarrIntegration } from "./media-organizer/readarr/readarr-integration";
export { NextcloudIntegration } from "./nextcloud/nextcloud.integration";
// Types // Types
export type { IntegrationInput } from "./base/integration"; export type { IntegrationInput } from "./base/integration";
export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data"; export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items"; export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status"; export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types"; export type { HealthMonitoring } from "./interfaces/health-monitoring/healt-monitoring";
export { MediaRequestStatus } from "./interfaces/media-requests/media-request-types"; export { MediaRequestStatus } from "./interfaces/media-requests/media-request";
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types"; export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request";
export type { StreamSession } from "./interfaces/media-server/media-server-types"; export type { StreamSession } from "./interfaces/media-server/session";
export type { export type { TdarrQueue } from "./interfaces/media-transcoding/queue";
TdarrQueue, export type { TdarrPieSegment, TdarrStatistics } from "./interfaces/media-transcoding/statistics";
TdarrPieSegment, export type { TdarrWorker } from "./interfaces/media-transcoding/workers";
TdarrStatistics,
TdarrWorker,
} from "./interfaces/media-transcoding/media-transcoding-types";
export type { Notification } from "./interfaces/notifications/notification-types";
// Schemas // Schemas
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items"; export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";
Loaded 100 of 243 files, more files were not shown because too many files have changed in this diff. Show more