Compare commits

..
163 changed files with 4183 additions and 7350 deletions

No files matched your search

-5
View File
@@ -31,11 +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.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
-212
View File
@@ -1,215 +1,3 @@
## [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
+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
+28 -28
View File
@@ -48,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.4", "@tanstack/react-query": "^5.76.2",
"@tanstack/react-query-devtools": "^5.81.4", "@tanstack/react-query-devtools": "^5.76.2",
"@tanstack/react-query-next-experimental": "^5.81.4", "@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": "^16.6.0", "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.4", "next": "15.3.2",
"postcss-preset-mantine": "^1.17.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.25.3", "swagger-ui-react": "^5.22.0",
"use-deep-compare-effect": "^1.8.1", "use-deep-compare-effect": "^1.8.1",
"zod": "^3.25.67" "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.15.33", "@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.29.0", "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"
} }
} }
@@ -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;
};
+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);
} }
+7 -7
View File
@@ -36,20 +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": "^16.6.0", "dotenv": "^16.5.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.15.33", "@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.29.0", "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"
} }
} }
+5 -5
View File
@@ -25,8 +25,8 @@
"@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": "^16.6.0", "dotenv": "^16.5.0",
"tsx": "4.20.3", "tsx": "4.19.4",
"ws": "^8.18.2" "ws": "^8.18.2"
}, },
"devDependencies": { "devDependencies": {
@@ -34,9 +34,9 @@
"@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.29.0", "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.26.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.5", "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.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+11 -11
View File
@@ -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.4", "@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.4", "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.1", "trpc-to-openapi": "^2.2.0",
"zod": "^3.25.67" "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.29.0", "eslint": "^9.27.0",
"prettier": "^3.6.2", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
@@ -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,
-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.2", "ldapts": "8.0.0",
"next": "15.3.4", "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.67" "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.29.0", "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.29.0", "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.29.0", "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": "^16.6.0" "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.29.0", "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.4", "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.67", "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.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
@@ -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
+1 -1
View File
@@ -33,7 +33,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.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
-1
View File
@@ -25,7 +25,6 @@ export const cronJobs = {
minecraftServerStatus: { preventManualExecution: false }, minecraftServerStatus: { preventManualExecution: false },
networkController: { preventManualExecution: false }, networkController: { preventManualExecution: false },
dockerContainers: { preventManualExecution: false }, dockerContainers: { preventManualExecution: false },
refreshNotifications: { preventManualExecution: false },
} satisfies Record<JobGroupKeys, { preventManualExecution?: boolean }>; } satisfies Record<JobGroupKeys, { preventManualExecution?: boolean }>;
/** /**
+1 -1
View File
@@ -7,6 +7,6 @@ import { cronJobRunnerChannel } from ".";
*/ */
export const registerCronJobRunner = () => { export const registerCronJobRunner = () => {
cronJobRunnerChannel.subscribe((jobName) => { cronJobRunnerChannel.subscribe((jobName) => {
void jobGroup.runManuallyAsync(jobName); jobGroup.runManually(jobName);
}); });
}; };
+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.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+2 -2
View File
@@ -25,14 +25,14 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"node-cron": "^4.1.1" "node-cron": "^3.0.3"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"eslint": "^9.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+7 -7
View File
@@ -1,6 +1,5 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import type { ScheduledTask } from "node-cron"; import cron from "node-cron";
import { schedule, validate } 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";
@@ -28,7 +27,7 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
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 {
@@ -65,11 +64,12 @@ 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. * 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. * 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: ScheduledTask | null = null; let scheduledTask: cron.ScheduledTask | null = null;
if (cronExpression !== "never") { if (cronExpression !== "never") {
scheduledTask = schedule(cronExpression, () => void catchingCallbackAsync(), { scheduledTask = cron.schedule(cronExpression, () => void catchingCallbackAsync(), {
scheduled: false,
name, name,
timezone: creatorOptions.timezone, timezone: creatorOptions.timezone,
}); });
@@ -110,7 +110,7 @@ export const createCronJobCreator = <TAllowedNames extends string = string>(
options: CreateCronJobOptions = { runOnStart: false }, options: CreateCronJobOptions = { runOnStart: false },
) => { ) => {
creatorOptions.logger.logDebug(`Validating cron expression '${cronExpression}' for job: ${name}`); creatorOptions.logger.logDebug(`Validating cron expression '${cronExpression}' for job: ${name}`);
if (cronExpression !== "never" && !validate(cronExpression)) { if (cronExpression !== "never" && !cron.validate(cronExpression)) {
throw new Error(`Invalid cron expression '${cronExpression}' for job '${name}'`); throw new Error(`Invalid cron expression '${cronExpression}' for job '${name}'`);
} }
creatorOptions.logger.logDebug(`Cron job expression '${cronExpression}' for job ${name} is valid`); creatorOptions.logger.logDebug(`Cron job expression '${cronExpression}' for job ${name} is valid`);
+8 -8
View File
@@ -34,33 +34,33 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
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 job.scheduledTask?.start(); job.scheduledTask?.start();
}, },
startAllAsync: async () => { startAllAsync: async () => {
for (const job of jobRegistry.values()) { for (const job of jobRegistry.values()) {
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 job.scheduledTask?.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;
options.logger.logInfo(`Running schedule cron job ${job.name} manually.`); options.logger.logInfo(`Running schedule cron job ${job.name} manually.`);
await job.scheduledTask?.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 job.scheduledTask?.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 job.scheduledTask?.stop(); job.scheduledTask?.stop();
} }
}, },
getJobRegistry() { getJobRegistry() {
+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.29.0", "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,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,
}),
}, },
}), }),
); );
@@ -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"),
}); });
+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.1.1", "better-sqlite3": "^11.10.0",
"dotenv": "^16.6.0", "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.29.0", "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 -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.67" "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.29.0", "eslint": "^9.27.0",
"tsx": "4.20.3", "tsx": "4.19.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -9
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,12 +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"],
},
} 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>;
@@ -230,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.41", "@types/dockerode": "^3.3.39",
"eslint": "^9.29.0", "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.67" "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.29.0", "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.67" "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.29.0", "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.67" "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.29.0", "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.29.0", "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.67" "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.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
@@ -21,7 +21,6 @@ import { ReadarrIntegration } from "../media-organizer/readarr/readarr-integrati
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 { 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";
@@ -93,7 +92,6 @@ export const integrationCreators = {
emby: EmbyIntegration, emby: EmbyIntegration,
nextcloud: NextcloudIntegration, nextcloud: NextcloudIntegration,
unifiController: UnifiControllerIntegration, unifiController: UnifiControllerIntegration,
ntfy: NTFYIntegration,
} 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,7 +12,7 @@ import type { DownloadClientItem } from "../../interfaces/downloads/download-cli
import type { Aria2Download, Aria2GetClient } from "./aria2-types"; import type { Aria2Download, Aria2GetClient } from "./aria2-types";
export class Aria2Integration extends DownloadClientIntegration { 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",
@@ -27,12 +27,12 @@ export class Aria2Integration extends DownloadClientIntegration {
]; ];
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 {
@@ -29,10 +29,9 @@ export class DelugeIntegration extends DownloadClientIntegration {
}; };
} }
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,
@@ -50,29 +49,27 @@ export class DelugeIntegration extends DownloadClientIntegration {
}, },
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 };
} }
@@ -20,7 +20,7 @@ export class NzbGetIntegration extends DownloadClientIntegration {
}; };
} }
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");
@@ -65,8 +65,7 @@ export class NzbGetIntegration extends DownloadClientIntegration {
category: file.Category, category: file.Category,
}; };
}), }),
) );
.slice(0, input.limit);
return { status, items }; return { status, items };
} }
@@ -26,10 +26,10 @@ export class QBitTorrentIntegration extends DownloadClientIntegration {
}; };
} }
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 },
@@ -22,14 +22,10 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
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 ()
@@ -77,8 +73,7 @@ export class SabnzbdIntegration extends DownloadClientIntegration {
category: slot.category, category: slot.category,
}; };
}), }),
) );
.slice(0, input.limit);
return { status, items }; return { status, items };
} }
@@ -23,10 +23,9 @@ export class TransmissionIntegration extends DownloadClientIntegration {
}; };
} }
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 }),
@@ -35,29 +34,27 @@ export class TransmissionIntegration extends DownloadClientIntegration {
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 };
} }
+4 -6
View File
@@ -1,27 +1,26 @@
// 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 { 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";
@@ -35,7 +34,6 @@ export type { StreamSession } from "./interfaces/media-server/session";
export type { TdarrQueue } from "./interfaces/media-transcoding/queue"; export type { TdarrQueue } from "./interfaces/media-transcoding/queue";
export type { TdarrPieSegment, TdarrStatistics } from "./interfaces/media-transcoding/statistics"; export type { TdarrPieSegment, TdarrStatistics } from "./interfaces/media-transcoding/statistics";
export type { TdarrWorker } from "./interfaces/media-transcoding/workers"; export type { TdarrWorker } from "./interfaces/media-transcoding/workers";
export type { Notification } from "./interfaces/notifications/notification";
// Schemas // Schemas
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items"; export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";
@@ -4,7 +4,7 @@ import type { DownloadClientItem } from "./download-client-items";
export abstract class DownloadClientIntegration extends Integration { export abstract class DownloadClientIntegration extends Integration {
/** Get download client's status and list of all of it's items */ /** Get download client's status and list of all of it's items */
public abstract getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus>; public abstract getClientJobsAndStatusAsync(): Promise<DownloadClientJobsAndStatus>;
/** Pauses the client or all of it's items */ /** Pauses the client or all of it's items */
public abstract pauseQueueAsync(): Promise<void>; public abstract pauseQueueAsync(): Promise<void>;
/** Pause a single item using it's ID */ /** Pause a single item using it's ID */
@@ -43,7 +43,6 @@ export interface MediaRequestStats {
users: RequestUser[]; users: RequestUser[];
} }
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L1
export enum MediaRequestStatus { export enum MediaRequestStatus {
PendingApproval = 1, PendingApproval = 1,
Approved = 2, Approved = 2,
@@ -52,7 +51,6 @@ export enum MediaRequestStatus {
Completed = 5, Completed = 5,
} }
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L14
export enum MediaAvailability { export enum MediaAvailability {
Unknown = 1, Unknown = 1,
Pending = 2, Pending = 2,
@@ -60,5 +58,4 @@ export enum MediaAvailability {
PartiallyAvailable = 4, PartiallyAvailable = 4,
Available = 5, Available = 5,
Blacklisted = 6, Blacklisted = 6,
Deleted = 7,
} }
@@ -1,6 +0,0 @@
export interface Notification {
id: string;
time: Date;
title: string;
body: string;
}
@@ -1,6 +0,0 @@
import { Integration } from "../../base/integration";
import type { Notification } from "./notification";
export abstract class NotificationsIntegration extends Integration {
public abstract getNotificationsAsync(): Promise<Notification[]>;
}
@@ -1,3 +1,6 @@
import dayjs from "dayjs";
import objectSupport from "dayjs/plugin/objectSupport";
import utc from "dayjs/plugin/utc";
import * as ical from "node-ical"; import * as ical from "node-ical";
import { DAVClient } from "tsdav"; import { DAVClient } from "tsdav";
import type { Dispatcher, RequestInit as UndiciFetchRequestInit } from "undici"; import type { Dispatcher, RequestInit as UndiciFetchRequestInit } from "undici";
@@ -12,6 +15,9 @@ import { Integration } 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 { CalendarEvent } from "../calendar-types"; import type { CalendarEvent } from "../calendar-types";
dayjs.extend(utc);
dayjs.extend(objectSupport);
@HandleIntegrationErrors([integrationTsdavHttpErrorHandler]) @HandleIntegrationErrors([integrationTsdavHttpErrorHandler])
export class NextcloudIntegration extends Integration { export class NextcloudIntegration extends Integration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> { protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
@@ -21,7 +27,7 @@ export class NextcloudIntegration extends Integration {
return { success: true }; return { success: true };
} }
public async getCalendarEventsAsync(start: Date, end: Date, _showUnmonitored?: boolean): Promise<CalendarEvent[]> { public async getCalendarEventsAsync(start: Date, end: Date): Promise<CalendarEvent[]> {
const client = await this.createCalendarClientAsync(); const client = await this.createCalendarClientAsync();
await client.login(); await client.login();
@@ -51,7 +57,14 @@ export class NextcloudIntegration extends Integration {
logger.debug(`Converting VEVENT event to ${event.etag} from Nextcloud: ${JSON.stringify(veventObject)}`); logger.debug(`Converting VEVENT event to ${event.etag} from Nextcloud: ${JSON.stringify(veventObject)}`);
const date = veventObject.start; const date = dayjs.utc({
days: veventObject.start.getDay(),
month: veventObject.start.getMonth(),
year: veventObject.start.getFullYear(),
hours: veventObject.start.getHours(),
minutes: veventObject.start.getMinutes(),
seconds: veventObject.start.getSeconds(),
});
const eventUrlWithoutHost = new URL(event.url).pathname; const eventUrlWithoutHost = new URL(event.url).pathname;
const dateInMillis = veventObject.start.valueOf(); const dateInMillis = veventObject.start.valueOf();
@@ -62,7 +75,7 @@ export class NextcloudIntegration extends Integration {
return { return {
name: veventObject.summary, name: veventObject.summary,
date, date: date.toDate(),
subName: "", subName: "",
description: veventObject.description, description: veventObject.description,
links: [ links: [
@@ -1,65 +0,0 @@
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { ResponseError } from "@homarr/common/server";
import type { IntegrationTestingInput } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { Notification } from "../interfaces/notifications/notification";
import { NotificationsIntegration } from "../interfaces/notifications/notifications-integration";
import { ntfyNotificationSchema } from "./ntfy-schema";
export class NTFYIntegration extends NotificationsIntegration {
public async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
await input.fetchAsync(this.url("/v1/account"), { headers: this.getHeaders() });
return { success: true };
}
private getTopicURL() {
return this.url(`/${encodeURIComponent(super.getSecretValue("topic"))}/json`, { poll: 1 });
}
private getHeaders() {
return this.hasSecretValue("apiKey") ? { Authorization: `Bearer ${super.getSecretValue("apiKey")}` } : {};
}
public async getNotificationsAsync() {
const url = this.getTopicURL();
const notifications = await Promise.all(
(
await fetchWithTrustedCertificatesAsync(url, { headers: this.getHeaders() })
.then((response) => {
if (!response.ok) throw new ResponseError(response);
return response.text();
})
.catch((error) => {
if (error instanceof Error) throw error;
else {
throw new Error("Error communicating with ntfy");
}
})
)
// response is provided as individual lines of JSON
.split("\n")
.map(async (line) => {
// ignore empty lines
if (line.length === 0) return null;
const json = JSON.parse(line) as unknown;
const parsed = await ntfyNotificationSchema.parseAsync(json);
if (parsed.event === "message") return parsed;
// ignore non-event messages
else return null;
}),
);
return notifications
.filter((notification) => notification !== null)
.map((notification): Notification => {
const topicURL = this.url(`/${notification.topic}`);
return {
id: notification.id,
time: new Date(notification.time * 1000),
title: notification.title ?? topicURL.hostname + topicURL.pathname,
body: notification.message,
};
});
}
}
@@ -1,12 +0,0 @@
import { z } from "zod";
// There are more properties, see: https://docs.ntfy.sh/subscribe/api/#json-message-format
// Not all properties are required for this use case.
export const ntfyNotificationSchema = z.object({
id: z.string(),
time: z.number(),
event: z.string(), // we only care about "message"
topic: z.string(),
title: z.optional(z.string()),
message: z.string(),
});
@@ -66,7 +66,7 @@ export class OpenMediaVaultIntegration extends Integration {
return { return {
version: systemResult.data.response.version, version: systemResult.data.response.version,
cpuModelName: systemResult.data.response.cpuModelName ?? "Unknown CPU", cpuModelName: systemResult.data.response.cpuModelName,
cpuUtilization: systemResult.data.response.cpuUtilization, cpuUtilization: systemResult.data.response.cpuUtilization,
memUsed: systemResult.data.response.memUsed, memUsed: systemResult.data.response.memUsed,
memAvailable: systemResult.data.response.memAvailable, memAvailable: systemResult.data.response.memAvailable,
@@ -4,7 +4,7 @@ import { z } from "zod";
export const systemInformationSchema = z.object({ export const systemInformationSchema = z.object({
response: z.object({ response: z.object({
version: z.string(), version: z.string(),
cpuModelName: z.string().nullable(), cpuModelName: z.string(),
cpuUtilization: z.number(), cpuUtilization: z.number(),
memUsed: z.string(), memUsed: z.string(),
memAvailable: z.string(), memAvailable: z.string(),
@@ -184,20 +184,21 @@ export class OverseerrIntegration extends Integration implements ISearchableInte
public async approveRequestAsync(requestId: number): Promise<void> { public async approveRequestAsync(requestId: number): Promise<void> {
logger.info(`Approving media request id='${requestId}' integration='${this.integration.name}'`); logger.info(`Approving media request id='${requestId}' integration='${this.integration.name}'`);
await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/request/${requestId}/approve`), { const response = await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/request/${requestId}/approve`), {
method: "POST", method: "POST",
headers: { headers: {
"X-Api-Key": this.getSecretValue("apiKey"), "X-Api-Key": this.getSecretValue("apiKey"),
}, },
}).then((response) => {
if (!response.ok) {
logger.error(
`Failed to approve media request id='${requestId}' integration='${this.integration.name}' reason='${response.status} ${response.statusText}' url='${response.url}'`,
);
}
logger.info(`Successfully approved media request id='${requestId}' integration='${this.integration.name}'`);
}); });
if (!response.ok) {
const content = await response.text();
logger.error(
`Failed to approve media request id='${requestId}' integration='${this.integration.name}' reason='${response.status} ${response.statusText}' url='${response.url}' content='${content}'`,
);
}
logger.info(`Successfully approved media request id='${requestId}' integration='${this.integration.name}'`);
} }
public async declineRequestAsync(requestId: number): Promise<void> { public async declineRequestAsync(requestId: number): Promise<void> {
@@ -77,7 +77,6 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
protected async testingAsync({ fetchAsync }: IntegrationTestingInput): Promise<TestingResult> { protected async testingAsync({ fetchAsync }: IntegrationTestingInput): Promise<TestingResult> {
const sessionId = await this.getSessionAsync(fetchAsync); const sessionId = await this.getSessionAsync(fetchAsync);
await this.clearSessionAsync(sessionId, fetchAsync); await this.clearSessionAsync(sessionId, fetchAsync);
await this.sessionStore.clearAsync();
return { success: true }; return { success: true };
} }
@@ -69,7 +69,6 @@ const mapResource = (resource: Proxmox.clusterResourcesResources): Resource | nu
const mapComputeResource = (resource: Proxmox.clusterResourcesResources): Omit<ComputeResourceBase<string>, "type"> => { const mapComputeResource = (resource: Proxmox.clusterResourcesResources): Omit<ComputeResourceBase<string>, "type"> => {
return { return {
id: resource.id,
cpu: { cpu: {
utilization: resource.cpu ?? 0, utilization: resource.cpu ?? 0,
cores: resource.maxcpu ?? 0, cores: resource.maxcpu ?? 0,
@@ -115,7 +114,6 @@ const mapVmResource = (resource: Proxmox.clusterResourcesResources): LxcResource
const mapStorageResource = (resource: Proxmox.clusterResourcesResources): StorageResource => { const mapStorageResource = (resource: Proxmox.clusterResourcesResources): StorageResource => {
return { return {
id: resource.id,
type: "storage", type: "storage",
name: resource.storage ?? "", name: resource.storage ?? "",
node: resource.node ?? "", node: resource.node ?? "",
@@ -7,7 +7,6 @@ interface ResourceBase<TType extends string> {
} }
export interface ComputeResourceBase<TType extends string> extends ResourceBase<TType> { export interface ComputeResourceBase<TType extends string> extends ResourceBase<TType> {
id: string;
cpu: { cpu: {
utilization: number; // previously cpu (0-1) utilization: number; // previously cpu (0-1)
cores: number; // previously cpuCores cores: number; // previously cpuCores
@@ -41,7 +40,6 @@ export interface QemuResource extends ComputeResourceBase<"qemu"> {
} }
export interface StorageResource extends ResourceBase<"storage"> { export interface StorageResource extends ResourceBase<"storage"> {
id: string;
storagePlugin: string; storagePlugin: string;
used: number; // previously disk used: number; // previously disk
total: number; // previously maxDisk total: number; // previously maxDisk
+5 -5
View File
@@ -46,7 +46,7 @@ describe("Aria2 integration", () => {
// Acts // Acts
const actAsync = async () => await aria2Integration.pauseQueueAsync(); const actAsync = async () => await aria2Integration.pauseQueueAsync();
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 }); const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync();
// Assert // Assert
await expect(actAsync()).resolves.not.toThrow(); await expect(actAsync()).resolves.not.toThrow();
@@ -62,7 +62,7 @@ describe("Aria2 integration", () => {
const aria2Integration = createAria2Intergration(startedContainer, API_KEY); const aria2Integration = createAria2Intergration(startedContainer, API_KEY);
// Act // Act
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 }); const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync();
// Assert // Assert
await expect(getAsync()).resolves.not.toThrow(); await expect(getAsync()).resolves.not.toThrow();
@@ -81,7 +81,7 @@ describe("Aria2 integration", () => {
await aria2AddItemAsync(startedContainer, API_KEY, aria2Integration); await aria2AddItemAsync(startedContainer, API_KEY, aria2Integration);
// Act // Act
const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 }); const getAsync = async () => await aria2Integration.getClientJobsAndStatusAsync();
// Assert // Assert
await expect(getAsync()).resolves.not.toThrow(); await expect(getAsync()).resolves.not.toThrow();
@@ -104,7 +104,7 @@ describe("Aria2 integration", () => {
await expect(actAsync()).resolves.not.toThrow(); await expect(actAsync()).resolves.not.toThrow();
// NzbGet is slow and we wait for a second before querying the items. Test was flaky without this. // NzbGet is slow and we wait for a second before querying the items. Test was flaky without this.
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
const result = await aria2Integration.getClientJobsAndStatusAsync({ limit: 99 }); const result = await aria2Integration.getClientJobsAndStatusAsync();
expect(result.items).toHaveLength(0); expect(result.items).toHaveLength(0);
// Cleanup // Cleanup
@@ -153,7 +153,7 @@ const aria2AddItemAsync = async (container: StartedTestContainer, apiKey: string
const { const {
items: [item], items: [item],
} = await integration.getClientJobsAndStatusAsync({ limit: 99 }); } = await integration.getClientJobsAndStatusAsync();
if (!item) { if (!item) {
throw new Error("No item found"); throw new Error("No item found");
@@ -64,29 +64,26 @@ describe("Home Assistant integration", () => {
const prepareHomeAssistantContainerAsync = async () => { const prepareHomeAssistantContainerAsync = async () => {
const homeAssistantContainer = createHomeAssistantContainer(); const homeAssistantContainer = createHomeAssistantContainer();
const startedContainer = await homeAssistantContainer.start(); const startedContainer = await homeAssistantContainer.start();
await startedContainer.exec(["unzip", "-o", "/tmp/config.zip", "-d", "/config"]); await startedContainer.exec(["unzip", "-o", "/tmp/config.zip", "-d", "/config"]);
await startedContainer.restart(); await startedContainer.restart();
return startedContainer; return startedContainer;
}; };
const createHomeAssistantContainer = () => { const createHomeAssistantContainer = () => {
return ( return new GenericContainer(IMAGE_NAME)
new GenericContainer(IMAGE_NAME) .withCopyFilesToContainer([
.withCopyFilesToContainer([ {
{ source: join(__dirname, "/volumes/home-assistant-config.zip"),
source: join(__dirname, "/volumes/home-assistant-config.zip"), target: "/tmp/config.zip",
target: "/tmp/config.zip", },
}, ])
]) .withPrivilegedMode()
.withPrivilegedMode() .withExposedPorts(8123)
.withExposedPorts(8123) .withWaitStrategy(Wait.forHttp("/", 8123));
// This has to be a page that is not redirected (or a status code has to be defined withStatusCode(statusCode))
.withWaitStrategy(Wait.forHttp("/onboarding.html", 8123))
);
}; };
const createHomeAssistantIntegration = (container: StartedTestContainer, apiKeyOverride?: string) => { const createHomeAssistantIntegration = (container: StartedTestContainer, apiKeyOverride?: string) => {
console.log("Creating Home Assistant integration...");
return new HomeAssistantIntegration({ return new HomeAssistantIntegration({
id: "1", id: "1",
decryptedSecrets: [ decryptedSecrets: [
+8 -8
View File
@@ -69,7 +69,7 @@ describe("Nzbget integration", () => {
// Acts // Acts
const actAsync = async () => await nzbGetIntegration.pauseQueueAsync(); const actAsync = async () => await nzbGetIntegration.pauseQueueAsync();
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 }); const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
// Assert // Assert
await expect(actAsync()).resolves.not.toThrow(); await expect(actAsync()).resolves.not.toThrow();
@@ -87,7 +87,7 @@ describe("Nzbget integration", () => {
// Acts // Acts
const actAsync = async () => await nzbGetIntegration.resumeQueueAsync(); const actAsync = async () => await nzbGetIntegration.resumeQueueAsync();
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 }); const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
// Assert // Assert
await expect(actAsync()).resolves.not.toThrow(); await expect(actAsync()).resolves.not.toThrow();
@@ -105,7 +105,7 @@ describe("Nzbget integration", () => {
const nzbGetIntegration = createNzbGetIntegration(startedContainer, username, password); const nzbGetIntegration = createNzbGetIntegration(startedContainer, username, password);
// Act // Act
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 }); const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
// Assert // Assert
await expect(getAsync()).resolves.not.toThrow(); await expect(getAsync()).resolves.not.toThrow();
@@ -124,7 +124,7 @@ describe("Nzbget integration", () => {
await nzbGetAddItemAsync(startedContainer, username, password, nzbGetIntegration); await nzbGetAddItemAsync(startedContainer, username, password, nzbGetIntegration);
// Act // Act
const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 }); const getAsync = async () => await nzbGetIntegration.getClientJobsAndStatusAsync();
// Assert // Assert
await expect(getAsync()).resolves.not.toThrow(); await expect(getAsync()).resolves.not.toThrow();
@@ -145,9 +145,9 @@ describe("Nzbget integration", () => {
// Assert // Assert
await expect(actAsync()).resolves.not.toThrow(); await expect(actAsync()).resolves.not.toThrow();
// NzbGet is slow and we wait for a few seconds before querying the items. Test was flaky without this. // NzbGet is slow and we wait for a second before querying the items. Test was flaky without this.
await new Promise((resolve) => setTimeout(resolve, 5000)); await new Promise((resolve) => setTimeout(resolve, 1000));
const result = await nzbGetIntegration.getClientJobsAndStatusAsync({ limit: 99 }); const result = await nzbGetIntegration.getClientJobsAndStatusAsync();
expect(result.items).toHaveLength(0); expect(result.items).toHaveLength(0);
// Cleanup // Cleanup
@@ -209,7 +209,7 @@ const nzbGetAddItemAsync = async (
const { const {
items: [item], items: [item],
} = await integration.getClientJobsAndStatusAsync({ limit: 99 }); } = await integration.getClientJobsAndStatusAsync();
if (!item) { if (!item) {
throw new Error("No item found"); throw new Error("No item found");
+6 -9
View File
@@ -211,15 +211,12 @@ const createPiHoleIntegrationV5 = (container: StartedTestContainer, apiKey: stri
}; };
const createPiHoleV6Container = (password: string) => { const createPiHoleV6Container = (password: string) => {
return ( return new GenericContainer("pihole/pihole:latest")
new GenericContainer("pihole/pihole:latest") .withEnvironment({
.withEnvironment({ FTLCONF_webserver_api_password: password,
FTLCONF_webserver_api_password: password, })
}) .withExposedPorts(80)
.withExposedPorts(80) .withWaitStrategy(Wait.forHttp("/admin", 80));
// This has to be a page that is not redirected (or a status code has to be defined withStatusCode(statusCode))
.withWaitStrategy(Wait.forHttp("/admin/login", 80))
);
}; };
const createPiHoleIntegrationV6 = (container: StartedTestContainer, apiKey: string) => { const createPiHoleIntegrationV6 = (container: StartedTestContainer, apiKey: string) => {
+18 -21
View File
@@ -67,7 +67,7 @@ describe("Sabnzbd integration", () => {
// Acts // Acts
const actAsync = async () => await sabnzbdIntegration.pauseQueueAsync(); const actAsync = async () => await sabnzbdIntegration.pauseQueueAsync();
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 }); const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
// Assert // Assert
await expect(actAsync()).resolves.not.toThrow(); await expect(actAsync()).resolves.not.toThrow();
@@ -85,7 +85,7 @@ describe("Sabnzbd integration", () => {
// Acts // Acts
const actAsync = async () => await sabnzbdIntegration.resumeQueueAsync(); const actAsync = async () => await sabnzbdIntegration.resumeQueueAsync();
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 }); const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
// Assert // Assert
await expect(actAsync()).resolves.not.toThrow(); await expect(actAsync()).resolves.not.toThrow();
@@ -103,7 +103,7 @@ describe("Sabnzbd integration", () => {
const sabnzbdIntegration = createSabnzbdIntegration(startedContainer, DEFAULT_API_KEY); const sabnzbdIntegration = createSabnzbdIntegration(startedContainer, DEFAULT_API_KEY);
// Act // Act
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 }); const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
// Assert // Assert
await expect(getAsync()).resolves.not.toThrow(); await expect(getAsync()).resolves.not.toThrow();
@@ -122,7 +122,7 @@ describe("Sabnzbd integration", () => {
await sabNzbdAddItemAsync(startedContainer, DEFAULT_API_KEY, sabnzbdIntegration); await sabNzbdAddItemAsync(startedContainer, DEFAULT_API_KEY, sabnzbdIntegration);
// Act // Act
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 }); const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
// Assert // Assert
await expect(getAsync()).resolves.not.toThrow(); await expect(getAsync()).resolves.not.toThrow();
@@ -140,7 +140,7 @@ describe("Sabnzbd integration", () => {
// Act // Act
const actAsync = async () => await sabnzbdIntegration.pauseItemAsync(item); const actAsync = async () => await sabnzbdIntegration.pauseItemAsync(item);
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 }); const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
// Assert // Assert
await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "downloading" }] }); await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "downloading" }] });
@@ -160,7 +160,7 @@ describe("Sabnzbd integration", () => {
// Act // Act
const actAsync = async () => await sabnzbdIntegration.resumeItemAsync(item); const actAsync = async () => await sabnzbdIntegration.resumeItemAsync(item);
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 }); const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
// Assert // Assert
await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "paused" }] }); await expect(getAsync()).resolves.toMatchObject({ items: [{ ...item, state: "paused" }] });
@@ -180,7 +180,7 @@ describe("Sabnzbd integration", () => {
// Act - fromDisk already doesn't work for sabnzbd, so only test deletion itself. // Act - fromDisk already doesn't work for sabnzbd, so only test deletion itself.
const actAsync = async () => const actAsync = async () =>
await sabnzbdIntegration.deleteItemAsync({ ...item, progress: 0 } as DownloadClientItem, false); await sabnzbdIntegration.deleteItemAsync({ ...item, progress: 0 } as DownloadClientItem, false);
const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync({ limit: 99 }); const getAsync = async () => await sabnzbdIntegration.getClientJobsAndStatusAsync();
// Assert // Assert
await expect(actAsync()).resolves.not.toThrow(); await expect(actAsync()).resolves.not.toThrow();
@@ -192,19 +192,16 @@ describe("Sabnzbd integration", () => {
}); });
const createSabnzbdContainer = () => { const createSabnzbdContainer = () => {
return ( return new GenericContainer(IMAGE_NAME)
new GenericContainer(IMAGE_NAME) .withCopyFilesToContainer([
.withCopyFilesToContainer([ {
{ source: join(__dirname, "/volumes/usenet/sabnzbd.ini"),
source: join(__dirname, "/volumes/usenet/sabnzbd.ini"), target: "/config/sabnzbd.ini",
target: "/config/sabnzbd.ini", },
}, ])
]) .withExposedPorts(1212)
.withExposedPorts(1212) .withEnvironment({ PUID: "0", PGID: "0" })
.withEnvironment({ PUID: "0", PGID: "0" }) .withWaitStrategy(Wait.forHttp("/", 1212));
// This has to be a page that is not redirected (or a status code has to be defined withStatusCode(statusCode))
.withWaitStrategy(Wait.forHttp("/sabnzbd/wizard/", 1212))
);
}; };
const createSabnzbdIntegration = (container: StartedTestContainer, apiKey: string) => { const createSabnzbdIntegration = (container: StartedTestContainer, apiKey: string) => {
@@ -245,7 +242,7 @@ const sabNzbdAddItemAsync = async (
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
const { const {
items: [item], items: [item],
} = await integration.getClientJobsAndStatusAsync({ limit: 99 }); } = await integration.getClientJobsAndStatusAsync();
if (item) return item; if (item) return item;
} }
// Throws if it can't find the item // Throws if it can't find the item
+2 -2
View File
@@ -27,13 +27,13 @@
"ioredis": "5.6.1", "ioredis": "5.6.1",
"superjson": "2.2.2", "superjson": "2.2.2",
"winston": "3.17.0", "winston": "3.17.0",
"zod": "^3.25.67" "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.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+5 -5
View File
@@ -33,19 +33,19 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "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",
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.33.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"next": "15.3.4", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"zod": "^3.25.67" "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.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
@@ -1,4 +1,5 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import Image from "next/image";
import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core"; import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core";
import { IconPlus, IconSearch } from "@tabler/icons-react"; import { IconPlus, IconSearch } from "@tabler/icons-react";
@@ -22,7 +23,7 @@ export const AppSelectModal = createModal<AppSelectModalProps>(({ actions, inner
() => () =>
apps apps
.filter((app) => app.name.toLowerCase().includes(search.toLowerCase())) .filter((app) => app.name.toLowerCase().includes(search.toLowerCase()))
.sort((appA, appB) => appA.name.localeCompare(appB.name)), .sort((a, b) => a.name.localeCompare(b.name)),
[apps, search], [apps, search],
); );
@@ -87,7 +88,7 @@ export const AppSelectModal = createModal<AppSelectModalProps>(({ actions, inner
<Stack justify="space-between" h="100%"> <Stack justify="space-between" h="100%">
<Stack gap="xs"> <Stack gap="xs">
<Center> <Center>
<img src={app.iconUrl} alt={app.name} width={24} height={24} /> <Image src={app.iconUrl || ""} alt={app.name} width={24} height={24} />
</Center> </Center>
<Text lh={1.2} style={{ whiteSpace: "normal" }} ta="center"> <Text lh={1.2} style={{ whiteSpace: "normal" }} ta="center">
{app.name} {app.name}
+3 -3
View File
@@ -24,15 +24,15 @@
"dependencies": { "dependencies": {
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^8.1.2", "@mantine/core": "^8.0.1",
"@mantine/hooks": "^8.1.2", "@mantine/hooks": "^8.0.1",
"react": "19.1.0" "react": "19.1.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.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+3 -3
View File
@@ -24,14 +24,14 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@mantine/notifications": "^8.1.2", "@mantine/notifications": "^8.0.1",
"@tabler/icons-react": "^3.34.0" "@tabler/icons-react": "^3.33.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.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+5 -5
View File
@@ -37,14 +37,14 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "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",
"@mantine/hooks": "^8.1.2", "@mantine/hooks": "^8.0.1",
"adm-zip": "0.5.16", "adm-zip": "0.5.16",
"next": "15.3.4", "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",
"zod": "^3.25.67", "zod": "^3.25.23",
"zod-form-data": "^2.0.7" "zod-form-data": "^2.0.7"
}, },
"devDependencies": { "devDependencies": {
@@ -52,7 +52,7 @@
"@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/adm-zip": "0.5.7", "@types/adm-zip": "0.5.7",
"eslint": "^9.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
@@ -77,7 +77,6 @@ const optionMapping: OptionMapping = {
descendingDefaultSort: () => false, descendingDefaultSort: () => false,
showCompletedUsenet: () => true, showCompletedUsenet: () => true,
showCompletedHttp: () => true, showCompletedHttp: () => true,
limitPerIntegration: () => undefined,
}, },
weather: { weather: {
forecastDayCount: (oldOptions) => oldOptions.forecastDays, forecastDayCount: (oldOptions) => oldOptions.forecastDays,
@@ -157,21 +156,6 @@ const optionMapping: OptionMapping = {
defaultTab: (oldOptions) => ("defaultTabState" in oldOptions ? oldOptions.defaultTabState : undefined), defaultTab: (oldOptions) => ("defaultTabState" in oldOptions ? oldOptions.defaultTabState : undefined),
sectionIndicatorRequirement: (oldOptions) => sectionIndicatorRequirement: (oldOptions) =>
"sectionIndicatorColor" in oldOptions ? oldOptions.sectionIndicatorColor : undefined, "sectionIndicatorColor" in oldOptions ? oldOptions.sectionIndicatorColor : undefined,
showUptime: () => undefined,
visibleClusterSections: (oldOptions) => {
if (!("showNode" in oldOptions)) return undefined;
const oldKeys = {
showNode: "node" as const,
showLXCs: "lxc" as const,
showVM: "qemu" as const,
showStorage: "storage" as const,
} satisfies Partial<Record<keyof typeof oldOptions, string>>;
return objectEntries(oldKeys)
.filter(([key]) => oldOptions[key])
.map(([_, section]) => section);
},
}, },
mediaTranscoding: { mediaTranscoding: {
defaultView: (oldOptions) => oldOptions.defaultView, defaultView: (oldOptions) => oldOptions.defaultView,
+2 -2
View File
@@ -23,13 +23,13 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"zod": "^3.25.67" "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.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -1
View File
@@ -30,7 +30,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1 -1
View File
@@ -33,7 +33,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.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+2 -2
View File
@@ -30,14 +30,14 @@
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"@homarr/redis": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"octokit": "^5.0.3", "octokit": "^5.0.2",
"superjson": "2.2.2" "superjson": "2.2.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",
"eslint": "^9.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+2 -4
View File
@@ -13,10 +13,8 @@ export const calendarMonthRequestHandler = createCachedIntegrationRequestHandler
>({ >({
async requestAsync(integration, input) { async requestAsync(integration, input) {
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
// Calendar component shows up to 6 days before and after the month, for example if 1. of january is sunday, it shows the last 6 days of december. const startDate = dayjs().year(input.year).month(input.month).startOf("month");
const startDate = dayjs().year(input.year).month(input.month).startOf("month").subtract(6, "days"); const endDate = startDate.clone().endOf("month");
const endDate = dayjs().year(input.year).month(input.month).endOf("month").add(6, "days");
return await integrationInstance.getCalendarEventsAsync( return await integrationInstance.getCalendarEventsAsync(
startDate.toDate(), startDate.toDate(),
endDate.toDate(), endDate.toDate(),
+3 -3
View File
@@ -9,11 +9,11 @@ import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-
export const downloadClientRequestHandler = createCachedIntegrationRequestHandler< export const downloadClientRequestHandler = createCachedIntegrationRequestHandler<
DownloadClientJobsAndStatus, DownloadClientJobsAndStatus,
IntegrationKindByCategory<"downloadClient">, IntegrationKindByCategory<"downloadClient">,
{ limit: number } Record<string, never>
>({ >({
async requestAsync(integration, input) { async requestAsync(integration, _input) {
const integrationInstance = await createIntegrationAsync(integration); const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getClientJobsAndStatusAsync(input); return await integrationInstance.getClientJobsAndStatusAsync();
}, },
cacheDuration: dayjs.duration(5, "seconds"), cacheDuration: dayjs.duration(5, "seconds"),
queryKey: "downloadClientJobStatus", queryKey: "downloadClientJobStatus",
@@ -1,20 +0,0 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import type { Notification } from "@homarr/integrations";
import { createIntegrationAsync } from "@homarr/integrations";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const notificationsRequestHandler = createCachedIntegrationRequestHandler<
Notification[],
IntegrationKindByCategory<"notifications">,
Record<string, never>
>({
async requestAsync(integration) {
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getNotificationsAsync();
},
cacheDuration: dayjs.duration(5, "minutes"),
queryKey: "notificationsJobStatus",
});
+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.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+3 -3
View File
@@ -26,8 +26,8 @@
"@homarr/api": "workspace:^0.1.0", "@homarr/api": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@mantine/dates": "^8.1.2", "@mantine/dates": "^8.0.1",
"next": "15.3.4", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0" "react-dom": "19.1.0"
}, },
@@ -35,7 +35,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.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+7 -7
View File
@@ -33,12 +33,12 @@
"@homarr/settings": "workspace:^0.1.0", "@homarr/settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^8.1.2", "@mantine/core": "^8.0.1",
"@mantine/hooks": "^8.1.2", "@mantine/hooks": "^8.0.1",
"@mantine/spotlight": "^8.1.2", "@mantine/spotlight": "^8.0.1",
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.33.0",
"jotai": "^2.12.5", "jotai": "^2.12.4",
"next": "15.3.4", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"use-deep-compare-effect": "^1.8.1" "use-deep-compare-effect": "^1.8.1"
@@ -47,7 +47,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.29.0", "eslint": "^9.27.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
Loaded 100 of 163 files, more files were not shown because too many files have changed in this diff. Show more