Compare commits

..
77 Commits
Author SHA1 Message Date
Releases Homarr 55cdddb274 chore(release): 1.18.0 [skip ci] 2025-04-25 19:15:32 +00:00
homarr-releases[bot] dd85f5a9d5 chore(release): automatic release v1.18.0 2025-04-25 19:14:47 +00:00
Manuel 8550c69d51 feat: migrate to unifi package (#2894) 2025-04-25 18:58:55 +00:00
3dcee8cb86 feat: add releases widget (#2497)
Co-authored-by: Andre Silva <asilva01@acuitysso.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
2025-04-25 18:49:32 +00:00
homarr-renovate[bot] d97e74047d fix(deps): update turbo monorepo to ^2.5.2 (#2949)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-25 16:24:57 +00:00
homarr-renovate[bot] e36892fc8f chore(deps): update dependency @types/node to ^22.15.2 (#2946)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-25 09:22:12 +00:00
homarr-renovate[bot] 1d28bf4c93 fix(deps): update dependency drizzle-orm to ^0.43.1 (#2947)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-25 08:26:07 +00:00
homarr-renovate[bot] 8b076ce761 chore(deps): update dependency @types/node to ^22.15.1 (#2945)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-25 04:24:44 +00:00
homarr-renovate[bot] d78b2e1e85 fix(deps): update dependency tsdav to ^2.1.4 (#2944)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-25 03:09:10 +00:00
homarr-renovate[bot] f65c0125a9 chore(deps): update dependency @types/node to ^22.15.0 (#2942)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-24 21:21:27 +00:00
homarr-renovate[bot] 253308357a fix(deps): update dependency next-auth to v5.0.0-beta.27 (#2940)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-24 17:19:21 +00:00
homarr-renovate[bot] 295cc4a3c4 fix(deps): update dependency drizzle-orm to ^0.43.0 (#2939)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-24 14:20:39 +00:00
homarr-renovate[bot] 767fe946d7 fix(deps): update dependency next-intl to v4.1.0 (#2938)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-24 09:24:32 +00:00
homarr-renovate[bot] aba963c7cf fix(deps): update mantine monorepo to ^7.17.5 (#2937)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-24 09:23:37 +00:00
homarr-crowdin[bot] 4529cabb8a chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-24 04:29:49 +00:00
homarr-renovate[bot] 7766c3cbbc fix(deps): update turbo monorepo to ^2.5.1 (#2934)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-23 23:21:49 +00:00
homarr-renovate[bot] eb95ee285b fix(deps): update dependency @t3-oss/env-nextjs to ^0.13.0 (#2935)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-23 22:22:00 +00:00
homarr-renovate[bot] 1421c381cc chore(deps): update node.js to v22.15.0 (#2932)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-23 20:22:21 +02:00
homarr-renovate[bot] 573d5f89d6 fix(deps): update dependency glob to ^11.0.2 (#2931)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-23 16:24:21 +00:00
homarr-renovate[bot] e01b909402 chore(deps): update node.js to v22.15.0 (#2928)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-23 08:27:13 +00:00
homarr-renovate[bot] bd3535dcf8 fix(deps): update trpc monorepo to ^11.1.1 (#2927)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-23 01:13:29 +00:00
homarr-crowdin[bot] c4bd513134 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-23 00:50:32 +00:00
Meier Lukas c64d903f2b feat(widgets): add title to advanced options (#2909) 2025-04-22 18:33:15 +02:00
Meier Lukas f98750d0b3 feat(media-server): add option to only show playing sessions (#2899) 2025-04-22 18:30:46 +02:00
Meier Lukas c51424717d fix(certificates): improve validation and prevent crash (#2910) 2025-04-22 18:28:58 +02:00
homarr-renovate[bot] 3172e6e0c4 fix(deps): update dependency next-intl to v4.0.3 (#2922)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-22 16:25:51 +00:00
homarr-renovate[bot] 7aa379076d fix(deps): update dependency typescript-eslint to ^8.31.0 (#2919)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-22 16:25:09 +00:00
homarr-renovate[bot] 023ffcf7bf chore(deps): update dependency eslint to ^9.25.1 (#2918)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-22 01:13:02 +00:00
homarr-renovate[bot] fed28bd775 fix(deps): update dependency sass to ^1.87.0 (#2921)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-21 23:20:50 +00:00
homarr-renovate[bot] 09718b8258 chore(deps): update vitest monorepo to ^3.1.2 (#2915)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-21 09:22:24 +00:00
homarr-renovate[bot] 6040f063ca chore(deps): update pnpm to v10.9.0 (#2916)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-21 09:21:11 +00:00
homarr-renovate[bot] 3d508088d5 fix(deps): update dependency @ctrl/qbittorrent to ^9.6.0 (#2914)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-21 03:13:01 +00:00
homarr-crowdin[bot] 37484ce032 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-21 00:52:28 +00:00
Meier Lukas ac3c2a6377 chore(issue-template): add missing homarr version 1.17.0 2025-04-21 00:34:42 +02:00
homarr-renovate[bot]andMeier Lukas 6e6edc8a0d fix(deps): update dependency next-auth to v5.0.0-beta.26 (#2906)
* fix(deps): update dependency next-auth to v5.0.0-beta.26

* fix(deps): update dependency next-auth to v5.0.0-beta.26

* fix: lint issue

* fix: format issue

* fix: login with oidc no longer working

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-04-20 12:21:33 +00:00
homarr-renovate[bot] b0d7f9a48c fix(deps): update dependency @auth/core to ^0.39.0 (#2904)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-20 08:56:24 +00:00
homarr-renovate[bot] fdf700dba5 fix(deps): update dependency @auth/drizzle-adapter to ^1.9.0 (#2905)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-20 06:24:43 +00:00
homarr-renovate[bot] 2bf67c3de8 fix(deps): update dependency trpc-to-openapi to ^2.2.0 (#2903)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-19 23:19:45 +00:00
homarr-renovate[bot] 593e3ee5a6 fix(deps): update dependency dockerode to ^4.0.6 (#2901)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-19 13:03:16 +00:00
homarr-renovate[bot] ad5da4e8ac chore(deps): update dependency @vitejs/plugin-react to ^4.4.1 (#2902)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-19 12:26:25 +00:00
Releases Homarr 58ef0960b5 chore(release): 1.17.0 [skip ci] 2025-04-18 19:14:53 +00:00
homarr-releases[bot] 68d6967d24 chore(release): automatic release v1.17.0 2025-04-18 19:14:04 +00:00
homarr-renovate[bot] e5f0bb9792 chore(deps): update dependency eslint to ^9.25.0 (#2897)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-18 18:46:49 +00:00
Jacob HemmingandMeier Lukas 3d68f4d128 feat: add api-key support for tdarr (#2890)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-04-18 20:11:51 +02:00
Jacob Hemming cfca0ba583 fix: calculate available space correctly and filter unmounted drives (#2885) 2025-04-18 10:49:59 +00:00
Meier Lukas 9570abb2c3 chore(docs): update docs-sitemap 2025-04-18 10:52:02 +02:00
homarr-renovate[bot] 5fcb7a3b9c fix(deps): update dependency recharts to ^2.15.3 (#2893)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-18 05:18:26 +00:00
homarr-crowdin[bot] a55acb9eb9 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-18 00:49:15 +00:00
homarr-renovate[bot] d37cdbd080 fix(deps): update nextjs monorepo to v15.3.1 (#2889)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-17 15:56:13 +00:00
homarr-renovate[bot] 17174b6b96 fix(deps): update tanstack-query monorepo to ^5.74.4 (#2888)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-17 09:54:31 +00:00
homarr-crowdin[bot] 0466a2808a chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-17 00:49:43 +00:00
homarr-renovate[bot] 1bfe397d68 fix(deps): update dependency zod to ^3.24.3 (#2886)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-16 22:40:20 +00:00
Meier Lukas 60de6361bb chore(docs): update docs-sitemap 2025-04-16 22:58:28 +02:00
homarr-renovate[bot] 47d7abc0d7 fix(deps): update dependency drizzle-orm to ^0.42.0 (#2882)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-16 18:51:44 +00:00
rezstjeandManuel 2d12c1e641 fix: Tdarr widget empty statistics tab (#2883)
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
2025-04-16 18:34:06 +00:00
Thomas Camlong 00053bda7b feat: Prefer primary color (#2665) 2025-04-16 11:34:33 +02:00
homarr-renovate[bot] 97390c6030 fix(deps): update dependency drizzle-kit to ^0.31.0 (#2881)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-15 13:58:27 +00:00
homarr-renovate[bot] 865a52ae7d chore(deps): update dependency @vitejs/plugin-react to ^4.4.0 (#2879)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-15 09:40:02 +00:00
homarr-renovate[bot] fdca513929 fix(deps): update dependency typescript-eslint to ^8.30.1 (#2876)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-15 01:02:52 +00:00
homarr-crowdin[bot] ce5784b6c4 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-15 00:50:50 +00:00
homarr-renovate[bot] 1a4c136d41 chore(deps): update dependency @types/react to v19.1.2 (#2874)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-14 19:17:29 +00:00
homarr-renovate[bot] 089734b293 fix(deps): update dependency typescript-eslint to ^8.30.0 (#2873)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-14 17:37:45 +00:00
homarr-renovate[bot] 2e59bfcd60 chore(deps): update dependency @types/dockerode to ^3.3.38 (#2872)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-14 15:55:56 +00:00
homarr-renovate[bot] 02fb1b9dae fix(deps): update tanstack-query monorepo to ^5.74.3 (#2871)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-14 13:09:11 +00:00
homarr-renovate[bot] bf4a1cf1e8 fix(deps): update tanstack-query monorepo to ^5.74.2 (#2869)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-14 11:39:37 +00:00
homarr-renovate[bot] 4a0fb5d0bb chore(deps): update pnpm to v10.8.1 (#2870)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-14 11:31:01 +00:00
homarr-renovate[bot] c57b63b506 fix(deps): update dependency jotai to ^2.12.3 (#2868)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-14 01:03:30 +00:00
homarr-crowdin[bot] fa0e694ce0 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-14 00:52:46 +00:00
homarr-renovate[bot] ff1263b217 fix(deps): update dependency swagger-ui-react to ^5.21.0 (#2866)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-13 19:53:29 +00:00
homarr-renovate[bot] ac78e87042 fix(deps): update tanstack-query monorepo to ^5.74.0 (#2864)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-13 18:21:57 +00:00
homarr-renovate[bot] ec7adb03b6 chore(deps): update dependency jsdom to ^26.1.0 (#2863)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-13 10:41:29 +00:00
homarr-crowdin[bot] f5f39bc094 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-13 02:16:30 +00:00
homarr-renovate[bot] 4e6ab037c9 fix(deps): update tanstack-query monorepo to ^5.73.3 (#2860)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-12 16:18:00 +00:00
Andrea a29556ee8e fix(widget): format stock values and changes for better readability (#2836) 2025-04-12 15:53:40 +00:00
homarr-crowdin[bot] 71d8f7920d chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-12 00:48:49 +00:00
homarr-renovate[bot] 835e007334 chore(deps): update dependency @types/node to ^22.14.1 (#2857)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-11 21:43:33 +00:00
Meier Lukas d46b8f63c7 chore(issue-template): add missing homarr version 1.16.0 2025-04-11 21:56:00 +02:00
157 changed files with 5384 additions and 3324 deletions

No files matched your search

+2
View File
@@ -31,6 +31,8 @@ body:
label: Version
description: What version of Homarr are you running?
options:
- 1.17.0
- 1.16.0
- 1.15.0
- 1.14.0
- 1.13.1
+1 -1
View File
@@ -1 +1 @@
22.14.0
22.15.0
+1
View File
@@ -26,6 +26,7 @@
"Sabnzbd",
"SeDemal",
"Sonarr",
"sslverify",
"superjson",
"tabler",
"trpc",
+60
View File
@@ -1,3 +1,63 @@
## [1.18.0](https://github.com/homarr-labs/homarr/compare/v1.17.0...v1.18.0) (2025-04-25)
### Features
* add releases widget ([#2497](https://github.com/homarr-labs/homarr/issues/2497)) ([3dcee8c](https://github.com/homarr-labs/homarr/commit/3dcee8cb869dd11d54631636253ad17e2af97dd7))
* **media-server:** add option to only show playing sessions ([#2899](https://github.com/homarr-labs/homarr/issues/2899)) ([f98750d](https://github.com/homarr-labs/homarr/commit/f98750d0b3e96b234d6f57b82bf7aa8729a091f2))
* migrate to unifi package ([#2894](https://github.com/homarr-labs/homarr/issues/2894)) ([8550c69](https://github.com/homarr-labs/homarr/commit/8550c69d51497676b654e204a85cdc84e8a38e30))
* **widgets:** add title to advanced options ([#2909](https://github.com/homarr-labs/homarr/issues/2909)) ([c64d903](https://github.com/homarr-labs/homarr/commit/c64d903f2b6375970325fb5031afd468dbf0ae11))
### Bug Fixes
* **certificates:** improve validation and prevent crash ([#2910](https://github.com/homarr-labs/homarr/issues/2910)) ([c514247](https://github.com/homarr-labs/homarr/commit/c51424717ddce5a2f4110658a1fbfab375185f66))
* **deps:** update dependency @auth/core to ^0.39.0 ([#2904](https://github.com/homarr-labs/homarr/issues/2904)) ([b0d7f9a](https://github.com/homarr-labs/homarr/commit/b0d7f9a48c05d380ff84ed7f48c9a9fd8dc33602))
* **deps:** update dependency @auth/drizzle-adapter to ^1.9.0 ([#2905](https://github.com/homarr-labs/homarr/issues/2905)) ([fdf700d](https://github.com/homarr-labs/homarr/commit/fdf700dba51d0bc4e72cdd63a63acb99c2196329))
* **deps:** update dependency @ctrl/qbittorrent to ^9.6.0 ([#2914](https://github.com/homarr-labs/homarr/issues/2914)) ([3d50808](https://github.com/homarr-labs/homarr/commit/3d508088d5a7d7fda53f49c196c2c6dd84d598d1))
* **deps:** update dependency @t3-oss/env-nextjs to ^0.13.0 ([#2935](https://github.com/homarr-labs/homarr/issues/2935)) ([eb95ee2](https://github.com/homarr-labs/homarr/commit/eb95ee285b727a8bda7ac9df3f75c25ced475358))
* **deps:** update dependency dockerode to ^4.0.6 ([#2901](https://github.com/homarr-labs/homarr/issues/2901)) ([593e3ee](https://github.com/homarr-labs/homarr/commit/593e3ee5a6f9cb8d50a51262c85c3c067fce0691))
* **deps:** update dependency drizzle-orm to ^0.43.0 ([#2939](https://github.com/homarr-labs/homarr/issues/2939)) ([295cc4a](https://github.com/homarr-labs/homarr/commit/295cc4a3c41ba9ad20e93df9d6ea5627d56d310d))
* **deps:** update dependency drizzle-orm to ^0.43.1 ([#2947](https://github.com/homarr-labs/homarr/issues/2947)) ([1d28bf4](https://github.com/homarr-labs/homarr/commit/1d28bf4c93c43c1956c5b386ca8a02ca6ae59eb5))
* **deps:** update dependency glob to ^11.0.2 ([#2931](https://github.com/homarr-labs/homarr/issues/2931)) ([573d5f8](https://github.com/homarr-labs/homarr/commit/573d5f89d6576020c3a4393372a56f8f75628e58))
* **deps:** update dependency next-auth to v5.0.0-beta.26 ([#2906](https://github.com/homarr-labs/homarr/issues/2906)) ([6e6edc8](https://github.com/homarr-labs/homarr/commit/6e6edc8a0d809614588967b51751257929a6ccd3))
* **deps:** update dependency next-auth to v5.0.0-beta.27 ([#2940](https://github.com/homarr-labs/homarr/issues/2940)) ([2533083](https://github.com/homarr-labs/homarr/commit/253308357a55ab2fac7ac4144215beefdc517b8d))
* **deps:** update dependency next-intl to v4.0.3 ([#2922](https://github.com/homarr-labs/homarr/issues/2922)) ([3172e6e](https://github.com/homarr-labs/homarr/commit/3172e6e0c4e55f62a7ee32ca84fcaa940e774565))
* **deps:** update dependency next-intl to v4.1.0 ([#2938](https://github.com/homarr-labs/homarr/issues/2938)) ([767fe94](https://github.com/homarr-labs/homarr/commit/767fe946d78268eddce6e213665176b8c6397e92))
* **deps:** update dependency sass to ^1.87.0 ([#2921](https://github.com/homarr-labs/homarr/issues/2921)) ([fed28bd](https://github.com/homarr-labs/homarr/commit/fed28bd77546be83b9965bd1116996b621f6b639))
* **deps:** update dependency trpc-to-openapi to ^2.2.0 ([#2903](https://github.com/homarr-labs/homarr/issues/2903)) ([2bf67c3](https://github.com/homarr-labs/homarr/commit/2bf67c3de8609eb504e322d830f507973a55d6fc))
* **deps:** update dependency tsdav to ^2.1.4 ([#2944](https://github.com/homarr-labs/homarr/issues/2944)) ([d78b2e1](https://github.com/homarr-labs/homarr/commit/d78b2e1e85a6c72ce1366e50ee4a1e83176b7913))
* **deps:** update dependency typescript-eslint to ^8.31.0 ([#2919](https://github.com/homarr-labs/homarr/issues/2919)) ([7aa3790](https://github.com/homarr-labs/homarr/commit/7aa379076da24c90238dd5fe2714dbceb3b84c2a))
* **deps:** update mantine monorepo to ^7.17.5 ([#2937](https://github.com/homarr-labs/homarr/issues/2937)) ([aba963c](https://github.com/homarr-labs/homarr/commit/aba963c7cf0d554d51e5fc233e546e4917ecdf67))
* **deps:** update trpc monorepo to ^11.1.1 ([#2927](https://github.com/homarr-labs/homarr/issues/2927)) ([bd3535d](https://github.com/homarr-labs/homarr/commit/bd3535dcf84569b87cf647bc5e0b957667b7e199))
* **deps:** update turbo monorepo to ^2.5.1 ([#2934](https://github.com/homarr-labs/homarr/issues/2934)) ([7766c3c](https://github.com/homarr-labs/homarr/commit/7766c3cbbc1187f6c38157fe15182c8b7d329acf))
* **deps:** update turbo monorepo to ^2.5.2 ([#2949](https://github.com/homarr-labs/homarr/issues/2949)) ([d97e740](https://github.com/homarr-labs/homarr/commit/d97e74047dc107abb05ca65d8a91a41c6071fbd4))
## [1.17.0](https://github.com/homarr-labs/homarr/compare/v1.16.0...v1.17.0) (2025-04-18)
### Features
* add api-key support for tdarr ([#2890](https://github.com/homarr-labs/homarr/issues/2890)) ([3d68f4d](https://github.com/homarr-labs/homarr/commit/3d68f4d1283b6cb27f9532d1adf52cef78a0fb03))
* Prefer primary color ([#2665](https://github.com/homarr-labs/homarr/issues/2665)) ([00053bd](https://github.com/homarr-labs/homarr/commit/00053bda7bbcaaf359074c3b35573ee0afc1409d))
### Bug Fixes
* calculate available space correctly and filter unmounted drives ([#2885](https://github.com/homarr-labs/homarr/issues/2885)) ([cfca0ba](https://github.com/homarr-labs/homarr/commit/cfca0ba58323ec09f8f64da2fee251ef14ba914a))
* **deps:** update dependency drizzle-kit to ^0.31.0 ([#2881](https://github.com/homarr-labs/homarr/issues/2881)) ([97390c6](https://github.com/homarr-labs/homarr/commit/97390c60300f3d16dabf0710fa435c6b3ae86a6f))
* **deps:** update dependency drizzle-orm to ^0.42.0 ([#2882](https://github.com/homarr-labs/homarr/issues/2882)) ([47d7abc](https://github.com/homarr-labs/homarr/commit/47d7abc0d739b91328834e0eaea033637632f22c))
* **deps:** update dependency jotai to ^2.12.3 ([#2868](https://github.com/homarr-labs/homarr/issues/2868)) ([c57b63b](https://github.com/homarr-labs/homarr/commit/c57b63b506733459ea0c09776917042fa878829b))
* **deps:** update dependency recharts to ^2.15.3 ([#2893](https://github.com/homarr-labs/homarr/issues/2893)) ([5fcb7a3](https://github.com/homarr-labs/homarr/commit/5fcb7a3b9c279c1b402e120a4a6f08265e8d5912))
* **deps:** update dependency swagger-ui-react to ^5.21.0 ([#2866](https://github.com/homarr-labs/homarr/issues/2866)) ([ff1263b](https://github.com/homarr-labs/homarr/commit/ff1263b217e96cbb2bc6b8e98ff117c44ca9f00d))
* **deps:** update dependency typescript-eslint to ^8.30.0 ([#2873](https://github.com/homarr-labs/homarr/issues/2873)) ([089734b](https://github.com/homarr-labs/homarr/commit/089734b293a21d6eeaa7c5df5ee9930853ed2fcf))
* **deps:** update dependency typescript-eslint to ^8.30.1 ([#2876](https://github.com/homarr-labs/homarr/issues/2876)) ([fdca513](https://github.com/homarr-labs/homarr/commit/fdca513929e9e594af2d59ad17c915cbc7ac2e08))
* **deps:** update dependency zod to ^3.24.3 ([#2886](https://github.com/homarr-labs/homarr/issues/2886)) ([1bfe397](https://github.com/homarr-labs/homarr/commit/1bfe397d68a88037e35701aab8a7b4c9027ec1e3))
* **deps:** update nextjs monorepo to v15.3.1 ([#2889](https://github.com/homarr-labs/homarr/issues/2889)) ([d37cdbd](https://github.com/homarr-labs/homarr/commit/d37cdbd080c2b3a4df536ab7dad4fc4a4b3c915f))
* **deps:** update tanstack-query monorepo to ^5.73.3 ([#2860](https://github.com/homarr-labs/homarr/issues/2860)) ([4e6ab03](https://github.com/homarr-labs/homarr/commit/4e6ab037c9245103b05afe977e9c288082a2b539))
* **deps:** update tanstack-query monorepo to ^5.74.0 ([#2864](https://github.com/homarr-labs/homarr/issues/2864)) ([ac78e87](https://github.com/homarr-labs/homarr/commit/ac78e8704287f45d6a8ca26cba9b87146e1c6161))
* **deps:** update tanstack-query monorepo to ^5.74.2 ([#2869](https://github.com/homarr-labs/homarr/issues/2869)) ([bf4a1cf](https://github.com/homarr-labs/homarr/commit/bf4a1cf1e8c99ada97adf6ee118ddaafc00115e9))
* **deps:** update tanstack-query monorepo to ^5.74.3 ([#2871](https://github.com/homarr-labs/homarr/issues/2871)) ([02fb1b9](https://github.com/homarr-labs/homarr/commit/02fb1b9dae501e475353e8e2510e4f8938c4e124))
* **deps:** update tanstack-query monorepo to ^5.74.4 ([#2888](https://github.com/homarr-labs/homarr/issues/2888)) ([17174b6](https://github.com/homarr-labs/homarr/commit/17174b6b96ae16b480e2bc5eb9f8ab7b914bc843))
* Tdarr widget empty statistics tab ([#2883](https://github.com/homarr-labs/homarr/issues/2883)) ([2d12c1e](https://github.com/homarr-labs/homarr/commit/2d12c1e641398e20a70f8bfb24a48de02498496a))
* **widget:** format stock values and changes for better readability ([#2836](https://github.com/homarr-labs/homarr/issues/2836)) ([a29556e](https://github.com/homarr-labs/homarr/commit/a29556ee8ec50aa384a9a46f6f5a5fa5dfa4cdac))
## [1.16.0](https://github.com/homarr-labs/homarr/compare/v1.15.0...v1.16.0) (2025-04-11)
### Features
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:22.14.0-alpine AS base
FROM node:22.15.0-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
+22 -22
View File
@@ -48,21 +48,21 @@
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "^7.17.4",
"@mantine/core": "^7.17.4",
"@mantine/dropzone": "^7.17.4",
"@mantine/hooks": "^7.17.4",
"@mantine/modals": "^7.17.4",
"@mantine/tiptap": "^7.17.4",
"@mantine/colors-generator": "^7.17.5",
"@mantine/core": "^7.17.5",
"@mantine/dropzone": "^7.17.5",
"@mantine/hooks": "^7.17.5",
"@mantine/modals": "^7.17.5",
"@mantine/tiptap": "^7.17.5",
"@million/lint": "1.0.14",
"@tabler/icons-react": "^3.31.0",
"@tanstack/react-query": "^5.72.2",
"@tanstack/react-query-devtools": "^5.72.2",
"@tanstack/react-query-next-experimental": "^5.72.2",
"@trpc/client": "^11.1.0",
"@trpc/next": "^11.1.0",
"@trpc/react-query": "^11.1.0",
"@trpc/server": "^11.1.0",
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-query-devtools": "^5.74.4",
"@tanstack/react-query-next-experimental": "^5.74.4",
"@trpc/client": "^11.1.1",
"@trpc/next": "^11.1.1",
"@trpc/react-query": "^11.1.1",
"@trpc/server": "^11.1.1",
"@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0",
@@ -71,34 +71,34 @@
"dayjs": "^1.11.13",
"dotenv": "^16.5.0",
"flag-icons": "^7.3.2",
"glob": "^11.0.1",
"jotai": "^2.12.2",
"glob": "^11.0.2",
"jotai": "^2.12.3",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.3.0",
"next": "15.3.1",
"postcss-preset-mantine": "^1.17.0",
"prismjs": "^1.30.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-error-boundary": "^5.0.0",
"react-simple-code-editor": "^0.14.1",
"sass": "^1.86.3",
"sass": "^1.87.0",
"superjson": "2.2.2",
"swagger-ui-react": "^5.20.8",
"swagger-ui-react": "^5.21.0",
"use-deep-compare-effect": "^1.8.1",
"zod": "^3.24.2"
"zod": "^3.24.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1",
"@types/node": "^22.14.0",
"@types/node": "^22.15.2",
"@types/prismjs": "^1.26.5",
"@types/react": "19.1.1",
"@types/react": "19.1.2",
"@types/react-dom": "19.1.2",
"@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.1.2",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"node-loader": "^2.1.0",
"prettier": "^3.5.3",
"typescript": "^5.8.3"
@@ -2,7 +2,7 @@
import type { PropsWithChildren } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import { Anchor, Button, Card, Code, Collapse, Divider, PasswordInput, Stack, Text, TextInput } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { z } from "zod";
@@ -15,6 +15,8 @@ import { showErrorNotification, showSuccessNotification } from "@homarr/notifica
import { useScopedI18n } from "@homarr/translation/client";
import { userSignInSchema } from "@homarr/validation/user";
type Provider = "credentials" | "ldap" | "oidc";
interface LoginFormProps {
providers: string[];
oidcClientName: string;
@@ -26,6 +28,8 @@ const extendedValidation = userSignInSchema.extend({ provider: z.enum(["credenti
export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, callbackUrl }: LoginFormProps) => {
const t = useScopedI18n("user");
const searchParams = useSearchParams();
const isError = searchParams.has("error");
const router = useRouter();
const [isPending, setIsPending] = useState(false);
const form = useZodForm(extendedValidation, {
@@ -39,22 +43,34 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
const credentialInputsVisible = providers.includes("credentials") || providers.includes("ldap");
const onSuccess = useCallback(
async (response: Awaited<ReturnType<typeof signIn>>) => {
if (response && (!response.ok || response.error)) {
async (provider: Provider, response: Awaited<ReturnType<typeof signIn>>) => {
if (!response.ok || response.error) {
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw response.error;
}
if (provider === "oidc") {
if (!response.url) {
showErrorNotification({
title: t("action.login.notification.error.title"),
message: t("action.login.notification.error.message"),
autoClose: 10000,
});
return;
}
router.push(response.url);
return;
}
showSuccessNotification({
title: t("action.login.notification.success.title"),
message: t("action.login.notification.success.message"),
});
// Redirect to the callback URL if the response is defined and comes from a credentials provider (ldap or credentials). oidc is redirected automatically.
if (response) {
await revalidatePathActionAsync("/");
router.push(callbackUrl);
}
await revalidatePathActionAsync("/");
router.push(callbackUrl);
},
[t, router, callbackUrl],
);
@@ -70,14 +86,14 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
}, [t]);
const signInAsync = useCallback(
async (provider: string, options?: Parameters<typeof signIn>[1]) => {
async (provider: Provider, options?: Parameters<typeof signIn>[1]) => {
setIsPending(true);
await signIn(provider, {
...options,
redirect: false,
callbackUrl: new URL(callbackUrl, window.location.href).href,
})
.then(onSuccess)
.then((response) => onSuccess(provider, response))
.catch(onError);
},
[setIsPending, onSuccess, onError, callbackUrl],
@@ -86,11 +102,12 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
const isLoginInProgress = useRef(false);
useEffect(() => {
if (isError) return;
if (isOidcAutoLoginEnabled && !isPending && !isLoginInProgress.current) {
isLoginInProgress.current = true;
void signInAsync("oidc");
}
}, [signInAsync, isOidcAutoLoginEnabled, isPending]);
}, [signInAsync, isOidcAutoLoginEnabled, isPending, isError]);
return (
<Stack gap="xl">
@@ -123,7 +123,7 @@ export const ColorSettingsContent = ({ board }: Props) => {
</Grid.Col>
</Grid>
<Group justify="end">
<Button type="submit" loading={isPending} color="teal">
<Button type="submit" loading={isPending}>
{t("common.action.saveChanges")}
</Button>
</Group>
@@ -159,7 +159,7 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
</Grid>
<Group justify="end">
<Button type="submit" loading={isPending} color="teal">
<Button type="submit" loading={isPending}>
{t("common.action.saveChanges")}
</Button>
</Group>
@@ -38,7 +38,7 @@ export const BehaviorSettingsContent = ({ board }: Props) => {
/>
<Group justify="end">
<Button type="submit" loading={isPending} color="teal">
<Button type="submit" loading={isPending}>
{t("common.action.saveChanges")}
</Button>
</Group>
@@ -46,7 +46,7 @@ export const CustomCssSettingsContent = ({ board }: Props) => {
</Alert>
<Group justify="end">
<Button type="submit" loading={isPending} color="teal">
<Button type="submit" loading={isPending}>
{t("common.action.saveChanges")}
</Button>
</Group>
@@ -116,7 +116,7 @@ export const GeneralSettingsContent = ({ board }: Props) => {
</Grid.Col>
</Grid>
<Group justify="end">
<Button type="submit" loading={isPending} color="teal">
<Button type="submit" loading={isPending}>
{t("common.action.saveChanges")}
</Button>
</Group>
@@ -105,7 +105,7 @@ export const LayoutSettingsContent = ({ board }: Props) => {
</Stack>
<Group justify="end">
<Button type="submit" loading={isPending} color="teal">
<Button type="submit" loading={isPending}>
{t("common.action.saveChanges")}
</Button>
</Group>
@@ -1,7 +1,7 @@
import { X509Certificate } from "node:crypto";
import { notFound } from "next/navigation";
import { Card, Group, SimpleGrid, Stack, Text, Title } from "@mantine/core";
import { IconCertificate, IconCertificateOff } from "@tabler/icons-react";
import { IconAlertTriangle, IconCertificate, IconCertificateOff } from "@tabler/icons-react";
import dayjs from "dayjs";
import { auth } from "@homarr/auth/next";
@@ -31,11 +31,27 @@ export default async function CertificatesPage({ params }: CertificatesPageProps
const t = await getI18n();
const certificates = await loadCustomRootCertificatesAsync();
const x509Certificates = certificates
.map((cert) => ({
...cert,
x509: new X509Certificate(cert.content),
}))
.sort((certA, certB) => certA.x509.validToDate.getTime() - certB.x509.validToDate.getTime());
.map((cert) => {
try {
const x509 = new X509Certificate(cert.content);
return {
...cert,
isError: false,
x509,
} as const;
} catch {
return {
...cert,
isError: true,
x509: null,
} as const;
}
})
.sort((certA, certB) => {
if (certA.isError) return -1;
if (certB.isError) return 1;
return certA.x509.validToDate.getTime() - certB.x509.validToDate.getTime();
});
return (
<>
@@ -57,32 +73,47 @@ export default async function CertificatesPage({ params }: CertificatesPageProps
<SimpleGrid cols={{ sm: 1, lg: 2, xl: 3 }} spacing="lg">
{x509Certificates.map((cert) => (
<Card key={cert.x509.fingerprint} withBorder>
<Card key={cert.fileName} withBorder>
<Group wrap="nowrap">
<IconCertificate
color={getMantineColor(iconColor(cert.x509.validToDate), 6)}
style={{ minWidth: 32 }}
size={32}
stroke={1.5}
/>
{cert.isError ? (
<IconAlertTriangle
color={getMantineColor("red", 6)}
style={{ minWidth: 32 }}
size={32}
stroke={1.5}
/>
) : (
<IconCertificate
color={getMantineColor(iconColor(cert.x509.validToDate), 6)}
style={{ minWidth: 32 }}
size={32}
stroke={1.5}
/>
)}
<Stack flex={1} gap="xs" maw="calc(100% - 48px)">
<Group justify="space-between" wrap="nowrap">
<Text fw={500} lineClamp={1} style={{ wordBreak: "break-all" }}>
{cert.x509.subject}
{cert.isError ? t("certificate.page.list.invalid.title") : cert.x509.subject}
</Text>
<Text c="gray.6" ta="end" size="sm">
{cert.fileName}
</Text>
</Group>
<Group justify="space-between">
<Text size="sm" c="gray.6" title={cert.x509.validToDate.toISOString()}>
{t("certificate.page.list.expires", {
when: new Intl.RelativeTimeFormat(locale).format(
dayjs(cert.x509.validToDate).diff(dayjs(), "days"),
"days",
),
})}
</Text>
{cert.isError ? (
<Text size="sm" c="gray.6">
{t("certificate.page.list.invalid.description")}
</Text>
) : (
<Text size="sm" c="gray.6" title={cert.x509.validToDate.toISOString()}>
{t("certificate.page.list.expires", {
when: new Intl.RelativeTimeFormat(locale).format(
dayjs(cert.x509.validToDate).diff(dayjs(), "days"),
"days",
),
})}
</Text>
)}
<RemoveCertificate fileName={cert.fileName} />
</Group>
</Stack>
@@ -73,7 +73,7 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
/>
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
@@ -66,7 +66,7 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS
/>
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
@@ -71,7 +71,7 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
</Radio.Group>
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
@@ -57,7 +57,7 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
/>
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
@@ -81,7 +81,7 @@ export const UserProfileForm = ({ user }: UserProfileFormProps) => {
{isProviderCredentials && (
<Group justify="end">
<Button type="submit" color="teal" disabled={!form.isDirty()} loading={isPending}>
<Button type="submit" disabled={!form.isDirty()} loading={isPending}>
{t("common.action.saveChanges")}
</Button>
</Group>
@@ -85,7 +85,7 @@ export const ChangePasswordForm = ({ user }: ChangePasswordFormProps) => {
/>
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
<Button type="submit" loading={isPending}>
{t("common.action.confirm")}
</Button>
</Group>
@@ -68,7 +68,7 @@ export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => {
{!disabled && (
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
<Button type="submit" loading={isPending}>
{t("common.action.saveChanges")}
</Button>
</Group>
@@ -40,9 +40,5 @@ export const AddGroupMember = ({ groupId, presentUserIds }: AddGroupMemberProps)
);
}, [openModal, presentUserIds, groupId, mutateAsync, tMembersAdd]);
return (
<MobileAffixButton color="teal" onClick={handleAddMember}>
{tMembersAdd("label")}
</MobileAffixButton>
);
return <MobileAffixButton onClick={handleAddMember}>{tMembersAdd("label")}</MobileAffixButton>;
};
@@ -1,7 +1,7 @@
"use client";
import { useCallback } from "react";
import type { PropsWithChildren } from "react";
import { useCallback } from "react";
import { Button, Card, Group, Switch, Text, Transition } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
@@ -108,7 +108,7 @@ export const SaveAffix = ({ groupId }: SaveAffixProps) => {
<Button disabled={isPending} onClick={form.reset}>
{t("common.action.discard")}
</Button>
<Button color="teal" loading={isPending} onClick={handleSubmit}>
<Button loading={isPending} onClick={handleSubmit}>
{t("common.action.saveChanges")}
</Button>
</Group>
@@ -71,7 +71,7 @@ export const GroupHomeBoards = ({ homeBoardId, mobileHomeBoardId, groupId }: Gro
/>
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
@@ -57,9 +57,5 @@ const AddGroup = () => {
openModal();
}, [openModal]);
return (
<MobileAffixButton onClick={handleAddGroup} color="teal">
{t("group.action.create.label")}
</MobileAffixButton>
);
return <MobileAffixButton onClick={handleAddGroup}>{t("group.action.create.label")}</MobileAffixButton>;
};
@@ -264,7 +264,7 @@ const SaveAffix = ({ visible, isPending, onDiscard, onSave }: SaveAffixProps) =>
<Button disabled={isPending} onClick={onDiscard}>
{t("common.action.discard")}
</Button>
<Button color="teal" loading={isPending} onClick={onSave}>
<Button loading={isPending} onClick={onSave}>
{t("common.action.saveChanges")}
</Button>
</Group>
@@ -89,11 +89,7 @@ const RenderTopToolbarCustomActions = () => {
openModal();
}, [openModal]);
return (
<Button color="teal" onClick={handleNewInvite}>
{t("action.new.title")}
</Button>
);
return <Button onClick={handleNewInvite}>{t("action.new.title")}</Button>;
};
const RenderRowActions = ({ row }: { row: MRT_Row<RouterOutputs["invite"]["getAll"][number]> }) => {
@@ -48,6 +48,7 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
options: reduceWidgetOptionsWithDefaultValues(kind, settings, {}),
integrationIds: [],
advancedOptions: {
title: null,
customCssClasses: [],
borderColor: "",
},
@@ -82,7 +82,7 @@ export const GroupAccessForm = <TPermission extends string>({
<Button rightSection={<IconPlus size="1rem" />} variant="light" onClick={handleAddUser}>
{t("common.action.add")}
</Button>
<Button type="submit" loading={isPending} color="teal">
<Button type="submit" loading={isPending}>
{t("permission.action.saveGroup")}
</Button>
</Group>
@@ -104,7 +104,7 @@ export const UsersAccessForm = <TPermission extends string>({
<Button rightSection={<IconPlus size="1rem" />} variant="light" onClick={handleAddUser}>
{t("common.action.add")}
</Button>
<Button type="submit" loading={isPending} color="teal">
<Button type="submit" loading={isPending}>
{t("permission.action.saveUser")}
</Button>
</Group>
@@ -29,6 +29,7 @@ export const createItemCallback =
layouts: createItemLayouts(previous, firstSection),
integrationIds: [],
advancedOptions: {
title: null,
customCssClasses: [],
borderColor: "",
},
@@ -20,7 +20,7 @@ describe("item actions duplicate-item", () => {
kind: itemKind,
integrationIds: ["1"],
options: { address: "localhost" },
advancedOptions: { customCssClasses: ["test"], borderColor: "#ff0000" },
advancedOptions: { title: "The best one", customCssClasses: ["test"], borderColor: "#ff0000" },
})
.addLayout({ layoutId, sectionId: currentSectionId, ...currentItemSize })
.build();
@@ -13,6 +13,7 @@ export class ItemMockBuilder {
layouts: [],
integrationIds: [],
advancedOptions: {
title: null,
customCssClasses: [],
borderColor: "",
},
@@ -0,0 +1,12 @@
.badge {
@mixin dark {
--background-color: rgb(from var(--mantine-color-dark-6) r g b / var(--opacity));
--border-color: rgb(from var(--mantine-color-dark-4) r g b / var(--opacity));
}
@mixin light {
--background-color: rgb(from var(--mantine-color-white) r g b / var(--opacity));
--border-color: rgb(from var(--mantine-color-gray-3) r g b / var(--opacity));
}
background-color: var(--background-color) !important;
border-color: var(--border-color) !important;
}
@@ -1,4 +1,4 @@
import { Card } from "@mantine/core";
import { Badge, Card } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { QueryErrorResetBoundary } from "@tanstack/react-query";
import combineClasses from "clsx";
@@ -14,6 +14,7 @@ import { WidgetError } from "@homarr/widgets/errors";
import type { SectionItem } from "~/app/[locale]/boards/_types";
import classes from "../sections/item.module.css";
import { useItemActions } from "./item-actions";
import itemContentClasses from "./item-content.module.css";
import { BoardItemMenu } from "./item-menu";
interface BoardItemContentProps {
@@ -25,28 +26,51 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
const board = useRequiredBoard();
return (
<Card
ref={ref}
className={combineClasses(
classes.itemCard,
`${item.kind}-wrapper`,
"grid-stack-item-content",
item.advancedOptions.customCssClasses.join(" "),
<>
<Card
ref={ref}
className={combineClasses(
classes.itemCard,
`${item.kind}-wrapper`,
"grid-stack-item-content",
item.advancedOptions.customCssClasses.join(" "),
)}
radius={board.itemRadius}
withBorder
styles={{
root: {
"--opacity": board.opacity / 100,
containerType: "size",
overflow: item.kind === "iframe" ? "hidden" : undefined,
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
},
}}
p={0}
>
<InnerContent item={item} width={width} height={height} />
</Card>
{item.advancedOptions.title?.trim() && (
<Badge
pos="absolute"
// It's 4 because of the mantine-react-table that has z-index 3
style={{ zIndex: 4 }}
top={2}
left={16}
size="xs"
radius={board.itemRadius}
styles={{
root: {
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
"--opacity": board.opacity / 100,
},
}}
className={itemContentClasses.badge}
c="var(--mantine-color-text)"
>
{item.advancedOptions.title}
</Badge>
)}
radius={board.itemRadius}
withBorder
styles={{
root: {
"--opacity": board.opacity / 100,
containerType: "size",
overflow: item.kind === "iframe" ? "hidden" : undefined,
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
},
}}
p={0}
>
<InnerContent item={item} width={width} height={height} />
</Card>
</>
);
};
@@ -40,9 +40,7 @@ export const CategoryEditModal = createModal<InnerProps>(({ actions, innerProps
<Button onClick={actions.closeModal} variant="subtle" color="gray">
{t("common.action.cancel")}
</Button>
<Button type="submit" color="teal">
{innerProps.submitLabel}
</Button>
<Button type="submit">{innerProps.submitLabel}</Button>
</Group>
</Stack>
</form>
@@ -48,9 +48,7 @@ export const DynamicSectionEditModal = createModal<ModalProps>(({ actions, inner
<Button onClick={actions.closeModal} variant="subtle" color="gray">
{t("common.action.cancel")}
</Button>
<Button type="submit" color="teal">
{t("common.action.saveChanges")}
</Button>
<Button type="submit">{t("common.action.saveChanges")}</Button>
</Group>
</Group>
</Stack>
+3 -3
View File
@@ -10,7 +10,7 @@
"main": "./src/main.ts",
"types": "./src/main.ts",
"scripts": {
"build": "esbuild src/main.ts --bundle --platform=node --loader:.scss=text --external:@opentelemetry/api --outfile=tasks.cjs",
"build": "esbuild src/main.ts --bundle --platform=node --loader:.scss=text --external:@opentelemetry/api --external:http-cookie-agent --outfile=tasks.cjs",
"clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts",
"format": "prettier --check . --ignore-path ../../.gitignore",
@@ -44,9 +44,9 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.14.0",
"@types/node": "^22.15.2",
"dotenv-cli": "^8.0.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"prettier": "^3.5.3",
"tsx": "4.19.3",
"typescript": "^5.8.3"
+2 -2
View File
@@ -7,7 +7,7 @@
"main": "./src/main.ts",
"types": "./src/main.ts",
"scripts": {
"build": "esbuild src/main.ts --bundle --platform=node --outfile=wssServer.cjs --external:bcrypt --external:@opentelemetry/api --external:cpu-features --loader:.html=text --loader:.scss=text --loader:.node=text",
"build": "esbuild src/main.ts --bundle --platform=node --outfile=wssServer.cjs --external:bcrypt --external:@opentelemetry/api --external:http-cookie-agent --external:cpu-features --loader:.html=text --loader:.scss=text --loader:.node=text",
"clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts",
"format": "prettier --check . --ignore-path ../../.gitignore",
@@ -34,7 +34,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.1",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"prettier": "^3.5.3",
"typescript": "^5.8.3"
}
+10 -10
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.16.0",
"version": "1.18.0",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -38,24 +38,24 @@
"@semantic-release/github": "^11.0.1",
"@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.5.0",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^3.1.1",
"@vitest/ui": "^3.1.1",
"@turbo/gen": "^2.5.2",
"@vitejs/plugin-react": "^4.4.1",
"@vitest/coverage-v8": "^3.1.2",
"@vitest/ui": "^3.1.2",
"conventional-changelog-conventionalcommits": "^8.0.0",
"cross-env": "^7.0.3",
"jsdom": "^26.0.0",
"jsdom": "^26.1.0",
"prettier": "^3.5.3",
"semantic-release": "^24.2.3",
"testcontainers": "^10.24.2",
"turbo": "^2.5.0",
"turbo": "^2.5.2",
"typescript": "^5.8.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.1"
"vitest": "^3.1.2"
},
"packageManager": "pnpm@10.8.0",
"packageManager": "pnpm@10.9.0",
"engines": {
"node": ">=22.14.0"
"node": ">=22.15.0"
},
"pnpm": {
"onlyBuiltDependencies": [
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+7 -7
View File
@@ -41,23 +41,23 @@
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.1.2",
"@trpc/client": "^11.1.0",
"@trpc/react-query": "^11.1.0",
"@trpc/server": "^11.1.0",
"@trpc/client": "^11.1.1",
"@trpc/react-query": "^11.1.1",
"@trpc/server": "^11.1.1",
"lodash.clonedeep": "^4.5.0",
"next": "15.3.0",
"next": "15.3.1",
"pretty-print-error": "^1.1.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"superjson": "2.2.2",
"trpc-to-openapi": "^2.1.5",
"zod": "^3.24.2"
"trpc-to-openapi": "^2.2.0",
"zod": "^3.24.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"prettier": "^3.5.3",
"typescript": "^5.8.3"
}
@@ -1,3 +1,5 @@
import { X509Certificate } from "node:crypto";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { zfd } from "zod-form-data";
@@ -16,6 +18,17 @@ export const certificateRouter = createTRPCRouter({
)
.mutation(async ({ input }) => {
const content = await input.file.text();
// Validate the certificate
try {
new X509Certificate(content);
} catch {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid certificate",
});
}
await addCustomRootCertificateAsync(input.file.name, content);
}),
removeCertificate: permissionRequiredProcedure
@@ -101,6 +101,18 @@ const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedInteg
);
if (onlyFormSecretsKindOptions.length >= 1) {
// If the first option is no secret it would always be selected even if we want to have a secret
if (
onlyFormSecretsKindOptions.length >= 2 &&
onlyFormSecretsKindOptions.some((secretKinds) => secretKinds.length === 0)
) {
return (
// Will never be undefined because of the check above
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
onlyFormSecretsKindOptions.find((secretKinds) => secretKinds.length >= 1) ?? onlyFormSecretsKindOptions[0]!
);
}
// Will never be undefined because of the check above
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return onlyFormSecretsKindOptions[0]!;
+2
View File
@@ -12,6 +12,7 @@ import { minecraftRouter } from "./minecraft";
import { networkControllerRouter } from "./network-controller";
import { notebookRouter } from "./notebook";
import { optionsRouter } from "./options";
import { releasesRouter } from "./releases";
import { rssFeedRouter } from "./rssFeed";
import { smartHomeRouter } from "./smart-home";
import { stockPriceRouter } from "./stocks";
@@ -34,5 +35,6 @@ export const widgetRouter = createTRPCRouter({
mediaTranscoding: mediaTranscodingRouter,
minecraft: minecraftRouter,
options: optionsRouter,
releases: releasesRouter,
networkController: networkControllerRouter,
});
@@ -1,4 +1,5 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import type { StreamSession } from "@homarr/integrations";
@@ -14,10 +15,13 @@ const createMediaServerIntegrationMiddleware = (action: IntegrationAction) =>
export const mediaServerRouter = createTRPCRouter({
getCurrentStreams: publicProcedure
.unstable_concat(createMediaServerIntegrationMiddleware("query"))
.query(async ({ ctx }) => {
.input(z.object({ showOnlyPlaying: z.boolean() }))
.query(async ({ ctx, input }) => {
return await Promise.all(
ctx.integrations.map(async (integration) => {
const innerHandler = mediaServerRequestHandler.handler(integration, {});
const innerHandler = mediaServerRequestHandler.handler(integration, {
showOnlyPlaying: input.showOnlyPlaying,
});
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return {
integrationId: integration.id,
@@ -29,11 +33,14 @@ export const mediaServerRouter = createTRPCRouter({
}),
subscribeToCurrentStreams: publicProcedure
.unstable_concat(createMediaServerIntegrationMiddleware("query"))
.subscription(({ ctx }) => {
.input(z.object({ showOnlyPlaying: z.boolean() }))
.subscription(({ ctx, input }) => {
return observable<{ integrationId: string; data: StreamSession[] }>((emit) => {
const unsubscribes: (() => void)[] = [];
for (const integration of ctx.integrations) {
const innerHandler = mediaServerRequestHandler.handler(integration, {});
const innerHandler = mediaServerRequestHandler.handler(integration, {
showOnlyPlaying: input.showOnlyPlaying,
});
const unsubscribe = innerHandler.subscribe((sessions) => {
emit.next({
@@ -0,0 +1,53 @@
import { escapeForRegEx } from "@tiptap/react";
import { z } from "zod";
import { releasesRequestHandler } from "@homarr/request-handler/releases";
import { createTRPCRouter, publicProcedure } from "../../trpc";
const formatVersionFilterRegex = (versionFilter: z.infer<typeof _releaseVersionFilterSchema> | undefined) => {
if (!versionFilter) return undefined;
const escapedPrefix = versionFilter.prefix ? escapeForRegEx(versionFilter.prefix) : "";
const precision = "[0-9]+\\.".repeat(versionFilter.precision).slice(0, -2);
const escapedSuffix = versionFilter.suffix ? escapeForRegEx(versionFilter.suffix) : "";
return `^${escapedPrefix}${precision}${escapedSuffix}$`;
};
const _releaseVersionFilterSchema = z.object({
prefix: z.string().optional(),
precision: z.number(),
suffix: z.string().optional(),
});
export const releasesRouter = createTRPCRouter({
getLatest: publicProcedure
.input(
z.object({
repositories: z.array(
z.object({
providerKey: z.string(),
identifier: z.string(),
versionFilter: _releaseVersionFilterSchema.optional(),
}),
),
}),
)
.query(async ({ input }) => {
const result = await Promise.all(
input.repositories.map(async (repository) => {
const innerHandler = releasesRequestHandler.handler({
providerKey: repository.providerKey,
identifier: repository.identifier,
versionRegex: formatVersionFilterRegex(repository.versionFilter),
});
return await innerHandler.getCachedOrUpdatedDataAsync({
forceUpdate: false,
});
}),
);
return result;
}),
});
+6 -6
View File
@@ -23,8 +23,8 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@auth/core": "^0.38.0",
"@auth/drizzle-adapter": "^1.8.0",
"@auth/core": "^0.39.0",
"@auth/drizzle-adapter": "^1.9.0",
"@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
@@ -35,12 +35,12 @@
"bcrypt": "^5.1.1",
"cookies": "^0.9.1",
"ldapts": "7.4.0",
"next": "15.3.0",
"next-auth": "5.0.0-beta.25",
"next": "15.3.1",
"next-auth": "5.0.0-beta.27",
"pretty-print-error": "^1.1.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "^3.24.2"
"zod": "^3.24.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -48,7 +48,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "5.0.2",
"@types/cookies": "0.9.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"prettier": "^3.5.3",
"typescript": "^5.8.3"
}
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+1 -1
View File
@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ export const loadCustomRootCertificatesAsync = async () => {
const dirContent = await fs.readdir(folder);
return await Promise.all(
dirContent
.filter((file) => file.endsWith(".crt"))
.filter((file) => file.endsWith(".crt") || file.endsWith(".pem"))
.map(async (file) => ({
content: await fs.readFile(path.join(folder, file), "utf8"),
fileName: file,
+1 -1
View File
@@ -34,7 +34,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+3 -3
View File
@@ -30,17 +30,17 @@
"@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"dayjs": "^1.11.13",
"next": "15.3.0",
"next": "15.3.1",
"react": "19.1.0",
"react-dom": "19.1.0",
"undici": "7.8.0",
"zod": "^3.24.2"
"zod": "^3.24.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+1 -1
View File
@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+1 -1
View File
@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+1 -1
View File
@@ -44,7 +44,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
@@ -8,7 +8,7 @@ export const mediaServerJob = createCronJob("mediaServer", EVERY_5_SECONDS).with
createRequestIntegrationJobHandler(mediaServerRequestHandler.handler, {
widgetKinds: ["mediaServer"],
getInput: {
mediaServer: () => ({}),
mediaServer: ({ showOnlyPlaying }) => ({ showOnlyPlaying }),
},
}),
);
+5 -5
View File
@@ -38,19 +38,19 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@auth/core": "^0.38.0",
"@auth/core": "^0.39.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^7.17.4",
"@mantine/core": "^7.17.5",
"@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^10.24.2",
"better-sqlite3": "^11.9.1",
"dotenv": "^16.5.0",
"drizzle-kit": "^0.30.6",
"drizzle-orm": "^0.41.0",
"drizzle-kit": "^0.31.0",
"drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.7.1",
"mysql2": "3.14.0"
},
@@ -60,7 +60,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "7.6.13",
"dotenv-cli": "^8.0.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"prettier": "^3.5.3",
"tsx": "4.19.3",
"typescript": "^5.8.3"
+1 -1
View File
@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
@@ -37,883 +37,6 @@ export type HomarrDocumentationPath =
| "/blog/tags/version"
| "/blog/translations"
| "/search"
| "/docs/1.10.0/tags"
| "/docs/1.10.0/tags/active-directory"
| "/docs/1.10.0/tags/ad-guard"
| "/docs/1.10.0/tags/ad-guard-home"
| "/docs/1.10.0/tags/administration"
| "/docs/1.10.0/tags/advanced"
| "/docs/1.10.0/tags/analytics"
| "/docs/1.10.0/tags/api"
| "/docs/1.10.0/tags/apps"
| "/docs/1.10.0/tags/banner"
| "/docs/1.10.0/tags/blocking"
| "/docs/1.10.0/tags/boards"
| "/docs/1.10.0/tags/bookmark"
| "/docs/1.10.0/tags/bookmarks"
| "/docs/1.10.0/tags/caddy"
| "/docs/1.10.0/tags/certificates"
| "/docs/1.10.0/tags/checklist"
| "/docs/1.10.0/tags/code"
| "/docs/1.10.0/tags/community"
| "/docs/1.10.0/tags/configuration"
| "/docs/1.10.0/tags/connections"
| "/docs/1.10.0/tags/customization"
| "/docs/1.10.0/tags/data-sources"
| "/docs/1.10.0/tags/database"
| "/docs/1.10.0/tags/developer"
| "/docs/1.10.0/tags/development"
| "/docs/1.10.0/tags/dns"
| "/docs/1.10.0/tags/docker"
| "/docs/1.10.0/tags/donation"
| "/docs/1.10.0/tags/edit-mode"
| "/docs/1.10.0/tags/env"
| "/docs/1.10.0/tags/environment-variables"
| "/docs/1.10.0/tags/feeds"
| "/docs/1.10.0/tags/getting-started"
| "/docs/1.10.0/tags/google"
| "/docs/1.10.0/tags/grafana"
| "/docs/1.10.0/tags/groups"
| "/docs/1.10.0/tags/hardware"
| "/docs/1.10.0/tags/health"
| "/docs/1.10.0/tags/help"
| "/docs/1.10.0/tags/icon-picker"
| "/docs/1.10.0/tags/icon-repositories"
| "/docs/1.10.0/tags/icons"
| "/docs/1.10.0/tags/iframe"
| "/docs/1.10.0/tags/images"
| "/docs/1.10.0/tags/installation"
| "/docs/1.10.0/tags/integrade"
| "/docs/1.10.0/tags/integration"
| "/docs/1.10.0/tags/integrations"
| "/docs/1.10.0/tags/interface"
| "/docs/1.10.0/tags/jellyserr"
| "/docs/1.10.0/tags/layout"
| "/docs/1.10.0/tags/ldap"
| "/docs/1.10.0/tags/links"
| "/docs/1.10.0/tags/lists"
| "/docs/1.10.0/tags/management"
| "/docs/1.10.0/tags/media"
| "/docs/1.10.0/tags/minecraft"
| "/docs/1.10.0/tags/monitoring"
| "/docs/1.10.0/tags/news"
| "/docs/1.10.0/tags/notebook"
| "/docs/1.10.0/tags/notes"
| "/docs/1.10.0/tags/oidc"
| "/docs/1.10.0/tags/open-collective"
| "/docs/1.10.0/tags/open-media-vault"
| "/docs/1.10.0/tags/overseerr"
| "/docs/1.10.0/tags/permissions"
| "/docs/1.10.0/tags/pgid"
| "/docs/1.10.0/tags/pi-hole"
| "/docs/1.10.0/tags/ping"
| "/docs/1.10.0/tags/programming"
| "/docs/1.10.0/tags/proxmox"
| "/docs/1.10.0/tags/proxy"
| "/docs/1.10.0/tags/puid"
| "/docs/1.10.0/tags/responsive"
| "/docs/1.10.0/tags/roles"
| "/docs/1.10.0/tags/rss"
| "/docs/1.10.0/tags/search"
| "/docs/1.10.0/tags/search-engines"
| "/docs/1.10.0/tags/security"
| "/docs/1.10.0/tags/self-signed"
| "/docs/1.10.0/tags/seo"
| "/docs/1.10.0/tags/server"
| "/docs/1.10.0/tags/settings"
| "/docs/1.10.0/tags/sinkhole"
| "/docs/1.10.0/tags/sso"
| "/docs/1.10.0/tags/system"
| "/docs/1.10.0/tags/table"
| "/docs/1.10.0/tags/technical-documentation"
| "/docs/1.10.0/tags/text"
| "/docs/1.10.0/tags/torrent"
| "/docs/1.10.0/tags/traefik"
| "/docs/1.10.0/tags/translations"
| "/docs/1.10.0/tags/unraid"
| "/docs/1.10.0/tags/uploads"
| "/docs/1.10.0/tags/usenet"
| "/docs/1.10.0/tags/users"
| "/docs/1.10.0/tags/variables"
| "/docs/1.10.0/tags/widgets"
| "/docs/1.10.0/advanced/command-line"
| "/docs/1.10.0/advanced/command-line/fix-usernames"
| "/docs/1.10.0/advanced/command-line/password-recovery"
| "/docs/1.10.0/advanced/development/getting-started"
| "/docs/1.10.0/advanced/environment-variables"
| "/docs/1.10.0/advanced/icons"
| "/docs/1.10.0/advanced/keyboard-shortcuts"
| "/docs/1.10.0/advanced/proxy"
| "/docs/1.10.0/advanced/running-as-different-user"
| "/docs/1.10.0/advanced/single-sign-on"
| "/docs/1.10.0/category/advanced"
| "/docs/1.10.0/category/community"
| "/docs/1.10.0/category/developer-guides"
| "/docs/1.10.0/category/getting-started"
| "/docs/1.10.0/category/installation"
| "/docs/1.10.0/category/installation-1"
| "/docs/1.10.0/category/integrations"
| "/docs/1.10.0/category/management"
| "/docs/1.10.0/category/widgets"
| "/docs/1.10.0/community/donate"
| "/docs/1.10.0/community/faq"
| "/docs/1.10.0/community/get-in-touch"
| "/docs/1.10.0/community/license"
| "/docs/1.10.0/community/translations"
| "/docs/1.10.0/getting-started"
| "/docs/1.10.0/getting-started/after-the-installation"
| "/docs/1.10.0/getting-started/glossary"
| "/docs/1.10.0/getting-started/installation/docker"
| "/docs/1.10.0/getting-started/installation/easy-panel"
| "/docs/1.10.0/getting-started/installation/helm"
| "/docs/1.10.0/getting-started/installation/home-assistant"
| "/docs/1.10.0/getting-started/installation/portainer"
| "/docs/1.10.0/getting-started/installation/qnap"
| "/docs/1.10.0/getting-started/installation/railway"
| "/docs/1.10.0/getting-started/installation/saltbox"
| "/docs/1.10.0/getting-started/installation/source"
| "/docs/1.10.0/getting-started/installation/synology"
| "/docs/1.10.0/getting-started/installation/unraid"
| "/docs/1.10.0/integrations/containers"
| "/docs/1.10.0/integrations/dns"
| "/docs/1.10.0/integrations/hardware"
| "/docs/1.10.0/integrations/media-requester"
| "/docs/1.10.0/integrations/media-server"
| "/docs/1.10.0/integrations/servarr"
| "/docs/1.10.0/integrations/torrent"
| "/docs/1.10.0/integrations/usenet"
| "/docs/1.10.0/management/api"
| "/docs/1.10.0/management/apps"
| "/docs/1.10.0/management/boards"
| "/docs/1.10.0/management/certificates"
| "/docs/1.10.0/management/integrations"
| "/docs/1.10.0/management/media"
| "/docs/1.10.0/management/search-engines"
| "/docs/1.10.0/management/settings"
| "/docs/1.10.0/management/users"
| "/docs/1.10.0/widgets/bookmarks"
| "/docs/1.10.0/widgets/calendar"
| "/docs/1.10.0/widgets/clock"
| "/docs/1.10.0/widgets/dns-hole"
| "/docs/1.10.0/widgets/downloads"
| "/docs/1.10.0/widgets/health-monitoring"
| "/docs/1.10.0/widgets/home-assistant"
| "/docs/1.10.0/widgets/iframe"
| "/docs/1.10.0/widgets/indexer-manager"
| "/docs/1.10.0/widgets/media-requests"
| "/docs/1.10.0/widgets/media-server"
| "/docs/1.10.0/widgets/minecraft-server-status"
| "/docs/1.10.0/widgets/notebook"
| "/docs/1.10.0/widgets/rss"
| "/docs/1.10.0/widgets/video"
| "/docs/1.10.0/widgets/weather"
| "/docs/1.11.0/tags"
| "/docs/1.11.0/tags/active-directory"
| "/docs/1.11.0/tags/ad-guard"
| "/docs/1.11.0/tags/ad-guard-home"
| "/docs/1.11.0/tags/administration"
| "/docs/1.11.0/tags/advanced"
| "/docs/1.11.0/tags/analytics"
| "/docs/1.11.0/tags/api"
| "/docs/1.11.0/tags/apps"
| "/docs/1.11.0/tags/banner"
| "/docs/1.11.0/tags/blocking"
| "/docs/1.11.0/tags/boards"
| "/docs/1.11.0/tags/bookmark"
| "/docs/1.11.0/tags/bookmarks"
| "/docs/1.11.0/tags/caddy"
| "/docs/1.11.0/tags/certificates"
| "/docs/1.11.0/tags/checklist"
| "/docs/1.11.0/tags/code"
| "/docs/1.11.0/tags/community"
| "/docs/1.11.0/tags/configuration"
| "/docs/1.11.0/tags/connections"
| "/docs/1.11.0/tags/customization"
| "/docs/1.11.0/tags/data-sources"
| "/docs/1.11.0/tags/database"
| "/docs/1.11.0/tags/developer"
| "/docs/1.11.0/tags/development"
| "/docs/1.11.0/tags/dns"
| "/docs/1.11.0/tags/docker"
| "/docs/1.11.0/tags/donation"
| "/docs/1.11.0/tags/edit-mode"
| "/docs/1.11.0/tags/env"
| "/docs/1.11.0/tags/environment-variables"
| "/docs/1.11.0/tags/feeds"
| "/docs/1.11.0/tags/getting-started"
| "/docs/1.11.0/tags/google"
| "/docs/1.11.0/tags/grafana"
| "/docs/1.11.0/tags/groups"
| "/docs/1.11.0/tags/hardware"
| "/docs/1.11.0/tags/health"
| "/docs/1.11.0/tags/help"
| "/docs/1.11.0/tags/icon-picker"
| "/docs/1.11.0/tags/icon-repositories"
| "/docs/1.11.0/tags/icons"
| "/docs/1.11.0/tags/iframe"
| "/docs/1.11.0/tags/images"
| "/docs/1.11.0/tags/installation"
| "/docs/1.11.0/tags/integrade"
| "/docs/1.11.0/tags/integration"
| "/docs/1.11.0/tags/integrations"
| "/docs/1.11.0/tags/interface"
| "/docs/1.11.0/tags/jellyserr"
| "/docs/1.11.0/tags/layout"
| "/docs/1.11.0/tags/ldap"
| "/docs/1.11.0/tags/links"
| "/docs/1.11.0/tags/lists"
| "/docs/1.11.0/tags/management"
| "/docs/1.11.0/tags/media"
| "/docs/1.11.0/tags/minecraft"
| "/docs/1.11.0/tags/monitoring"
| "/docs/1.11.0/tags/news"
| "/docs/1.11.0/tags/notebook"
| "/docs/1.11.0/tags/notes"
| "/docs/1.11.0/tags/oidc"
| "/docs/1.11.0/tags/open-collective"
| "/docs/1.11.0/tags/open-media-vault"
| "/docs/1.11.0/tags/overseerr"
| "/docs/1.11.0/tags/permissions"
| "/docs/1.11.0/tags/pgid"
| "/docs/1.11.0/tags/pi-hole"
| "/docs/1.11.0/tags/ping"
| "/docs/1.11.0/tags/programming"
| "/docs/1.11.0/tags/proxmox"
| "/docs/1.11.0/tags/proxy"
| "/docs/1.11.0/tags/puid"
| "/docs/1.11.0/tags/responsive"
| "/docs/1.11.0/tags/roles"
| "/docs/1.11.0/tags/rss"
| "/docs/1.11.0/tags/search"
| "/docs/1.11.0/tags/search-engines"
| "/docs/1.11.0/tags/security"
| "/docs/1.11.0/tags/self-signed"
| "/docs/1.11.0/tags/seo"
| "/docs/1.11.0/tags/server"
| "/docs/1.11.0/tags/settings"
| "/docs/1.11.0/tags/sinkhole"
| "/docs/1.11.0/tags/sso"
| "/docs/1.11.0/tags/system"
| "/docs/1.11.0/tags/table"
| "/docs/1.11.0/tags/technical-documentation"
| "/docs/1.11.0/tags/text"
| "/docs/1.11.0/tags/torrent"
| "/docs/1.11.0/tags/traefik"
| "/docs/1.11.0/tags/translations"
| "/docs/1.11.0/tags/unraid"
| "/docs/1.11.0/tags/uploads"
| "/docs/1.11.0/tags/usenet"
| "/docs/1.11.0/tags/users"
| "/docs/1.11.0/tags/variables"
| "/docs/1.11.0/tags/widgets"
| "/docs/1.11.0/advanced/command-line"
| "/docs/1.11.0/advanced/command-line/fix-usernames"
| "/docs/1.11.0/advanced/command-line/password-recovery"
| "/docs/1.11.0/advanced/development/getting-started"
| "/docs/1.11.0/advanced/development/kubernetes"
| "/docs/1.11.0/advanced/environment-variables"
| "/docs/1.11.0/advanced/icons"
| "/docs/1.11.0/advanced/keyboard-shortcuts"
| "/docs/1.11.0/advanced/proxy"
| "/docs/1.11.0/advanced/running-as-different-user"
| "/docs/1.11.0/advanced/single-sign-on"
| "/docs/1.11.0/category/advanced"
| "/docs/1.11.0/category/community"
| "/docs/1.11.0/category/developer-guides"
| "/docs/1.11.0/category/getting-started"
| "/docs/1.11.0/category/installation"
| "/docs/1.11.0/category/installation-1"
| "/docs/1.11.0/category/integrations"
| "/docs/1.11.0/category/management"
| "/docs/1.11.0/category/widgets"
| "/docs/1.11.0/community/donate"
| "/docs/1.11.0/community/faq"
| "/docs/1.11.0/community/get-in-touch"
| "/docs/1.11.0/community/license"
| "/docs/1.11.0/community/translations"
| "/docs/1.11.0/getting-started"
| "/docs/1.11.0/getting-started/after-the-installation"
| "/docs/1.11.0/getting-started/glossary"
| "/docs/1.11.0/getting-started/installation/docker"
| "/docs/1.11.0/getting-started/installation/easy-panel"
| "/docs/1.11.0/getting-started/installation/helm"
| "/docs/1.11.0/getting-started/installation/home-assistant"
| "/docs/1.11.0/getting-started/installation/portainer"
| "/docs/1.11.0/getting-started/installation/qnap"
| "/docs/1.11.0/getting-started/installation/railway"
| "/docs/1.11.0/getting-started/installation/saltbox"
| "/docs/1.11.0/getting-started/installation/source"
| "/docs/1.11.0/getting-started/installation/synology"
| "/docs/1.11.0/getting-started/installation/unraid"
| "/docs/1.11.0/integrations/containers"
| "/docs/1.11.0/integrations/dns"
| "/docs/1.11.0/integrations/hardware"
| "/docs/1.11.0/integrations/kubernetes"
| "/docs/1.11.0/integrations/media-requester"
| "/docs/1.11.0/integrations/media-server"
| "/docs/1.11.0/integrations/servarr"
| "/docs/1.11.0/integrations/torrent"
| "/docs/1.11.0/integrations/usenet"
| "/docs/1.11.0/management/api"
| "/docs/1.11.0/management/apps"
| "/docs/1.11.0/management/boards"
| "/docs/1.11.0/management/certificates"
| "/docs/1.11.0/management/integrations"
| "/docs/1.11.0/management/media"
| "/docs/1.11.0/management/search-engines"
| "/docs/1.11.0/management/settings"
| "/docs/1.11.0/management/users"
| "/docs/1.11.0/widgets/bookmarks"
| "/docs/1.11.0/widgets/calendar"
| "/docs/1.11.0/widgets/clock"
| "/docs/1.11.0/widgets/dns-hole"
| "/docs/1.11.0/widgets/downloads"
| "/docs/1.11.0/widgets/health-monitoring"
| "/docs/1.11.0/widgets/home-assistant"
| "/docs/1.11.0/widgets/iframe"
| "/docs/1.11.0/widgets/indexer-manager"
| "/docs/1.11.0/widgets/media-requests"
| "/docs/1.11.0/widgets/media-server"
| "/docs/1.11.0/widgets/minecraft-server-status"
| "/docs/1.11.0/widgets/notebook"
| "/docs/1.11.0/widgets/rss"
| "/docs/1.11.0/widgets/video"
| "/docs/1.11.0/widgets/weather"
| "/docs/1.12.0/tags"
| "/docs/1.12.0/tags/active-directory"
| "/docs/1.12.0/tags/ad-guard"
| "/docs/1.12.0/tags/ad-guard-home"
| "/docs/1.12.0/tags/administration"
| "/docs/1.12.0/tags/advanced"
| "/docs/1.12.0/tags/analytics"
| "/docs/1.12.0/tags/api"
| "/docs/1.12.0/tags/apps"
| "/docs/1.12.0/tags/banner"
| "/docs/1.12.0/tags/blocking"
| "/docs/1.12.0/tags/boards"
| "/docs/1.12.0/tags/bookmark"
| "/docs/1.12.0/tags/bookmarks"
| "/docs/1.12.0/tags/caddy"
| "/docs/1.12.0/tags/certificates"
| "/docs/1.12.0/tags/checklist"
| "/docs/1.12.0/tags/code"
| "/docs/1.12.0/tags/community"
| "/docs/1.12.0/tags/configuration"
| "/docs/1.12.0/tags/connections"
| "/docs/1.12.0/tags/customization"
| "/docs/1.12.0/tags/data-sources"
| "/docs/1.12.0/tags/database"
| "/docs/1.12.0/tags/developer"
| "/docs/1.12.0/tags/development"
| "/docs/1.12.0/tags/dns"
| "/docs/1.12.0/tags/docker"
| "/docs/1.12.0/tags/donation"
| "/docs/1.12.0/tags/edit-mode"
| "/docs/1.12.0/tags/env"
| "/docs/1.12.0/tags/environment-variables"
| "/docs/1.12.0/tags/feeds"
| "/docs/1.12.0/tags/finance"
| "/docs/1.12.0/tags/getting-started"
| "/docs/1.12.0/tags/google"
| "/docs/1.12.0/tags/grafana"
| "/docs/1.12.0/tags/groups"
| "/docs/1.12.0/tags/hardware"
| "/docs/1.12.0/tags/health"
| "/docs/1.12.0/tags/help"
| "/docs/1.12.0/tags/icon-picker"
| "/docs/1.12.0/tags/icon-repositories"
| "/docs/1.12.0/tags/icons"
| "/docs/1.12.0/tags/iframe"
| "/docs/1.12.0/tags/images"
| "/docs/1.12.0/tags/installation"
| "/docs/1.12.0/tags/integrade"
| "/docs/1.12.0/tags/integration"
| "/docs/1.12.0/tags/integrations"
| "/docs/1.12.0/tags/interface"
| "/docs/1.12.0/tags/jellyserr"
| "/docs/1.12.0/tags/layout"
| "/docs/1.12.0/tags/ldap"
| "/docs/1.12.0/tags/links"
| "/docs/1.12.0/tags/lists"
| "/docs/1.12.0/tags/management"
| "/docs/1.12.0/tags/market"
| "/docs/1.12.0/tags/media"
| "/docs/1.12.0/tags/minecraft"
| "/docs/1.12.0/tags/monitoring"
| "/docs/1.12.0/tags/news"
| "/docs/1.12.0/tags/notebook"
| "/docs/1.12.0/tags/notes"
| "/docs/1.12.0/tags/oidc"
| "/docs/1.12.0/tags/open-collective"
| "/docs/1.12.0/tags/open-media-vault"
| "/docs/1.12.0/tags/overseerr"
| "/docs/1.12.0/tags/permissions"
| "/docs/1.12.0/tags/pgid"
| "/docs/1.12.0/tags/pi-hole"
| "/docs/1.12.0/tags/ping"
| "/docs/1.12.0/tags/programming"
| "/docs/1.12.0/tags/proxmox"
| "/docs/1.12.0/tags/proxy"
| "/docs/1.12.0/tags/puid"
| "/docs/1.12.0/tags/responsive"
| "/docs/1.12.0/tags/roles"
| "/docs/1.12.0/tags/rss"
| "/docs/1.12.0/tags/search"
| "/docs/1.12.0/tags/search-engines"
| "/docs/1.12.0/tags/security"
| "/docs/1.12.0/tags/self-signed"
| "/docs/1.12.0/tags/seo"
| "/docs/1.12.0/tags/server"
| "/docs/1.12.0/tags/settings"
| "/docs/1.12.0/tags/sinkhole"
| "/docs/1.12.0/tags/sso"
| "/docs/1.12.0/tags/stocks"
| "/docs/1.12.0/tags/system"
| "/docs/1.12.0/tags/table"
| "/docs/1.12.0/tags/technical-documentation"
| "/docs/1.12.0/tags/text"
| "/docs/1.12.0/tags/torrent"
| "/docs/1.12.0/tags/traefik"
| "/docs/1.12.0/tags/translations"
| "/docs/1.12.0/tags/unraid"
| "/docs/1.12.0/tags/uploads"
| "/docs/1.12.0/tags/usenet"
| "/docs/1.12.0/tags/users"
| "/docs/1.12.0/tags/variables"
| "/docs/1.12.0/tags/widgets"
| "/docs/1.12.0/advanced/command-line"
| "/docs/1.12.0/advanced/command-line/fix-usernames"
| "/docs/1.12.0/advanced/command-line/password-recovery"
| "/docs/1.12.0/advanced/development/getting-started"
| "/docs/1.12.0/advanced/development/kubernetes"
| "/docs/1.12.0/advanced/environment-variables"
| "/docs/1.12.0/advanced/icons"
| "/docs/1.12.0/advanced/keyboard-shortcuts"
| "/docs/1.12.0/advanced/proxy"
| "/docs/1.12.0/advanced/running-as-different-user"
| "/docs/1.12.0/advanced/single-sign-on"
| "/docs/1.12.0/category/advanced"
| "/docs/1.12.0/category/community"
| "/docs/1.12.0/category/developer-guides"
| "/docs/1.12.0/category/getting-started"
| "/docs/1.12.0/category/installation"
| "/docs/1.12.0/category/installation-1"
| "/docs/1.12.0/category/integrations"
| "/docs/1.12.0/category/management"
| "/docs/1.12.0/category/widgets"
| "/docs/1.12.0/community/donate"
| "/docs/1.12.0/community/faq"
| "/docs/1.12.0/community/get-in-touch"
| "/docs/1.12.0/community/license"
| "/docs/1.12.0/community/translations"
| "/docs/1.12.0/getting-started"
| "/docs/1.12.0/getting-started/after-the-installation"
| "/docs/1.12.0/getting-started/glossary"
| "/docs/1.12.0/getting-started/installation/docker"
| "/docs/1.12.0/getting-started/installation/easy-panel"
| "/docs/1.12.0/getting-started/installation/helm"
| "/docs/1.12.0/getting-started/installation/home-assistant"
| "/docs/1.12.0/getting-started/installation/portainer"
| "/docs/1.12.0/getting-started/installation/proxmox"
| "/docs/1.12.0/getting-started/installation/qnap"
| "/docs/1.12.0/getting-started/installation/railway"
| "/docs/1.12.0/getting-started/installation/saltbox"
| "/docs/1.12.0/getting-started/installation/source"
| "/docs/1.12.0/getting-started/installation/synology"
| "/docs/1.12.0/getting-started/installation/unraid"
| "/docs/1.12.0/integrations/containers"
| "/docs/1.12.0/integrations/dns"
| "/docs/1.12.0/integrations/hardware"
| "/docs/1.12.0/integrations/kubernetes"
| "/docs/1.12.0/integrations/media-requester"
| "/docs/1.12.0/integrations/media-server"
| "/docs/1.12.0/integrations/servarr"
| "/docs/1.12.0/integrations/torrent"
| "/docs/1.12.0/integrations/usenet"
| "/docs/1.12.0/management/api"
| "/docs/1.12.0/management/apps"
| "/docs/1.12.0/management/boards"
| "/docs/1.12.0/management/certificates"
| "/docs/1.12.0/management/integrations"
| "/docs/1.12.0/management/media"
| "/docs/1.12.0/management/search-engines"
| "/docs/1.12.0/management/settings"
| "/docs/1.12.0/management/users"
| "/docs/1.12.0/widgets/bookmarks"
| "/docs/1.12.0/widgets/calendar"
| "/docs/1.12.0/widgets/clock"
| "/docs/1.12.0/widgets/dns-hole"
| "/docs/1.12.0/widgets/downloads"
| "/docs/1.12.0/widgets/health-monitoring"
| "/docs/1.12.0/widgets/home-assistant"
| "/docs/1.12.0/widgets/iframe"
| "/docs/1.12.0/widgets/indexer-manager"
| "/docs/1.12.0/widgets/media-requests"
| "/docs/1.12.0/widgets/media-server"
| "/docs/1.12.0/widgets/minecraft-server-status"
| "/docs/1.12.0/widgets/notebook"
| "/docs/1.12.0/widgets/rss"
| "/docs/1.12.0/widgets/stocks"
| "/docs/1.12.0/widgets/video"
| "/docs/1.12.0/widgets/weather"
| "/docs/1.13.1/tags"
| "/docs/1.13.1/tags/active-directory"
| "/docs/1.13.1/tags/ad-guard"
| "/docs/1.13.1/tags/ad-guard-home"
| "/docs/1.13.1/tags/administration"
| "/docs/1.13.1/tags/advanced"
| "/docs/1.13.1/tags/analytics"
| "/docs/1.13.1/tags/api"
| "/docs/1.13.1/tags/apps"
| "/docs/1.13.1/tags/banner"
| "/docs/1.13.1/tags/blocking"
| "/docs/1.13.1/tags/boards"
| "/docs/1.13.1/tags/bookmark"
| "/docs/1.13.1/tags/bookmarks"
| "/docs/1.13.1/tags/caddy"
| "/docs/1.13.1/tags/certificates"
| "/docs/1.13.1/tags/checklist"
| "/docs/1.13.1/tags/code"
| "/docs/1.13.1/tags/community"
| "/docs/1.13.1/tags/configuration"
| "/docs/1.13.1/tags/connections"
| "/docs/1.13.1/tags/customization"
| "/docs/1.13.1/tags/data-sources"
| "/docs/1.13.1/tags/database"
| "/docs/1.13.1/tags/developer"
| "/docs/1.13.1/tags/development"
| "/docs/1.13.1/tags/dns"
| "/docs/1.13.1/tags/docker"
| "/docs/1.13.1/tags/donation"
| "/docs/1.13.1/tags/edit-mode"
| "/docs/1.13.1/tags/env"
| "/docs/1.13.1/tags/environment-variables"
| "/docs/1.13.1/tags/feeds"
| "/docs/1.13.1/tags/finance"
| "/docs/1.13.1/tags/getting-started"
| "/docs/1.13.1/tags/google"
| "/docs/1.13.1/tags/grafana"
| "/docs/1.13.1/tags/groups"
| "/docs/1.13.1/tags/hardware"
| "/docs/1.13.1/tags/health"
| "/docs/1.13.1/tags/help"
| "/docs/1.13.1/tags/icon-picker"
| "/docs/1.13.1/tags/icon-repositories"
| "/docs/1.13.1/tags/icons"
| "/docs/1.13.1/tags/iframe"
| "/docs/1.13.1/tags/images"
| "/docs/1.13.1/tags/installation"
| "/docs/1.13.1/tags/integrade"
| "/docs/1.13.1/tags/integration"
| "/docs/1.13.1/tags/integrations"
| "/docs/1.13.1/tags/interface"
| "/docs/1.13.1/tags/jellyserr"
| "/docs/1.13.1/tags/layout"
| "/docs/1.13.1/tags/ldap"
| "/docs/1.13.1/tags/links"
| "/docs/1.13.1/tags/lists"
| "/docs/1.13.1/tags/management"
| "/docs/1.13.1/tags/market"
| "/docs/1.13.1/tags/media"
| "/docs/1.13.1/tags/minecraft"
| "/docs/1.13.1/tags/monitoring"
| "/docs/1.13.1/tags/news"
| "/docs/1.13.1/tags/notebook"
| "/docs/1.13.1/tags/notes"
| "/docs/1.13.1/tags/oidc"
| "/docs/1.13.1/tags/open-collective"
| "/docs/1.13.1/tags/open-media-vault"
| "/docs/1.13.1/tags/overseerr"
| "/docs/1.13.1/tags/permissions"
| "/docs/1.13.1/tags/pgid"
| "/docs/1.13.1/tags/pi-hole"
| "/docs/1.13.1/tags/ping"
| "/docs/1.13.1/tags/programming"
| "/docs/1.13.1/tags/proxmox"
| "/docs/1.13.1/tags/proxy"
| "/docs/1.13.1/tags/puid"
| "/docs/1.13.1/tags/responsive"
| "/docs/1.13.1/tags/roles"
| "/docs/1.13.1/tags/rss"
| "/docs/1.13.1/tags/search"
| "/docs/1.13.1/tags/search-engines"
| "/docs/1.13.1/tags/security"
| "/docs/1.13.1/tags/self-signed"
| "/docs/1.13.1/tags/seo"
| "/docs/1.13.1/tags/server"
| "/docs/1.13.1/tags/settings"
| "/docs/1.13.1/tags/sinkhole"
| "/docs/1.13.1/tags/sso"
| "/docs/1.13.1/tags/stocks"
| "/docs/1.13.1/tags/system"
| "/docs/1.13.1/tags/table"
| "/docs/1.13.1/tags/technical-documentation"
| "/docs/1.13.1/tags/text"
| "/docs/1.13.1/tags/torrent"
| "/docs/1.13.1/tags/traefik"
| "/docs/1.13.1/tags/translations"
| "/docs/1.13.1/tags/unraid"
| "/docs/1.13.1/tags/uploads"
| "/docs/1.13.1/tags/usenet"
| "/docs/1.13.1/tags/users"
| "/docs/1.13.1/tags/variables"
| "/docs/1.13.1/tags/widgets"
| "/docs/1.13.1/advanced/command-line"
| "/docs/1.13.1/advanced/command-line/fix-usernames"
| "/docs/1.13.1/advanced/command-line/password-recovery"
| "/docs/1.13.1/advanced/development/getting-started"
| "/docs/1.13.1/advanced/development/kubernetes"
| "/docs/1.13.1/advanced/environment-variables"
| "/docs/1.13.1/advanced/icons"
| "/docs/1.13.1/advanced/keyboard-shortcuts"
| "/docs/1.13.1/advanced/proxy"
| "/docs/1.13.1/advanced/running-as-different-user"
| "/docs/1.13.1/advanced/single-sign-on"
| "/docs/1.13.1/advanced/styling"
| "/docs/1.13.1/category/advanced"
| "/docs/1.13.1/category/community"
| "/docs/1.13.1/category/developer-guides"
| "/docs/1.13.1/category/getting-started"
| "/docs/1.13.1/category/installation"
| "/docs/1.13.1/category/installation-1"
| "/docs/1.13.1/category/integrations"
| "/docs/1.13.1/category/management"
| "/docs/1.13.1/category/widgets"
| "/docs/1.13.1/community/donate"
| "/docs/1.13.1/community/faq"
| "/docs/1.13.1/community/get-in-touch"
| "/docs/1.13.1/community/license"
| "/docs/1.13.1/community/translations"
| "/docs/1.13.1/getting-started"
| "/docs/1.13.1/getting-started/after-the-installation"
| "/docs/1.13.1/getting-started/glossary"
| "/docs/1.13.1/getting-started/installation/docker"
| "/docs/1.13.1/getting-started/installation/easy-panel"
| "/docs/1.13.1/getting-started/installation/helm"
| "/docs/1.13.1/getting-started/installation/home-assistant"
| "/docs/1.13.1/getting-started/installation/portainer"
| "/docs/1.13.1/getting-started/installation/proxmox"
| "/docs/1.13.1/getting-started/installation/qnap"
| "/docs/1.13.1/getting-started/installation/railway"
| "/docs/1.13.1/getting-started/installation/saltbox"
| "/docs/1.13.1/getting-started/installation/source"
| "/docs/1.13.1/getting-started/installation/synology"
| "/docs/1.13.1/getting-started/installation/unraid"
| "/docs/1.13.1/integrations/cloud"
| "/docs/1.13.1/integrations/containers"
| "/docs/1.13.1/integrations/dns"
| "/docs/1.13.1/integrations/hardware"
| "/docs/1.13.1/integrations/kubernetes"
| "/docs/1.13.1/integrations/media-requester"
| "/docs/1.13.1/integrations/media-server"
| "/docs/1.13.1/integrations/servarr"
| "/docs/1.13.1/integrations/torrent"
| "/docs/1.13.1/integrations/usenet"
| "/docs/1.13.1/management/api"
| "/docs/1.13.1/management/apps"
| "/docs/1.13.1/management/boards"
| "/docs/1.13.1/management/certificates"
| "/docs/1.13.1/management/integrations"
| "/docs/1.13.1/management/media"
| "/docs/1.13.1/management/search-engines"
| "/docs/1.13.1/management/settings"
| "/docs/1.13.1/management/users"
| "/docs/1.13.1/widgets/bookmarks"
| "/docs/1.13.1/widgets/calendar"
| "/docs/1.13.1/widgets/clock"
| "/docs/1.13.1/widgets/dns-hole"
| "/docs/1.13.1/widgets/downloads"
| "/docs/1.13.1/widgets/health-monitoring"
| "/docs/1.13.1/widgets/home-assistant"
| "/docs/1.13.1/widgets/iframe"
| "/docs/1.13.1/widgets/indexer-manager"
| "/docs/1.13.1/widgets/media-requests"
| "/docs/1.13.1/widgets/media-server"
| "/docs/1.13.1/widgets/minecraft-server-status"
| "/docs/1.13.1/widgets/notebook"
| "/docs/1.13.1/widgets/rss"
| "/docs/1.13.1/widgets/stocks"
| "/docs/1.13.1/widgets/video"
| "/docs/1.13.1/widgets/weather"
| "/docs/next/tags"
| "/docs/next/tags/active-directory"
| "/docs/next/tags/ad-guard"
| "/docs/next/tags/ad-guard-home"
| "/docs/next/tags/administration"
| "/docs/next/tags/advanced"
| "/docs/next/tags/analytics"
| "/docs/next/tags/api"
| "/docs/next/tags/apps"
| "/docs/next/tags/banner"
| "/docs/next/tags/blocking"
| "/docs/next/tags/boards"
| "/docs/next/tags/bookmark"
| "/docs/next/tags/bookmarks"
| "/docs/next/tags/caddy"
| "/docs/next/tags/certificates"
| "/docs/next/tags/checklist"
| "/docs/next/tags/code"
| "/docs/next/tags/community"
| "/docs/next/tags/configuration"
| "/docs/next/tags/connections"
| "/docs/next/tags/customization"
| "/docs/next/tags/data-sources"
| "/docs/next/tags/database"
| "/docs/next/tags/developer"
| "/docs/next/tags/development"
| "/docs/next/tags/dns"
| "/docs/next/tags/docker"
| "/docs/next/tags/donation"
| "/docs/next/tags/edit-mode"
| "/docs/next/tags/env"
| "/docs/next/tags/environment-variables"
| "/docs/next/tags/feeds"
| "/docs/next/tags/finance"
| "/docs/next/tags/getting-started"
| "/docs/next/tags/google"
| "/docs/next/tags/grafana"
| "/docs/next/tags/groups"
| "/docs/next/tags/hardware"
| "/docs/next/tags/health"
| "/docs/next/tags/help"
| "/docs/next/tags/icon-picker"
| "/docs/next/tags/icon-repositories"
| "/docs/next/tags/icons"
| "/docs/next/tags/iframe"
| "/docs/next/tags/images"
| "/docs/next/tags/installation"
| "/docs/next/tags/integrade"
| "/docs/next/tags/integration"
| "/docs/next/tags/integrations"
| "/docs/next/tags/interface"
| "/docs/next/tags/jellyserr"
| "/docs/next/tags/layout"
| "/docs/next/tags/ldap"
| "/docs/next/tags/links"
| "/docs/next/tags/lists"
| "/docs/next/tags/management"
| "/docs/next/tags/market"
| "/docs/next/tags/media"
| "/docs/next/tags/minecraft"
| "/docs/next/tags/monitoring"
| "/docs/next/tags/news"
| "/docs/next/tags/notebook"
| "/docs/next/tags/notes"
| "/docs/next/tags/oidc"
| "/docs/next/tags/open-collective"
| "/docs/next/tags/open-media-vault"
| "/docs/next/tags/overseerr"
| "/docs/next/tags/permissions"
| "/docs/next/tags/pgid"
| "/docs/next/tags/pi-hole"
| "/docs/next/tags/ping"
| "/docs/next/tags/programming"
| "/docs/next/tags/proxmox"
| "/docs/next/tags/proxy"
| "/docs/next/tags/puid"
| "/docs/next/tags/responsive"
| "/docs/next/tags/roles"
| "/docs/next/tags/rss"
| "/docs/next/tags/search"
| "/docs/next/tags/search-engines"
| "/docs/next/tags/security"
| "/docs/next/tags/self-signed"
| "/docs/next/tags/seo"
| "/docs/next/tags/server"
| "/docs/next/tags/settings"
| "/docs/next/tags/sinkhole"
| "/docs/next/tags/sso"
| "/docs/next/tags/stocks"
| "/docs/next/tags/system"
| "/docs/next/tags/table"
| "/docs/next/tags/technical-documentation"
| "/docs/next/tags/text"
| "/docs/next/tags/torrent"
| "/docs/next/tags/traefik"
| "/docs/next/tags/translations"
| "/docs/next/tags/unraid"
| "/docs/next/tags/uploads"
| "/docs/next/tags/usenet"
| "/docs/next/tags/users"
| "/docs/next/tags/variables"
| "/docs/next/tags/widgets"
| "/docs/next/advanced/command-line"
| "/docs/next/advanced/command-line/fix-usernames"
| "/docs/next/advanced/command-line/password-recovery"
| "/docs/next/advanced/development/getting-started"
| "/docs/next/advanced/development/kubernetes"
| "/docs/next/advanced/environment-variables"
| "/docs/next/advanced/icons"
| "/docs/next/advanced/keyboard-shortcuts"
| "/docs/next/advanced/proxy"
| "/docs/next/advanced/running-as-different-user"
| "/docs/next/advanced/single-sign-on"
| "/docs/next/advanced/styling"
| "/docs/next/category/advanced"
| "/docs/next/category/community"
| "/docs/next/category/developer-guides"
| "/docs/next/category/getting-started"
| "/docs/next/category/installation"
| "/docs/next/category/installation-1"
| "/docs/next/category/integrations"
| "/docs/next/category/management"
| "/docs/next/category/widgets"
| "/docs/next/community/donate"
| "/docs/next/community/faq"
| "/docs/next/community/get-in-touch"
| "/docs/next/community/license"
| "/docs/next/community/translations"
| "/docs/next/getting-started"
| "/docs/next/getting-started/after-the-installation"
| "/docs/next/getting-started/glossary"
| "/docs/next/getting-started/installation/docker"
| "/docs/next/getting-started/installation/easy-panel"
| "/docs/next/getting-started/installation/helm"
| "/docs/next/getting-started/installation/home-assistant"
| "/docs/next/getting-started/installation/portainer"
| "/docs/next/getting-started/installation/proxmox"
| "/docs/next/getting-started/installation/qnap"
| "/docs/next/getting-started/installation/railway"
| "/docs/next/getting-started/installation/saltbox"
| "/docs/next/getting-started/installation/source"
| "/docs/next/getting-started/installation/synology"
| "/docs/next/getting-started/installation/unraid"
| "/docs/next/integrations/cloud"
| "/docs/next/integrations/containers"
| "/docs/next/integrations/dns"
| "/docs/next/integrations/hardware"
| "/docs/next/integrations/kubernetes"
| "/docs/next/integrations/media-requester"
| "/docs/next/integrations/media-server"
| "/docs/next/integrations/servarr"
| "/docs/next/integrations/torrent"
| "/docs/next/integrations/usenet"
| "/docs/next/management/api"
| "/docs/next/management/apps"
| "/docs/next/management/boards"
| "/docs/next/management/certificates"
| "/docs/next/management/integrations"
| "/docs/next/management/media"
| "/docs/next/management/search-engines"
| "/docs/next/management/settings"
| "/docs/next/management/users"
| "/docs/next/widgets/bookmarks"
| "/docs/next/widgets/calendar"
| "/docs/next/widgets/clock"
| "/docs/next/widgets/dns-hole"
| "/docs/next/widgets/downloads"
| "/docs/next/widgets/health-monitoring"
| "/docs/next/widgets/home-assistant"
| "/docs/next/widgets/iframe"
| "/docs/next/widgets/indexer-manager"
| "/docs/next/widgets/media-requests"
| "/docs/next/widgets/media-server"
| "/docs/next/widgets/minecraft-server-status"
| "/docs/next/widgets/notebook"
| "/docs/next/widgets/rss"
| "/docs/next/widgets/stocks"
| "/docs/next/widgets/video"
| "/docs/next/widgets/weather"
| "/docs/tags"
| "/docs/tags/active-directory"
| "/docs/tags/ad-guard"
+5 -5
View File
@@ -142,26 +142,26 @@ export const integrationDefs = {
dashDot: {
name: "Dash.",
secretKinds: [[]],
category: ["healthMonitoring"],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/dashdot.png",
category: ["healthMonitoring"],
},
tdarr: {
name: "Tdarr",
secretKinds: [[]],
category: ["mediaTranscoding"],
secretKinds: [[], ["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/tdarr.png",
category: ["mediaTranscoding"],
},
proxmox: {
name: "Proxmox",
secretKinds: [["username", "tokenId", "apiKey", "realm"]],
category: ["healthMonitoring"],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/proxmox.svg",
category: ["healthMonitoring"],
},
nextcloud: {
name: "Nextcloud",
secretKinds: [["username", "password"]],
category: ["calendar"],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nextcloud.svg",
category: ["calendar"],
},
unifiController: {
name: "Unifi Controller",
+1
View File
@@ -23,5 +23,6 @@ export const widgetKinds = [
"bookmarks",
"indexerManager",
"healthMonitoring",
"releases",
] as const;
export type WidgetKind = (typeof widgetKinds)[number];
+3 -3
View File
@@ -25,14 +25,14 @@
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0",
"dockerode": "^4.0.5"
"dockerode": "^4.0.6"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.37",
"eslint": "^9.24.0",
"@types/dockerode": "^3.3.38",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+3 -3
View File
@@ -23,14 +23,14 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@t3-oss/env-nextjs": "^0.12.0",
"zod": "^3.24.2"
"@t3-oss/env-nextjs": "^0.13.0",
"zod": "^3.24.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+3 -3
View File
@@ -26,14 +26,14 @@
"@homarr/common": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^7.17.4",
"zod": "^3.24.2"
"@mantine/form": "^7.17.5",
"zod": "^3.24.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+3 -3
View File
@@ -29,15 +29,15 @@
"@homarr/notifications": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.17.4",
"@mantine/core": "^7.17.5",
"react": "19.1.0",
"zod": "^3.24.2"
"zod": "^3.24.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+1 -1
View File
@@ -31,7 +31,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+6 -4
View File
@@ -25,7 +25,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@ctrl/deluge": "^7.1.0",
"@ctrl/qbittorrent": "^9.5.2",
"@ctrl/qbittorrent": "^9.6.0",
"@ctrl/transmission": "^7.2.0",
"@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
@@ -38,18 +38,20 @@
"@jellyfin/sdk": "^0.11.0",
"maria2": "^0.4.0",
"node-ical": "^0.20.1",
"node-unifi": "^2.5.1",
"proxmox-api": "1.1.1",
"tsdav": "^2.1.3",
"tsdav": "^2.1.4",
"undici": "7.8.0",
"xml2js": "^0.6.2",
"zod": "^3.24.2"
"zod": "^3.24.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-unifi": "^2.5.1",
"@types/xml2js": "^0.4.14",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
@@ -30,13 +30,15 @@ export class DashDotIntegration extends Integration {
cpuUtilization: cpuLoad.sumLoad,
memUsed: `${memoryLoad.loadInBytes}`,
memAvailable: `${info.maxAvailableMemoryBytes - memoryLoad.loadInBytes}`,
fileSystem: info.storage.map((storage, index) => ({
deviceName: `Storage ${index + 1}: (${storage.disks.map((disk) => disk.device).join(", ")})`,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
used: humanFileSize(storageLoad[index]!),
available: `${storage.size}`,
percentage: storageLoad[index] ? (storageLoad[index] / storage.size) * 100 : 0,
})),
fileSystem: info.storage
.filter((_, index) => storageLoad[index] !== -1) // filter out undermoutned drives, they display as -1 in the load API
.map((storage, index) => ({
deviceName: `Storage ${index + 1}: (${storage.disks.map((disk) => disk.device).join(", ")})`,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
used: humanFileSize(storageLoad[index]!),
available: storageLoad[index] ? `${storage.size - storageLoad[index]}` : `${storage.size}`,
percentage: storageLoad[index] ? (storageLoad[index] / storage.size) * 100 : 0,
})),
cpuModelName: info.cpuModel === "" ? `Unknown Model (${info.cpuBrand})` : `${info.cpuModel} (${info.cpuBrand})`,
cpuTemp: cpuLoad.averageTemperature,
availablePkgUpdates: 0,
@@ -4,7 +4,7 @@ import { z } from "zod";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { Integration } from "../base/integration";
import type { StreamSession } from "../interfaces/media-server/session";
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session";
import { convertJellyfinType } from "../jellyfin/jellyfin-integration";
const sessionSchema = z.object({
@@ -47,7 +47,7 @@ export class EmbyIntegration extends Integration {
});
}
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
public async getCurrentSessionsAsync(options: CurrentSessionsInput): Promise<StreamSession[]> {
const apiKey = super.getSecretValue("apiKey");
const response = await fetchWithTrustedCertificatesAsync(super.url("/emby/Sessions"), {
headers: {
@@ -69,6 +69,7 @@ export class EmbyIntegration extends Integration {
return result.data
.filter((sessionInfo) => sessionInfo.UserId !== undefined)
.filter((sessionInfo) => sessionInfo.DeviceId !== EmbyIntegration.deviceId)
.filter((sessionInfo) => !options.showOnlyPlaying || sessionInfo.NowPlayingItem !== undefined)
.map((sessionInfo): StreamSession => {
let currentlyPlaying: StreamSession["currentlyPlaying"] | null = null;
@@ -15,3 +15,7 @@ export interface StreamSession {
episodeCount?: number | null;
} | null;
}
export interface CurrentSessionsInput {
showOnlyPlaying: boolean;
}
@@ -4,6 +4,7 @@ export interface TdarrPieSegment {
}
export interface TdarrStatistics {
libraryName: string;
totalFileCount: number;
totalTranscodeCount: number;
totalHealthCheckCount: number;
@@ -11,19 +12,12 @@ export interface TdarrStatistics {
failedHealthCheckCount: number;
stagedTranscodeCount: number;
stagedHealthCheckCount: number;
pies: {
libraryName: string;
libraryId: string;
totalFiles: number;
totalTranscodes: number;
savedSpace: number;
totalHealthChecks: number;
transcodeStatus: TdarrPieSegment[];
healthCheckStatus: TdarrPieSegment[];
videoCodecs: TdarrPieSegment[];
videoContainers: TdarrPieSegment[];
videoResolutions: TdarrPieSegment[];
audioCodecs: TdarrPieSegment[];
audioContainers: TdarrPieSegment[];
}[];
totalSavedSpace: number;
transcodeStatus: TdarrPieSegment[];
healthCheckStatus: TdarrPieSegment[];
videoCodecs: TdarrPieSegment[];
videoContainers: TdarrPieSegment[];
videoResolutions: TdarrPieSegment[];
audioCodecs: TdarrPieSegment[];
audioContainers: TdarrPieSegment[];
}
@@ -6,7 +6,7 @@ import { getSystemApi } from "@jellyfin/sdk/lib/utils/api/system-api";
import { createAxiosCertificateInstanceAsync } from "@homarr/certificates/server";
import { Integration } from "../base/integration";
import type { StreamSession } from "../interfaces/media-server/session";
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session";
export class JellyfinIntegration extends Integration {
private readonly jellyfin: Jellyfin = new Jellyfin({
@@ -26,7 +26,7 @@ export class JellyfinIntegration extends Integration {
await systemApi.getPingSystem();
}
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
public async getCurrentSessionsAsync(options: CurrentSessionsInput): Promise<StreamSession[]> {
const api = await this.getApiAsync();
const sessionApi = getSessionApi(api);
const sessions = await sessionApi.getSessions();
@@ -38,6 +38,7 @@ export class JellyfinIntegration extends Integration {
return sessions.data
.filter((sessionInfo) => sessionInfo.UserId !== undefined)
.filter((sessionInfo) => sessionInfo.DeviceId !== "homarr")
.filter((sessionInfo) => !options.showOnlyPlaying || sessionInfo.NowPlayingItem !== undefined)
.map((sessionInfo): StreamSession => {
let currentlyPlaying: StreamSession["currentlyPlaying"] | null = null;
@@ -1,5 +1,3 @@
import { z } from "zod";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { Integration } from "../base/integration";
@@ -10,25 +8,34 @@ import { getNodesResponseSchema, getStatisticsSchema, getStatusTableSchema } fro
export class TdarrIntegration extends Integration {
public async testConnectionAsync(): Promise<void> {
const url = this.url("/api/v2/status");
const response = await fetchWithTrustedCertificatesAsync(url);
if (response.status !== 200) {
throw new Error(`Unexpected status code: ${response.status}`);
}
await z.object({ status: z.string() }).parseAsync(await response.json());
await super.handleTestConnectionResponseAsync({
queryFunctionAsync: async () => {
return await fetchWithTrustedCertificatesAsync(this.url("/api/v2/is-server-alive"), {
method: "POST",
headers: {
accept: "application/json",
"X-Api-Key": super.hasSecretValue("apiKey") ? super.getSecretValue("apiKey") : "",
},
});
},
});
}
public async getStatisticsAsync(): Promise<TdarrStatistics> {
const url = this.url("/api/v2/cruddb");
const url = this.url("/api/v2/stats/get-pies");
const headerParams = {
accept: "application/json",
"Content-Type": "application/json",
...(super.hasSecretValue("apiKey") ? { "X-Api-Key": super.getSecretValue("apiKey") } : {}),
};
const response = await fetchWithTrustedCertificatesAsync(url, {
method: "POST",
headers: { "content-type": "application/json" },
headers: headerParams,
body: JSON.stringify({
data: {
collection: "StatisticsJSONDB",
mode: "getById",
docID: "statistics",
libraryId: "", // empty string to get all libraries
},
}),
});
@@ -36,36 +43,41 @@ export class TdarrIntegration extends Integration {
const statisticsData = await getStatisticsSchema.parseAsync(await response.json());
return {
totalFileCount: statisticsData.totalFileCount,
totalTranscodeCount: statisticsData.totalTranscodeCount,
totalHealthCheckCount: statisticsData.totalHealthCheckCount,
failedTranscodeCount: statisticsData.table3Count,
failedHealthCheckCount: statisticsData.table6Count,
stagedTranscodeCount: statisticsData.table1Count,
stagedHealthCheckCount: statisticsData.table4Count,
pies: statisticsData.pies.map((pie) => ({
libraryName: pie[0],
libraryId: pie[1],
totalFiles: pie[2],
totalTranscodes: pie[3],
savedSpace: pie[4] * 1_000_000_000, // file_size is in GB, convert to bytes,
totalHealthChecks: pie[5],
transcodeStatus: pie[6],
healthCheckStatus: pie[7],
videoCodecs: pie[8],
videoContainers: pie[9],
videoResolutions: pie[10],
audioCodecs: pie[11],
audioContainers: pie[12],
})),
libraryName: "All",
totalFileCount: statisticsData.pieStats.totalFiles,
totalTranscodeCount: statisticsData.pieStats.totalTranscodeCount,
totalHealthCheckCount: statisticsData.pieStats.totalHealthCheckCount,
// The Tdarr API only returns a category if there is at least one item in it
failedTranscodeCount:
statisticsData.pieStats.status.transcode.find((transcode) => transcode.name === "Transcode error")?.value ?? 0,
failedHealthCheckCount:
statisticsData.pieStats.status.healthcheck.find((healthcheck) => healthcheck.name === "Error")?.value ?? 0,
stagedTranscodeCount:
statisticsData.pieStats.status.transcode.find((transcode) => transcode.name === "Transcode success")?.value ??
0,
stagedHealthCheckCount:
statisticsData.pieStats.status.healthcheck.find((healthcheck) => healthcheck.name === "Queued")?.value ?? 0,
totalSavedSpace: statisticsData.pieStats.sizeDiff * 1_000_000_000, // sizeDiff is in GB, convert to bytes
transcodeStatus: statisticsData.pieStats.status.transcode,
healthCheckStatus: statisticsData.pieStats.status.healthcheck,
videoCodecs: statisticsData.pieStats.video.codecs,
videoContainers: statisticsData.pieStats.video.containers,
videoResolutions: statisticsData.pieStats.video.resolutions,
audioCodecs: statisticsData.pieStats.audio.codecs,
audioContainers: statisticsData.pieStats.audio.containers,
};
}
public async getWorkersAsync(): Promise<TdarrWorker[]> {
const url = this.url("/api/v2/get-nodes");
const headerParams = {
"Content-Type": "application/json",
...(super.hasSecretValue("apiKey") ? { "X-Api-Key": super.getSecretValue("apiKey") } : {}),
};
const response = await fetchWithTrustedCertificatesAsync(url, {
method: "GET",
headers: { "content-type": "application/json" },
headers: headerParams,
});
const nodesData = await getNodesResponseSchema.parseAsync(await response.json());
@@ -103,9 +115,13 @@ export class TdarrIntegration extends Integration {
private async getTranscodingQueueAsync(firstItemIndex: number, pageSize: number) {
const url = this.url("/api/v2/client/status-tables");
const headerParams = {
"Content-Type": "application/json",
...(super.hasSecretValue("apiKey") ? { "X-Api-Key": super.getSecretValue("apiKey") } : {}),
};
const response = await fetchWithTrustedCertificatesAsync(url, {
method: "POST",
headers: { "content-type": "application/json" },
headers: headerParams,
body: JSON.stringify({
data: {
start: firstItemIndex,
@@ -124,7 +140,7 @@ export class TdarrIntegration extends Integration {
healthCheck: item.HealthCheck,
transcode: item.TranscodeDecisionMaker,
filePath: item.file,
fileSize: item.file_size * 1_000_000, // file_size is in MB, convert to bytes
fileSize: Math.floor(item.file_size * 1_000_000), // file_size is in MB, convert to bytes, floor because it returns as float
container: item.container,
codec: item.video_codec_name,
resolution: item.video_resolution,
@@ -138,9 +154,13 @@ export class TdarrIntegration extends Integration {
private async getHealthCheckDataAsync(firstItemIndex: number, pageSize: number, totalQueueCount: number) {
const url = this.url("/api/v2/client/status-tables");
const headerParams = {
"Content-Type": "application/json",
...(super.hasSecretValue("apiKey") ? { "X-Api-Key": super.getSecretValue("apiKey") } : {}),
};
const response = await fetchWithTrustedCertificatesAsync(url, {
method: "POST",
headers: { "content-type": "application/json" },
headers: headerParams,
body: JSON.stringify({
data: {
start: Math.max(firstItemIndex - totalQueueCount, 0),
@@ -162,7 +182,7 @@ export class TdarrIntegration extends Integration {
healthCheck: item.HealthCheck,
transcode: item.TranscodeDecisionMaker,
filePath: item.file,
fileSize: item.file_size * 1_000_000, // file_size is in MB, convert to bytes
fileSize: Math.floor(item.file_size * 1_000_000), // file_size is in MB, convert to bytes, floor because it returns as float
container: item.container,
codec: item.video_codec_name,
resolution: item.video_resolution,
@@ -1,72 +1,60 @@
import { z } from "zod";
export const getStatisticsSchema = z.object({
totalFileCount: z.number(),
totalTranscodeCount: z.number(),
totalHealthCheckCount: z.number(),
table3Count: z.number(),
table6Count: z.number(),
table1Count: z.number(),
table4Count: z.number(),
pies: z.array(
z.tuple([
z.string(), // Library Name
z.string(), // Library ID
z.number(), // File count
z.number(), // Number of transcodes
z.number(), // Space saved (in GB)
z.number(), // Number of health checks
z.array(
pieStats: z.object({
totalFiles: z.number(),
totalTranscodeCount: z.number(),
sizeDiff: z.number(),
totalHealthCheckCount: z.number(),
status: z.object({
transcode: z.array(
z.object({
// Transcode Status (Pie segments)
name: z.string(),
value: z.number(),
}),
),
z.array(
healthcheck: z.array(
z.object({
// Health Status (Pie segments)
name: z.string(),
value: z.number(),
}),
),
z.array(
}),
video: z.object({
codecs: z.array(
z.object({
// Video files - Codecs (Pie segments)
name: z.string(),
value: z.number(),
}),
),
z.array(
containers: z.array(
z.object({
// Video files - Containers (Pie segments)
name: z.string(),
value: z.number(),
}),
),
z.array(
resolutions: z.array(
z.object({
// Video files - Resolutions (Pie segments)
name: z.string(),
value: z.number(),
}),
),
z.array(
}),
audio: z.object({
codecs: z.array(
z.object({
// Audio files - Codecs (Pie segments)
name: z.string(),
value: z.number(),
}),
),
z.array(
containers: z.array(
z.object({
// Audio files - Containers (Pie segments)
name: z.string(),
value: z.number(),
}),
),
]),
),
}),
}),
});
export const getNodesResponseSchema = z.record(
@@ -5,11 +5,11 @@ import { logger } from "@homarr/log";
import { Integration } from "../base/integration";
import { IntegrationTestConnectionError } from "../base/test-connection-error";
import type { StreamSession } from "../interfaces/media-server/session";
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session";
import type { PlexResponse } from "./interface";
export class PlexIntegration extends Integration {
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
public async getCurrentSessionsAsync(_options: CurrentSessionsInput): Promise<StreamSession[]> {
const token = super.getSecretValue("apiKey");
const response = await fetchWithTrustedCertificatesAsync(this.url("/status/sessions"), {
@@ -1,103 +1,78 @@
import type z from "zod";
import type { SiteStats } from "node-unifi";
import { Controller } from "node-unifi";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { logger } from "@homarr/log";
import { ParseError } from "../base/error";
import { Integration, throwErrorByStatusCode } from "../base/integration";
import { IntegrationTestConnectionError } from "../base/test-connection-error";
import { Integration } from "../base/integration";
import type { NetworkControllerSummaryIntegration } from "../interfaces/network-controller-summary/network-controller-summary-integration";
import type { NetworkControllerSummary } from "../interfaces/network-controller-summary/network-controller-summary-types";
import { unifiSummaryResponseSchema } from "./unifi-controller-types";
const udmpPrefix = "proxy/network";
type Subsystem = "www" | "wan" | "wlan" | "lan" | "vpn";
import type { HealthSubsystem } from "./unifi-controller-types";
export class UnifiControllerIntegration extends Integration implements NetworkControllerSummaryIntegration {
private prefix: string | undefined;
public async getNetworkSummaryAsync(): Promise<NetworkControllerSummary> {
if (!this.headers) {
await this.authenticateAndConstructSessionInHeaderAsync();
}
const requestUrl = this.url(`/${this.prefix}/api/stat/sites`);
const requestHeaders: Record<string, string> = {
"Content-Type": "application/json",
...this.headers,
};
if (this.csrfToken) {
requestHeaders["X-CSRF-TOKEN"] = this.csrfToken;
}
const statsResponse = await fetchWithTrustedCertificatesAsync(requestUrl, {
method: "GET",
headers: {
...requestHeaders,
},
}).catch((err: TypeError) => {
const detailMessage = String(err.cause);
throw new IntegrationTestConnectionError("invalidUrl", detailMessage);
});
if (!statsResponse.ok) {
throwErrorByStatusCode(statsResponse.status);
}
const result = unifiSummaryResponseSchema.safeParse(await statsResponse.json());
if (!result.success) {
throw new ParseError("Unifi controller", result.error);
}
const client = await this.createControllerClientAsync();
const stats = await client.getSitesStats();
return {
wanStatus: this.getStatusValueOverAllSites(result.data, "wan", (site) => site.status === "ok"),
wanStatus: this.getStatusValueOverAllSites(stats, "wan", (site) => site.status === "ok"),
www: {
status: this.getStatusValueOverAllSites(result.data, "wan", (site) => site.status === "ok"),
latency: this.getNumericValueOverAllSites(result.data, "www", (site) => site.latency, "max"),
ping: this.getNumericValueOverAllSites(result.data, "www", (site) => site.speedtest_ping, "max"),
uptime: this.getNumericValueOverAllSites(result.data, "www", (site) => site.uptime, "max"),
status: this.getStatusValueOverAllSites(stats, "wan", (site) => site.status === "ok"),
latency: this.getNumericValueOverAllSites(stats, "www", (site) => site.latency, "max"),
ping: this.getNumericValueOverAllSites(stats, "www", (site) => site.speedtest_ping, "max"),
uptime: this.getNumericValueOverAllSites(stats, "www", (site) => site.uptime, "max"),
},
wifi: {
status: this.getStatusValueOverAllSites(result.data, "wlan", (site) => site.status === "ok"),
users: this.getNumericValueOverAllSites(result.data, "wlan", (site) => site.num_user, "sum"),
guests: this.getNumericValueOverAllSites(result.data, "wlan", (site) => site.num_guest, "sum"),
status: this.getStatusValueOverAllSites(stats, "wlan", (site) => site.status === "ok"),
users: this.getNumericValueOverAllSites(stats, "wlan", (site) => site.num_user, "sum"),
guests: this.getNumericValueOverAllSites(stats, "wlan", (site) => site.num_guest, "sum"),
},
lan: {
status: this.getStatusValueOverAllSites(result.data, "lan", (site) => site.status === "ok"),
users: this.getNumericValueOverAllSites(result.data, "lan", (site) => site.num_user, "sum"),
guests: this.getNumericValueOverAllSites(result.data, "lan", (site) => site.num_guest, "sum"),
status: this.getStatusValueOverAllSites(stats, "lan", (site) => site.status === "ok"),
users: this.getNumericValueOverAllSites(stats, "lan", (site) => site.num_user, "sum"),
guests: this.getNumericValueOverAllSites(stats, "lan", (site) => site.num_guest, "sum"),
},
vpn: {
status: this.getStatusValueOverAllSites(result.data, "vpn", (site) => site.status === "ok"),
users: this.getNumericValueOverAllSites(result.data, "vpn", (site) => site.remote_user_num_active, "sum"),
status: this.getStatusValueOverAllSites(stats, "vpn", (site) => site.status === "ok"),
users: this.getNumericValueOverAllSites(stats, "vpn", (site) => site.remote_user_num_active, "sum"),
},
} satisfies NetworkControllerSummary;
}
public async testConnectionAsync(): Promise<void> {
await this.authenticateAndConstructSessionInHeaderAsync();
const client = await this.createControllerClientAsync();
await client.getSitesStats();
}
private getStatusValueOverAllSites(
data: z.infer<typeof unifiSummaryResponseSchema>,
subsystem: Subsystem,
selectCallback: (obj: z.infer<typeof unifiSummaryResponseSchema>["data"][number]["health"][number]) => boolean,
private async createControllerClientAsync() {
const portString = new URL(this.integration.url).port;
const port = Number.isInteger(portString) ? Number(portString) : undefined;
const hostname = new URL(this.integration.url).hostname;
const client = new Controller({
host: hostname,
// @ts-expect-error the URL construction is incorrect and does not append the required / at the end: https://github.com/jens-maus/node-unifi/blob/05665e8f82a900a15a9ea8b1071750b29825b3bc/unifi.js#L56, https://github.com/jens-maus/node-unifi/blob/05665e8f82a900a15a9ea8b1071750b29825b3bc/unifi.js#L95
port: port === undefined ? "/" : `${port}/`,
sslverify: false, // TODO: implement a "ignore certificate toggle", see https://github.com/homarr-labs/homarr/issues/2553
username: this.getSecretValue("username"),
password: this.getSecretValue("password"),
});
// Object.defineProperty(client, '_baseurl', { value: url });
await client.login(this.getSecretValue("username"), this.getSecretValue("password"), null);
return client;
}
private getStatusValueOverAllSites<S extends HealthSubsystem>(
data: SiteStats[],
subsystem: S,
selectCallback: (obj: SiteStats["health"][number]) => boolean,
) {
return this.getBooleanValueOverAllSites(data, subsystem, selectCallback) ? "enabled" : "disabled";
}
private getNumericValueOverAllSites<
S extends Subsystem,
T extends Extract<z.infer<typeof unifiSummaryResponseSchema>["data"][number]["health"][number], { subsystem: S }>,
>(
data: z.infer<typeof unifiSummaryResponseSchema>,
subsystem: S,
selectCallback: (obj: T) => number,
strategy: "average" | "sum" | "max",
): number {
const values = data.data.map((site) => selectCallback(this.getSubsystem(site.health, subsystem) as T));
S extends HealthSubsystem,
T extends Extract<SiteStats["health"][number], { subsystem: S }>,
>(data: SiteStats[], subsystem: S, selectCallback: (obj: T) => number, strategy: "average" | "sum" | "max"): number {
const values = data.map((site) => selectCallback(this.getSubsystem(site.health, subsystem) as T));
if (strategy === "sum") {
return values.reduce((first, second) => first + second, 0);
@@ -111,118 +86,18 @@ export class UnifiControllerIntegration extends Integration implements NetworkCo
}
private getBooleanValueOverAllSites(
data: z.infer<typeof unifiSummaryResponseSchema>,
subsystem: Subsystem,
selectCallback: (obj: z.infer<typeof unifiSummaryResponseSchema>["data"][number]["health"][number]) => boolean,
data: SiteStats[],
subsystem: HealthSubsystem,
selectCallback: (obj: SiteStats["health"][number]) => boolean,
): boolean {
return data.data.every((site) => selectCallback(this.getSubsystem(site.health, subsystem)));
return data.every((site) => selectCallback(this.getSubsystem(site.health, subsystem)));
}
private getSubsystem(
health: z.infer<typeof unifiSummaryResponseSchema>["data"][number]["health"],
subsystem: Subsystem,
) {
private getSubsystem(health: SiteStats["health"], subsystem: HealthSubsystem) {
const value = health.find((health) => health.subsystem === subsystem);
if (!value) {
throw new Error(`Subsystem ${subsystem} not found!`);
}
return value;
}
private headers: Record<string, string> | undefined = undefined;
private csrfToken: string | undefined;
private async authenticateAndConstructSessionInHeaderAsync(): Promise<void> {
await this.determineUDMVariantAsync();
await this.authenticateAndSetCookieAsync();
}
private async authenticateAndSetCookieAsync(): Promise<void> {
if (this.headers) {
return;
}
const endpoint = this.prefix === udmpPrefix ? "auth/login" : "login";
logger.debug("Authenticating at network console: " + endpoint);
const loginUrl = this.url(`/api/${endpoint}`);
const loginBody = {
username: this.getSecretValue("username"),
password: this.getSecretValue("password"),
remember: true,
};
const requestHeaders: Record<string, string> = { "Content-Type": "application/json" };
if (this.csrfToken) {
requestHeaders["X-CSRF-TOKEN"] = this.csrfToken;
}
const loginResponse = await fetchWithTrustedCertificatesAsync(loginUrl, {
method: "POST",
headers: {
...requestHeaders,
},
body: JSON.stringify(loginBody),
}).catch((err: TypeError) => {
const detailMessage = String(err.cause);
throw new IntegrationTestConnectionError("invalidUrl", detailMessage);
});
if (!loginResponse.ok) {
throwErrorByStatusCode(loginResponse.status);
}
const responseHeaders = loginResponse.headers;
const newHeaders: Record<string, string> = {};
const loginToken = UnifiControllerIntegration.extractLoginTokenFromCookies(responseHeaders);
newHeaders.Cookie = `${loginToken};`;
this.headers = newHeaders;
}
private async determineUDMVariantAsync(): Promise<void> {
if (this.prefix) {
return;
}
logger.debug("Prefix for authentication not set; initial connect to determine UDM variant");
const url = this.url("/");
const { status, ok, headers } = await fetchWithTrustedCertificatesAsync(url, { method: "HEAD" })
.then((res) => res)
.catch((err: TypeError) => {
const detailMessage = String(err.cause);
throw new IntegrationTestConnectionError("invalidUrl", detailMessage);
});
if (!ok) {
throw new IntegrationTestConnectionError("invalidUrl", "status code: " + status);
}
let prefix = "";
if (headers.get("x-csrf-token") !== null) {
// Unifi OS < 3.2.5 passes & requires csrf-token
prefix = udmpPrefix;
const headersCSRFToken = headers.get("x-csrf-token");
if (headersCSRFToken) {
this.csrfToken = headersCSRFToken;
}
} else if (headers.get("access-control-expose-headers") !== null) {
// Unifi OS ≥ 3.2.5 doesnt pass csrf token but still uses different endpoint
prefix = udmpPrefix;
}
this.prefix = prefix;
logger.debug("Final prefix: " + this.prefix);
}
private static extractLoginTokenFromCookies(headers: Headers): string {
const cookies = headers.get("set-cookie") ?? "";
const loginToken = cookies.split(";").find((cookie) => cookie.includes("TOKEN"));
if (loginToken) {
return loginToken;
}
throw new Error("Login token not found in cookies");
}
}
@@ -1,130 +1,3 @@
import { z } from "zod";
import type { SiteStats } from "node-unifi";
export const healthSchema = z.discriminatedUnion("subsystem", [
z.object({
subsystem: z.literal("wlan"),
num_user: z.number(),
num_guest: z.number(),
num_iot: z.number(),
"tx_bytes-r": z.number(),
"rx_bytes-r": z.number(),
status: z.string(),
num_ap: z.number(),
num_adopted: z.number(),
num_disabled: z.number(),
num_disconnected: z.number(),
num_pending: z.number(),
}),
z.object({
subsystem: z.literal("wan"),
num_gw: z.number(),
num_adopted: z.number(),
num_disconnected: z.number(),
num_pending: z.number(),
status: z.string(),
wan_ip: z.string().ip(),
gateways: z.array(z.string().ip()),
netmask: z.string().ip(),
nameservers: z.array(z.string().ip()).optional(),
num_sta: z.number(),
"tx_bytes-r": z.number(),
"rx_bytes-r": z.number(),
gw_mac: z.string(),
gw_name: z.string(),
"gw_system-stats": z.object({
cpu: z.string(),
mem: z.string(),
uptime: z.string(),
}),
gw_version: z.string(),
isp_name: z.string(),
isp_organization: z.string(),
uptime_stats: z.object({
WAN: z.object({
alerting_monitors: z.array(
z.object({
availability: z.number(),
latency_average: z.number(),
target: z.string(),
type: z.enum(["icmp", "dns"]),
}),
),
availability: z.number(),
latency_average: z.number(),
monitors: z.array(
z.object({
availability: z.number(),
latency_average: z.number(),
target: z.string(),
type: z.enum(["icmp", "dns"]),
}),
),
time_period: z.number(),
uptime: z.number(),
}),
}),
}),
z.object({
subsystem: z.literal("www"),
status: z.string(),
"tx_bytes-r": z.number(),
"rx_bytes-r": z.number(),
latency: z.number(),
uptime: z.number(),
drops: z.number(),
xput_up: z.number(),
xput_down: z.number(),
speedtest_status: z.string(),
speedtest_lastrun: z.number(),
speedtest_ping: z.number(),
gw_mac: z.string(),
}),
z.object({
subsystem: z.literal("lan"),
lan_ip: z.string().ip().nullish(),
status: z.string(),
num_user: z.number(),
num_guest: z.number(),
num_iot: z.number(),
"tx_bytes-r": z.number(),
"rx_bytes-r": z.number(),
num_sw: z.number(),
num_adopted: z.number(),
num_disconnected: z.number(),
num_pending: z.number(),
}),
z.object({
subsystem: z.literal("vpn"),
status: z.string(),
remote_user_enabled: z.boolean(),
remote_user_num_active: z.number(),
remote_user_num_inactive: z.number(),
remote_user_rx_bytes: z.number(),
remote_user_tx_bytes: z.number(),
remote_user_rx_packets: z.number(),
remote_user_tx_packets: z.number(),
site_to_site_enabled: z.boolean(),
}),
]);
export type Health = z.infer<typeof healthSchema>;
export const siteSchema = z.object({
anonymous_id: z.string().uuid(),
name: z.string(),
external_id: z.string().uuid(),
_id: z.string(),
attr_no_delete: z.boolean(),
attr_hidden_id: z.string(),
desc: z.string(),
health: z.array(healthSchema),
num_new_alarms: z.number(),
});
export type Site = z.infer<typeof siteSchema>;
export const unifiSummaryResponseSchema = z.object({
meta: z.object({
rc: z.enum(["ok"]),
}),
data: z.array(siteSchema),
});
export type HealthSubsystem = SiteStats["health"][number]["subsystem"];
+2 -2
View File
@@ -27,13 +27,13 @@
"ioredis": "5.6.1",
"superjson": "2.2.2",
"winston": "3.17.0",
"zod": "^3.24.2"
"zod": "^3.24.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+4 -4
View File
@@ -33,19 +33,19 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.17.4",
"@mantine/core": "^7.17.5",
"@tabler/icons-react": "^3.31.0",
"dayjs": "^1.11.13",
"next": "15.3.0",
"next": "15.3.1",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "^3.24.2"
"zod": "^3.24.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
@@ -82,7 +82,7 @@ export const AddBoardModal = createModal(({ actions }) => {
<Button onClick={actions.closeModal} variant="subtle" color="gray">
{t("common.action.cancel")}
</Button>
<Button type="submit" color="teal" loading={isPending}>
<Button type="submit" loading={isPending}>
{t("common.action.create")}
</Button>
</Group>
@@ -44,7 +44,7 @@ export const AddGroupModal = createModal<void>(({ actions }) => {
<Button onClick={actions.closeModal} variant="subtle" color="gray">
{t("common.action.cancel")}
</Button>
<Button loading={isPending} type="submit" color="teal">
<Button loading={isPending} type="submit">
{t("common.action.create")}
</Button>
</Group>
@@ -1,4 +1,3 @@
import React from "react";
import { Button, Group, Stack, Text } from "@mantine/core";
import { DateTimePicker } from "@mantine/dates";
import dayjs from "dayjs";
@@ -63,7 +62,7 @@ export const InviteCreateModal = createModal<void>(({ actions }) => {
<Button onClick={actions.closeModal} variant="subtle" color="gray">
{t("common.action.cancel")}
</Button>
<Button type="submit" loading={isPending} color="teal">
<Button type="submit" loading={isPending}>
{t("common.action.create")}
</Button>
</Group>
+3 -3
View File
@@ -24,15 +24,15 @@
"dependencies": {
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^7.17.4",
"@mantine/hooks": "^7.17.4",
"@mantine/core": "^7.17.5",
"@mantine/hooks": "^7.17.5",
"react": "19.1.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+2 -2
View File
@@ -24,14 +24,14 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/ui": "workspace:^0.1.0",
"@mantine/notifications": "^7.17.4",
"@mantine/notifications": "^7.17.5",
"@tabler/icons-react": "^3.31.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+5 -5
View File
@@ -37,14 +37,14 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.17.4",
"@mantine/hooks": "^7.17.4",
"@mantine/core": "^7.17.5",
"@mantine/hooks": "^7.17.5",
"adm-zip": "0.5.16",
"next": "15.3.0",
"next": "15.3.1",
"react": "19.1.0",
"react-dom": "19.1.0",
"superjson": "2.2.2",
"zod": "^3.24.2",
"zod": "^3.24.3",
"zod-form-data": "^2.0.7"
},
"devDependencies": {
@@ -52,7 +52,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/adm-zip": "0.5.7",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+3 -1
View File
@@ -133,7 +133,9 @@ const optionMapping: OptionMapping = {
automationId: (oldOptions) => oldOptions.automationId,
displayName: (oldOptions) => oldOptions.displayName,
},
mediaServer: {},
mediaServer: {
showOnlyPlaying: () => undefined,
},
indexerManager: {
openIndexerSiteInNewTab: (oldOptions) => oldOptions.openIndexerSiteInNewTab,
},
+2 -2
View File
@@ -23,13 +23,13 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"zod": "^3.24.2"
"zod": "^3.24.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+1 -1
View File
@@ -31,7 +31,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+1 -1
View File
@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+1 -1
View File
@@ -38,7 +38,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+5 -3
View File
@@ -9,11 +9,13 @@ import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-
export const mediaServerRequestHandler = createCachedIntegrationRequestHandler<
StreamSession[],
IntegrationKindByCategory<"mediaService">,
Record<string, never>
{
showOnlyPlaying: boolean;
}
>({
async requestAsync(integration, _input) {
async requestAsync(integration, input) {
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getCurrentSessionsAsync();
return await integrationInstance.getCurrentSessionsAsync({ showOnlyPlaying: input.showOnlyPlaying });
},
cacheDuration: dayjs.duration(5, "seconds"),
queryKey: "mediaServerSessions",
@@ -0,0 +1,306 @@
import { z } from "zod";
export interface ReleasesProvider {
getDetailsUrl: (identifier: string) => string | undefined;
parseDetailsResponse: (response: unknown) => z.SafeParseReturnType<unknown, DetailsResponse> | undefined;
getReleasesUrl: (identifier: string) => string;
parseReleasesResponse: (response: unknown) => z.SafeParseReturnType<unknown, ReleasesResponse[]>;
}
interface ProvidersProps {
[key: string]: ReleasesProvider;
DockerHub: ReleasesProvider;
Github: ReleasesProvider;
Gitlab: ReleasesProvider;
Npm: ReleasesProvider;
Codeberg: ReleasesProvider;
}
export const Providers: ProvidersProps = {
DockerHub: {
getDetailsUrl(identifier) {
if (identifier.indexOf("/") > 0) {
const [owner, name] = identifier.split("/");
if (!owner || !name) {
return "";
}
return `https://hub.docker.com/v2/namespaces/${encodeURIComponent(owner)}/repositories/${encodeURIComponent(name)}`;
} else {
return `https://hub.docker.com/v2/repositories/library/${encodeURIComponent(identifier)}`;
}
},
parseDetailsResponse(response) {
return z
.object({
name: z.string(),
namespace: z.string(),
description: z.string(),
star_count: z.number(),
date_registered: z.string().transform((value) => new Date(value)),
})
.transform((resp) => ({
projectUrl: `https://hub.docker.com/r/${resp.namespace === "library" ? "_" : resp.namespace}/${resp.name}`,
projectDescription: resp.description,
isFork: false,
isArchived: false,
createdAt: resp.date_registered,
starsCount: resp.star_count,
openIssues: 0,
forksCount: 0,
}))
.safeParse(response);
},
getReleasesUrl(identifier) {
return `${this.getDetailsUrl(identifier)}/tags?page_size=200`;
},
parseReleasesResponse(response) {
return z
.object({
results: z.array(
z
.object({ name: z.string(), last_updated: z.string().transform((value) => new Date(value)) })
.transform((tag) => ({
identifier: "",
latestRelease: tag.name,
latestReleaseAt: tag.last_updated,
})),
),
})
.transform((resp) => {
return resp.results.map((release) => ({
...release,
releaseUrl: "",
releaseDescription: "",
isPreRelease: false,
}));
})
.safeParse(response);
},
},
Github: {
getDetailsUrl(identifier) {
const [owner, name] = identifier.split("/");
if (!owner || !name) {
return "";
}
return `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
},
parseDetailsResponse(response) {
return z
.object({
html_url: z.string(),
description: z.string(),
fork: z.boolean(),
archived: z.boolean(),
created_at: z.string().transform((value) => new Date(value)),
stargazers_count: z.number(),
open_issues_count: z.number(),
forks_count: z.number(),
})
.transform((resp) => ({
projectUrl: resp.html_url,
projectDescription: resp.description,
isFork: resp.fork,
isArchived: resp.archived,
createdAt: resp.created_at,
starsCount: resp.stargazers_count,
openIssues: resp.open_issues_count,
forksCount: resp.forks_count,
}))
.safeParse(response);
},
getReleasesUrl(identifier) {
return `${this.getDetailsUrl(identifier)}/releases`;
},
parseReleasesResponse(response) {
return z
.array(
z
.object({
tag_name: z.string(),
published_at: z.string().transform((value) => new Date(value)),
html_url: z.string(),
body: z.string(),
prerelease: z.boolean(),
})
.transform((tag) => ({
identifier: "",
latestRelease: tag.tag_name,
latestReleaseAt: tag.published_at,
releaseUrl: tag.html_url,
releaseDescription: tag.body,
isPreRelease: tag.prerelease,
})),
)
.safeParse(response);
},
},
Gitlab: {
getDetailsUrl(identifier) {
return `https://gitlab.com/api/v4/projects/${encodeURIComponent(identifier)}`;
},
parseDetailsResponse(response) {
return z
.object({
web_url: z.string(),
description: z.string(),
forked_from_project: z.object({ id: z.number() }).nullable(),
archived: z.boolean(),
created_at: z.string().transform((value) => new Date(value)),
star_count: z.number(),
open_issues_count: z.number(),
forks_count: z.number(),
})
.transform((resp) => ({
projectUrl: resp.web_url,
projectDescription: resp.description,
isFork: resp.forked_from_project !== null,
isArchived: resp.archived,
createdAt: resp.created_at,
starsCount: resp.star_count,
openIssues: resp.open_issues_count,
forksCount: resp.forks_count,
}))
.safeParse(response);
},
getReleasesUrl(identifier) {
return `${this.getDetailsUrl(identifier)}/releases`;
},
parseReleasesResponse(response) {
return z
.array(
z
.object({
name: z.string(),
released_at: z.string().transform((value) => new Date(value)),
description: z.string(),
_links: z.object({ self: z.string() }),
upcoming_release: z.boolean(),
})
.transform((tag) => ({
identifier: "",
latestRelease: tag.name,
latestReleaseAt: tag.released_at,
releaseUrl: tag._links.self,
releaseDescription: tag.description,
isPreRelease: tag.upcoming_release,
})),
)
.safeParse(response);
},
},
Npm: {
getDetailsUrl(_) {
return undefined;
},
parseDetailsResponse(_) {
return undefined;
},
getReleasesUrl(identifier) {
return `https://registry.npmjs.org/${encodeURIComponent(identifier)}`;
},
parseReleasesResponse(response) {
return z
.object({
time: z.record(z.string().transform((value) => new Date(value))).transform((version) =>
Object.entries(version).map(([key, value]) => ({
identifier: "",
latestRelease: key,
latestReleaseAt: value,
})),
),
versions: z.record(z.object({ description: z.string() })),
name: z.string(),
})
.transform((resp) => {
return resp.time.map((release) => ({
...release,
releaseUrl: `https://www.npmjs.com/package/${resp.name}/v/${release.latestRelease}`,
releaseDescription: resp.versions[release.latestRelease]?.description ?? "",
isPreRelease: false,
}));
})
.safeParse(response);
},
},
Codeberg: {
getDetailsUrl(identifier) {
const [owner, name] = identifier.split("/");
if (!owner || !name) {
return "";
}
return `https://codeberg.org/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
},
parseDetailsResponse(response) {
return z
.object({
html_url: z.string(),
description: z.string(),
fork: z.boolean(),
archived: z.boolean(),
created_at: z.string().transform((value) => new Date(value)),
stars_count: z.number(),
open_issues_count: z.number(),
forks_count: z.number(),
})
.transform((resp) => ({
projectUrl: resp.html_url,
projectDescription: resp.description,
isFork: resp.fork,
isArchived: resp.archived,
createdAt: resp.created_at,
starsCount: resp.stars_count,
openIssues: resp.open_issues_count,
forksCount: resp.forks_count,
}))
.safeParse(response);
},
getReleasesUrl(identifier) {
return `${this.getDetailsUrl(identifier)}/releases`;
},
parseReleasesResponse(response) {
return z
.array(
z
.object({
tag_name: z.string(),
published_at: z.string().transform((value) => new Date(value)),
url: z.string(),
body: z.string(),
prerelease: z.boolean(),
})
.transform((tag) => ({
latestRelease: tag.tag_name,
latestReleaseAt: tag.published_at,
releaseUrl: tag.url,
releaseDescription: tag.body,
isPreRelease: tag.prerelease,
})),
)
.safeParse(response);
},
},
};
const _detailsSchema = z.object({
projectUrl: z.string(),
projectDescription: z.string(),
isFork: z.boolean(),
isArchived: z.boolean(),
createdAt: z.date(),
starsCount: z.number(),
openIssues: z.number(),
forksCount: z.number(),
});
const _releasesSchema = z.object({
latestRelease: z.string(),
latestReleaseAt: z.date(),
releaseUrl: z.string(),
releaseDescription: z.string(),
isPreRelease: z.boolean(),
});
export type DetailsResponse = z.infer<typeof _detailsSchema>;
export type ReleasesResponse = z.infer<typeof _releasesSchema>;
+105
View File
@@ -0,0 +1,105 @@
import dayjs from "dayjs";
import { z } from "zod";
import { fetchWithTimeout } from "@homarr/common";
import { logger } from "@homarr/log";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
import { Providers } from "./releases-providers";
import type { DetailsResponse } from "./releases-providers";
const _reponseSchema = z.object({
identifier: z.string(),
providerKey: z.string(),
latestRelease: z.string(),
latestReleaseAt: z.date(),
releaseUrl: z.string(),
releaseDescription: z.string(),
isPreRelease: z.boolean(),
projectUrl: z.string(),
projectDescription: z.string(),
isFork: z.boolean(),
isArchived: z.boolean(),
createdAt: z.date(),
starsCount: z.number(),
openIssues: z.number(),
forksCount: z.number(),
});
export const releasesRequestHandler = createCachedWidgetRequestHandler({
queryKey: "releasesApiResult",
widgetKind: "releases",
async requestAsync(input: { providerKey: string; identifier: string; versionRegex: string | undefined }) {
const provider = Providers[input.providerKey];
if (!provider) return undefined;
let detailsResult: DetailsResponse = {
projectUrl: "",
projectDescription: "",
isFork: false,
isArchived: false,
createdAt: new Date(0),
starsCount: 0,
openIssues: 0,
forksCount: 0,
};
const detailsUrl = provider.getDetailsUrl(input.identifier);
if (detailsUrl !== undefined) {
const detailsResponse = await fetchWithTimeout(detailsUrl);
const parsedDetails = provider.parseDetailsResponse(await detailsResponse.json());
if (parsedDetails?.success) {
detailsResult = parsedDetails.data;
} else {
logger.warn("Failed to parse details response", {
provider: input.providerKey,
identifier: input.identifier,
detailsUrl,
error: parsedDetails?.error,
});
}
}
const releasesResponse = await fetchWithTimeout(provider.getReleasesUrl(input.identifier));
const releasesResult = provider.parseReleasesResponse(await releasesResponse.json());
if (!releasesResult.success) return undefined;
const latest: ResponseResponse = releasesResult.data
.filter((result) => (input.versionRegex ? new RegExp(input.versionRegex).test(result.latestRelease) : true))
.reduce(
(latest, result) => {
return {
...detailsResult,
...(result.latestReleaseAt > latest.latestReleaseAt ? result : latest),
identifier: input.identifier,
providerKey: input.providerKey,
};
},
{
identifier: "",
providerKey: "",
latestRelease: "",
latestReleaseAt: new Date(0),
releaseUrl: "",
releaseDescription: "",
isPreRelease: false,
projectUrl: "",
projectDescription: "",
isFork: false,
isArchived: false,
createdAt: new Date(0),
starsCount: 0,
openIssues: 0,
forksCount: 0,
},
);
return latest;
},
cacheDuration: dayjs.duration(5, "minutes"),
});
export type ResponseResponse = z.infer<typeof _reponseSchema>;
+1 -1
View File
@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+3 -3
View File
@@ -25,8 +25,8 @@
"@homarr/api": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/dates": "^7.17.4",
"next": "15.3.0",
"@mantine/dates": "^7.17.5",
"next": "15.3.1",
"react": "19.1.0",
"react-dom": "19.1.0"
},
@@ -34,7 +34,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+6 -6
View File
@@ -33,12 +33,12 @@
"@homarr/settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^7.17.4",
"@mantine/hooks": "^7.17.4",
"@mantine/spotlight": "^7.17.4",
"@mantine/core": "^7.17.5",
"@mantine/hooks": "^7.17.5",
"@mantine/spotlight": "^7.17.5",
"@tabler/icons-react": "^3.31.0",
"jotai": "^2.12.2",
"next": "15.3.0",
"jotai": "^2.12.3",
"next": "15.3.1",
"react": "19.1.0",
"react-dom": "19.1.0",
"use-deep-compare-effect": "^1.8.1"
@@ -47,7 +47,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+3 -3
View File
@@ -32,8 +32,8 @@
"dayjs": "^1.11.13",
"deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.3.0",
"next-intl": "4.0.2",
"next": "15.3.1",
"next-intl": "4.1.0",
"react": "19.1.0",
"react-dom": "19.1.0"
},
@@ -41,7 +41,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.24.0",
"eslint": "^9.25.1",
"typescript": "^5.8.3"
}
}
+25 -3
View File
@@ -611,8 +611,18 @@
"action": {
"select": {
"label": "Seleccioneu l'aplicació",
"notFound": "No s'ha trobat cap aplicació"
}
"notFound": "No s'ha trobat cap aplicació",
"search": "",
"noResults": "",
"action": "",
"title": ""
},
"create": {
"title": "",
"description": "",
"action": ""
},
"add": ""
}
},
"integration": {
@@ -1082,6 +1092,9 @@
"integrations": {
"label": ""
},
"title": {
"label": ""
},
"customCssClasses": {
"label": ""
},
@@ -1756,7 +1769,12 @@
"mediaServer": {
"name": "",
"description": "",
"option": {},
"option": {
"showOnlyPlaying": {
"label": "",
"description": ""
}
},
"items": {
"currentlyPlaying": "",
"user": "",
@@ -3790,6 +3808,10 @@
"noResults": {
"title": ""
},
"invalid": {
"title": "",
"description": ""
},
"expires": ""
}
},
Loaded 100 of 157 files, more files were not shown because too many files have changed in this diff. Show more