Compare commits

..
76 changed files with 3846 additions and 14439 deletions

No files matched your search

@@ -20,7 +20,7 @@ runs:
using: "composite" using: "composite"
steps: steps:
- name: Prebuilt debian dependencies - name: Prebuilt debian dependencies
uses: homarr-labs/homarr/.github/actions/prebuilt-debian@dev uses: ./.github/actions/prebuilt-debian@dev
id: prebuilt-debian id: prebuilt-debian
with: with:
architecture: ${{ inputs.architecture }} architecture: ${{ inputs.architecture }}
-93
View File
@@ -1,96 +1,3 @@
## [1.49.0](https://github.com/homarr-labs/homarr/compare/v1.48.0...v1.49.0) (2026-01-02)
### Features
* unraid integration ([#4439](https://github.com/homarr-labs/homarr/issues/4439)) ([9b0dd6d](https://github.com/homarr-labs/homarr/commit/9b0dd6d6cebda4a541c8859506d89e9e9f782fa4))
### Bug Fixes
* add orphaned status for containers with invalid stats ([#4441](https://github.com/homarr-labs/homarr/issues/4441)) ([f04f537](https://github.com/homarr-labs/homarr/commit/f04f53759b6890d5d2dd683a9bf124d8ff12780b))
* **deps:** update dependency @xterm/addon-fit to v0.11.0 ([#4756](https://github.com/homarr-labs/homarr/issues/4756)) ([1c727e5](https://github.com/homarr-labs/homarr/commit/1c727e59af4041e5e41da854633ccd3c030c1d18))
* **deps:** update dependency @xterm/xterm to v6 ([#4757](https://github.com/homarr-labs/homarr/issues/4757)) ([fad478c](https://github.com/homarr-labs/homarr/commit/fad478c549cdbc616f921d9b444bff48b59e77a5))
* **deps:** update dependency isomorphic-dompurify to ^2.35.0 ([#4779](https://github.com/homarr-labs/homarr/issues/4779)) ([236fa35](https://github.com/homarr-labs/homarr/commit/236fa35f7e0943be33bba153e7030ce4e3bb204e))
* **deps:** update dependency jotai to ^2.16.1 ([#4772](https://github.com/homarr-labs/homarr/issues/4772)) ([d002700](https://github.com/homarr-labs/homarr/commit/d00270046a041a10c1c540cfa79c1eee477f3303))
* **deps:** update dependency ldapts to v8.0.31 ([#4755](https://github.com/homarr-labs/homarr/issues/4755)) ([8b09693](https://github.com/homarr-labs/homarr/commit/8b096939e4e1da8e98791a8e05f108a136fe2810))
* **deps:** update dependency ldapts to v8.0.32 ([#4762](https://github.com/homarr-labs/homarr/issues/4762)) ([49afb82](https://github.com/homarr-labs/homarr/commit/49afb8244615f67af0586aaacf77a509231ac6dc))
* **deps:** update dependency ldapts to v8.0.34 ([#4765](https://github.com/homarr-labs/homarr/issues/4765)) ([809bf55](https://github.com/homarr-labs/homarr/commit/809bf559fe1b96df9a903dc790d138cad7368adb))
* **deps:** update dependency ldapts to v8.0.35 ([#4768](https://github.com/homarr-labs/homarr/issues/4768)) ([e574dd7](https://github.com/homarr-labs/homarr/commit/e574dd70f1421e9254a289a0f02fd0b4d43e26ad))
* **deps:** update dependency typescript-eslint to ^8.50.1 ([#4758](https://github.com/homarr-labs/homarr/issues/4758)) ([adaa803](https://github.com/homarr-labs/homarr/commit/adaa8039dd545109246ccd9fa15570739f755f62))
* **deps:** update nextjs monorepo to v16.1.1 ([#4759](https://github.com/homarr-labs/homarr/issues/4759)) ([7712ab4](https://github.com/homarr-labs/homarr/commit/7712ab410b5094e5d84df75461e07efdfccc285b))
* **deps:** update tanstack-query monorepo ([#4782](https://github.com/homarr-labs/homarr/issues/4782)) ([1e5d38f](https://github.com/homarr-labs/homarr/commit/1e5d38fd3ca4c68fa4c61b02757a3cff39839fdb))
* **deps:** update testcontainers-node monorepo to ^11.11.0 ([#4781](https://github.com/homarr-labs/homarr/issues/4781)) ([e40a116](https://github.com/homarr-labs/homarr/commit/e40a11608cb21a75e6910028abf97e14b5275f56))
* **deps:** update turbo monorepo to ^2.7.2 ([#4764](https://github.com/homarr-labs/homarr/issues/4764)) ([9191430](https://github.com/homarr-labs/homarr/commit/919143022969350f6ee7be2f53be2fe09a89bba3))
## [1.48.0](https://github.com/homarr-labs/homarr/compare/v1.47.0...v1.48.0) (2025-12-26)
### Features
* enable dns caching by default ([#4706](https://github.com/homarr-labs/homarr/issues/4706)) ([fbb5298](https://github.com/homarr-labs/homarr/commit/fbb5298cb05b12eb2f19c121a1bc103caeb3e576))
* **http:** add proxy support ([#4721](https://github.com/homarr-labs/homarr/issues/4721)) ([5a57115](https://github.com/homarr-labs/homarr/commit/5a57115ca073aea1c75a000fda6a23648c5488e4))
### Bug Fixes
* **deps:** update dependency @t3-oss/env-nextjs to ^0.13.10 ([#4710](https://github.com/homarr-labs/homarr/issues/4710)) ([689523a](https://github.com/homarr-labs/homarr/commit/689523a194ab48bdc072b1cdf65f07ade6cbc584))
* **deps:** update dependency ldapts to v8.0.24 ([#4711](https://github.com/homarr-labs/homarr/issues/4711)) ([648f7e2](https://github.com/homarr-labs/homarr/commit/648f7e2c7743af81f4bced36b7e1e80c21e851e5))
* **deps:** update dependency ldapts to v8.0.25 ([#4724](https://github.com/homarr-labs/homarr/issues/4724)) ([ef12b2b](https://github.com/homarr-labs/homarr/commit/ef12b2b5c2e1131138937fb6da61ce4c167fe0eb))
* **deps:** update dependency ldapts to v8.0.27 ([#4737](https://github.com/homarr-labs/homarr/issues/4737)) ([8891ac4](https://github.com/homarr-labs/homarr/commit/8891ac42399e47fda6efc657b8ff2fe58aebca9e))
* **deps:** update dependency ldapts to v8.0.28 ([#4743](https://github.com/homarr-labs/homarr/issues/4743)) ([75c5453](https://github.com/homarr-labs/homarr/commit/75c545344aae39550917331458c01a894b6a942f))
* **deps:** update dependency ldapts to v8.0.29 ([#4747](https://github.com/homarr-labs/homarr/issues/4747)) ([5dd7400](https://github.com/homarr-labs/homarr/commit/5dd74006843ae72f63e631b14f0fd50c911d610b))
* **deps:** update dependency ldapts to v8.0.30 ([#4749](https://github.com/homarr-labs/homarr/issues/4749)) ([96c2c0d](https://github.com/homarr-labs/homarr/commit/96c2c0d604737c64722fa21bf79965c806bf4da3))
* **deps:** update dependency mysql2 to v3.16.0 ([#4731](https://github.com/homarr-labs/homarr/issues/4731)) ([f34bdce](https://github.com/homarr-labs/homarr/commit/f34bdce6446d4e62b54b96f63e91cfdddd51e901))
* **deps:** update dependency next-intl to v4.6.1 ([#4730](https://github.com/homarr-labs/homarr/issues/4730)) ([f4d782a](https://github.com/homarr-labs/homarr/commit/f4d782abefb78e085ca3fc66034c0ca54fb21120))
* **deps:** update dependency sass to ^1.97.0 ([#4726](https://github.com/homarr-labs/homarr/issues/4726)) ([88baad8](https://github.com/homarr-labs/homarr/commit/88baad8143f4ca830e50644d6bd1d1980ef1da82))
* **deps:** update dependency sass to ^1.97.1 ([#4746](https://github.com/homarr-labs/homarr/issues/4746)) ([6b774f4](https://github.com/homarr-labs/homarr/commit/6b774f47d0a8bf731e7e1837a9d47e0dc53a1ff0))
* **deps:** update dependency typescript-eslint to ^8.50.0 ([#4720](https://github.com/homarr-labs/homarr/issues/4720)) ([2dc8fea](https://github.com/homarr-labs/homarr/commit/2dc8feae7ea6174a1d826d8f0151ddc2c04c6aa6))
* **deps:** update dependency zod to ^4.2.0 ([#4712](https://github.com/homarr-labs/homarr/issues/4712)) ([6e39524](https://github.com/homarr-labs/homarr/commit/6e39524f9d28d201c13738212d238726e1685832))
* **deps:** update dependency zod to ^4.2.1 ([#4722](https://github.com/homarr-labs/homarr/issues/4722)) ([68dc684](https://github.com/homarr-labs/homarr/commit/68dc6844fdee53aca55b44dd7d19bf9d3a35c057))
* **deps:** update nextjs monorepo to v16.1.0 ([#4739](https://github.com/homarr-labs/homarr/issues/4739)) ([99f2842](https://github.com/homarr-labs/homarr/commit/99f2842a5a102056c76ddc68cb5083791f324c3c))
* **deps:** update tiptap monorepo to v3.14.0 ([#4744](https://github.com/homarr-labs/homarr/issues/4744)) ([adbe707](https://github.com/homarr-labs/homarr/commit/adbe707c73f462771c3d21cbbcc21e8245eea6f3))
* **deps:** update trpc monorepo to ^11.8.1 ([#4750](https://github.com/homarr-labs/homarr/issues/4750)) ([93bf9ab](https://github.com/homarr-labs/homarr/commit/93bf9ab84b5387eefb8cc0b03057a4ccf0b13aa9))
* **deps:** update turbo monorepo to ^2.7.0 ([#4745](https://github.com/homarr-labs/homarr/issues/4745)) ([5d659a3](https://github.com/homarr-labs/homarr/commit/5d659a3ebbf79266158cb57236de90e7883e7a4e))
* **deps:** update turbo monorepo to ^2.7.1 ([#4751](https://github.com/homarr-labs/homarr/issues/4751)) ([d8db016](https://github.com/homarr-labs/homarr/commit/d8db0169a142c2ec302806fe946cb17431ccae6b))
## [1.47.0](https://github.com/homarr-labs/homarr/compare/v1.46.0...v1.47.0) (2025-12-19)
### Features
* [#4452](https://github.com/homarr-labs/homarr/issues/4452) improve calendar design ([#4461](https://github.com/homarr-labs/homarr/issues/4461)) ([08e9f7d](https://github.com/homarr-labs/homarr/commit/08e9f7d8d85475788534b46e6cceb40a7231c09d))
### Bug Fixes
* **boards:** selection menu not shown to users without change access ([#4689](https://github.com/homarr-labs/homarr/issues/4689)) ([a0a11e3](https://github.com/homarr-labs/homarr/commit/a0a11e35707513fee45551e09c26ff633f636c32))
* **deps:** update dependency dns-caching to ^0.2.9 ([#4648](https://github.com/homarr-labs/homarr/issues/4648)) ([6deab2e](https://github.com/homarr-labs/homarr/commit/6deab2ecec2dc4f5192b53213cb755a9853eb7c0))
* **deps:** update dependency drizzle-orm to ^0.45.1 ([#4663](https://github.com/homarr-labs/homarr/issues/4663)) ([f635009](https://github.com/homarr-labs/homarr/commit/f63500961f11589a972a41dde9e4117b1f25141c))
* **deps:** update dependency fast-xml-parser to ^5.3.3 ([#4683](https://github.com/homarr-labs/homarr/issues/4683)) ([611b65e](https://github.com/homarr-labs/homarr/commit/611b65e8bd420aaad580bf35071b651093623c95))
* **deps:** update dependency isomorphic-dompurify to ^2.34.0 ([#4650](https://github.com/homarr-labs/homarr/issues/4650)) ([5d5c18b](https://github.com/homarr-labs/homarr/commit/5d5c18b3649d5fa1b2b933d443ccd5abcbb84e3a))
* **deps:** update dependency jotai to ^2.16.0 ([#4655](https://github.com/homarr-labs/homarr/issues/4655)) ([4aef85b](https://github.com/homarr-labs/homarr/commit/4aef85ba60f4978da5a0b13c08e1799949fe4497))
* **deps:** update dependency ldapts to v8.0.16 ([#4640](https://github.com/homarr-labs/homarr/issues/4640)) ([ada5597](https://github.com/homarr-labs/homarr/commit/ada5597cf3ab0bafb04e9176b431d257b36d1dc8))
* **deps:** update dependency ldapts to v8.0.17 ([#4642](https://github.com/homarr-labs/homarr/issues/4642)) ([c09da85](https://github.com/homarr-labs/homarr/commit/c09da85f3a8450a70991ff0656228a9aa1364f89))
* **deps:** update dependency ldapts to v8.0.18 ([#4653](https://github.com/homarr-labs/homarr/issues/4653)) ([4bd142e](https://github.com/homarr-labs/homarr/commit/4bd142e09c666cc84c67c93e2202e571565b766c))
* **deps:** update dependency ldapts to v8.0.19 ([#4669](https://github.com/homarr-labs/homarr/issues/4669)) ([8ed771d](https://github.com/homarr-labs/homarr/commit/8ed771d30073809b08a6ace2c29b3de2675e37d1))
* **deps:** update dependency ldapts to v8.0.20 ([#4684](https://github.com/homarr-labs/homarr/issues/4684)) ([703c8cc](https://github.com/homarr-labs/homarr/commit/703c8ccc060bb1775a72f405da1e169926b1ce5d))
* **deps:** update dependency ldapts to v8.0.21 ([#4695](https://github.com/homarr-labs/homarr/issues/4695)) ([7f6eaa0](https://github.com/homarr-labs/homarr/commit/7f6eaa0b1f397a37b6acd15e0ccfea75ee2e0161))
* **deps:** update dependency ldapts to v8.0.22 ([#4700](https://github.com/homarr-labs/homarr/issues/4700)) ([391d725](https://github.com/homarr-labs/homarr/commit/391d725191563ba5048593c14baf077a924cea44))
* **deps:** update dependency ldapts to v8.0.23 ([#4701](https://github.com/homarr-labs/homarr/issues/4701)) ([db337b2](https://github.com/homarr-labs/homarr/commit/db337b2525762c96acd815bd4fdacfaabff5f0cf))
* **deps:** update dependency next-intl to v4.6.0 ([#4691](https://github.com/homarr-labs/homarr/issues/4691)) ([1ff6fac](https://github.com/homarr-labs/homarr/commit/1ff6fac71bb9187d0eb703a7b6543daf8861efe8))
* **deps:** update dependency sass to ^1.94.3 ([#4646](https://github.com/homarr-labs/homarr/issues/4646)) ([7e91d12](https://github.com/homarr-labs/homarr/commit/7e91d12439826eee76f118448c583294c3c46b42))
* **deps:** update dependency sass to ^1.95.0 ([#4647](https://github.com/homarr-labs/homarr/issues/4647)) ([eb760da](https://github.com/homarr-labs/homarr/commit/eb760da8b30583575fb638995fc2f2acbd5b2961))
* **deps:** update dependency sass to ^1.95.1 ([#4661](https://github.com/homarr-labs/homarr/issues/4661)) ([e327c66](https://github.com/homarr-labs/homarr/commit/e327c660b239840c4929f536eff3096342cc3433))
* **deps:** update dependency sass to ^1.96.0 ([#4668](https://github.com/homarr-labs/homarr/issues/4668)) ([bdfc150](https://github.com/homarr-labs/homarr/commit/bdfc1501152450a1546962331037a6fc1c782e4e))
* **deps:** update dependency swagger-ui-react to ^5.31.0 ([#4672](https://github.com/homarr-labs/homarr/issues/4672)) ([4d593f5](https://github.com/homarr-labs/homarr/commit/4d593f5b9d1f47ae2368f42131e8ef8d32f73fc9))
* **deps:** update dependency typescript-eslint to ^8.49.0 ([#4645](https://github.com/homarr-labs/homarr/issues/4645)) ([220cb66](https://github.com/homarr-labs/homarr/commit/220cb669a13fead953ff42040b39e06d1dcc1fe2))
* **deps:** update mantine monorepo to ^8.3.10 ([#4651](https://github.com/homarr-labs/homarr/issues/4651)) ([d0652f7](https://github.com/homarr-labs/homarr/commit/d0652f7cf5ac0e63a61adcfe43e53c4ed130e8fc))
* **deps:** update react monorepo to v19.2.2 ([#4676](https://github.com/homarr-labs/homarr/issues/4676)) ([82e79f2](https://github.com/homarr-labs/homarr/commit/82e79f26274a289e1ce4f9331b6ba16f00f5a86c))
* **deps:** update react monorepo to v19.2.3 ([#4680](https://github.com/homarr-labs/homarr/issues/4680)) ([4c71856](https://github.com/homarr-labs/homarr/commit/4c7185646d9d061d1aebf9194f9ef2f810fc3384))
* **deps:** update testcontainers-node monorepo to ^11.10.0 ([#4659](https://github.com/homarr-labs/homarr/issues/4659)) ([c880ce9](https://github.com/homarr-labs/homarr/commit/c880ce9bd99a7056f20d450275fcbc37f9372f63))
* **deps:** update trpc monorepo to ^11.8.0 ([#4697](https://github.com/homarr-labs/homarr/issues/4697)) ([1f44845](https://github.com/homarr-labs/homarr/commit/1f44845df489bb33bcfe943c6a05c0f157037ed6))
* **downloads:** improve progress to only show 100 percent if fully completed ([#4621](https://github.com/homarr-labs/homarr/issues/4621)) ([1062745](https://github.com/homarr-labs/homarr/commit/10627453d5b5a17991bbe9b8d4bc23981b366c39))
* **iframe-widget:** permission selection not applying to any origin ([#4616](https://github.com/homarr-labs/homarr/issues/4616)) ([479f083](https://github.com/homarr-labs/homarr/commit/479f083ab8ec405d1b8296b32a20262f6192a1f9))
* **invites:** creation with custom expiration not working ([#4677](https://github.com/homarr-labs/homarr/issues/4677)) ([423da9e](https://github.com/homarr-labs/homarr/commit/423da9eeb88510a123bdd2447dc97b54c5f1dec4))
* reference to prebuilt-debian action does not work ([47d7458](https://github.com/homarr-labs/homarr/commit/47d7458143d8801947cae1985745fa827b247dd1))
* **stock-price:** use previous-close for change calculation ([#4674](https://github.com/homarr-labs/homarr/issues/4674)) ([17fc7a0](https://github.com/homarr-labs/homarr/commit/17fc7a043b4adbed9f14408e23c95ec5fba0c8b8))
* **tdarr:** array parse error ([#4678](https://github.com/homarr-labs/homarr/issues/4678)) ([d86af07](https://github.com/homarr-labs/homarr/commit/d86af072bf136d76b02f3efa2c15520a4f63974e))
## [1.46.0](https://github.com/homarr-labs/homarr/compare/v1.45.3...v1.46.0) (2025-12-12) ## [1.46.0](https://github.com/homarr-labs/homarr/compare/v1.45.3...v1.46.0) (2025-12-12)
### Features ### Features
+14 -15
View File
@@ -55,17 +55,18 @@
"@mantine/modals": "^8.3.10", "@mantine/modals": "^8.3.10",
"@mantine/tiptap": "^8.3.10", "@mantine/tiptap": "^8.3.10",
"@million/lint": "1.0.14", "@million/lint": "1.0.14",
"@scalar/api-reference-react": "^0.8.11",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"@tanstack/react-query": "^5.90.14", "@tanstack/react-query": "^5.90.12",
"@tanstack/react-query-devtools": "^5.91.2", "@tanstack/react-query-devtools": "^5.91.1",
"@tanstack/react-query-next-experimental": "^5.91.0", "@tanstack/react-query-next-experimental": "^5.91.0",
"@trpc/client": "^11.8.1", "@trpc/client": "^11.8.0",
"@trpc/next": "^11.8.1", "@trpc/next": "^11.8.0",
"@trpc/react-query": "^11.8.1", "@trpc/react-query": "^11.8.0",
"@trpc/server": "^11.8.1", "@trpc/server": "^11.8.0",
"@xterm/addon-canvas": "^0.7.0", "@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.11.0", "@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^6.0.0", "@xterm/xterm": "^5.5.0",
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"chroma-js": "^3.2.0", "chroma-js": "^3.2.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -73,21 +74,20 @@
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"flag-icons": "^7.5.0", "flag-icons": "^7.5.0",
"glob": "^13.0.0", "glob": "^13.0.0",
"isomorphic-dompurify": "^2.35.0", "isomorphic-dompurify": "^2.34.0",
"jotai": "^2.16.1", "jotai": "^2.16.0",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.1.1", "next": "16.0.10",
"postcss-preset-mantine": "^1.18.0", "postcss-preset-mantine": "^1.18.0",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1", "react-simple-code-editor": "^0.14.1",
"sass": "^1.97.1", "sass": "^1.96.0",
"superjson": "2.2.6", "superjson": "2.2.6",
"swagger-ui-react": "^5.31.0",
"use-deep-compare-effect": "^1.8.1", "use-deep-compare-effect": "^1.8.1",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
@@ -98,7 +98,6 @@
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@types/react": "19.2.7", "@types/react": "19.2.7",
"@types/react-dom": "19.2.3", "@types/react-dom": "19.2.3",
"@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"node-loader": "^2.1.0", "node-loader": "^2.1.0",
@@ -1,23 +0,0 @@
"use client";
import type { OpenAPIObject } from "openapi3-ts/oas31";
import SwaggerUI from "swagger-ui-react";
// workaround for CSS that cannot be processed by next.js, https://github.com/swagger-api/swagger-ui/issues/10045
import "../swagger-ui-dark.css";
import "../swagger-ui-overrides.css";
import "../swagger-ui.css";
interface SwaggerUIClientProps {
document: OpenAPIObject;
}
export const SwaggerUIClient = ({ document }: SwaggerUIClientProps) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const requestInterceptor = (req: Record<string, any>) => {
req.credentials = "omit";
return req;
};
return <SwaggerUI requestInterceptor={requestInterceptor} spec={document} />;
};
@@ -1,6 +1,7 @@
import { headers } from "next/headers"; import { headers } from "next/headers";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { Stack, Tabs, TabsList, TabsPanel, TabsTab } from "@mantine/core"; import { Stack, Tabs, TabsList, TabsPanel, TabsTab } from "@mantine/core";
import { ApiReferenceReact } from "@scalar/api-reference-react";
import { openApiDocument } from "@homarr/api"; import { openApiDocument } from "@homarr/api";
import { api } from "@homarr/api/server"; import { api } from "@homarr/api/server";
@@ -8,11 +9,14 @@ import { auth } from "@homarr/auth/next";
import { extractBaseUrlFromHeaders } from "@homarr/common"; import { extractBaseUrlFromHeaders } from "@homarr/common";
import { getScopedI18n } from "@homarr/translation/server"; import { getScopedI18n } from "@homarr/translation/server";
import { SwaggerUIClient } from "~/app/[locale]/manage/tools/api/components/swagger-ui";
import { createMetaTitle } from "~/metadata"; import { createMetaTitle } from "~/metadata";
import { ApiKeysManagement } from "./components/api-keys"; import { ApiKeysManagement } from "./components/api-keys";
export async function generateMetadata() { import "@scalar/api-reference-react/style.css";
import { Metadata } from "next";
export async function generateMetadata(): Promise<Metadata> {
const session = await auth(); const session = await auth();
if (!session?.user || !session.user.permissions.includes("admin")) { if (!session?.user || !session.user.permissions.includes("admin")) {
return {}; return {};
@@ -45,7 +49,21 @@ export default async function ApiPage() {
<ApiKeysManagement apiKeys={apiKeys} /> <ApiKeysManagement apiKeys={apiKeys} />
</TabsPanel> </TabsPanel>
<TabsPanel value={"documentation"}> <TabsPanel value={"documentation"}>
<SwaggerUIClient document={document} /> <ApiReferenceReact
configuration={{
content: document,
layout: "classic",
showSidebar: false,
hideDarkModeToggle: true,
showDeveloperTools: "never",
hiddenClients: true,
darkMode: session.user.colorScheme === "dark",
authentication: {
preferredSecurityScheme: "apikey",
},
hideSearch: true,
}}
/>
</TabsPanel> </TabsPanel>
</Tabs> </Tabs>
</Stack> </Stack>
File diff suppressed because it is too large. Load diff
@@ -1,4 +0,0 @@
.swagger-ui .info {
margin: 0 !important;
margin-bottom: 20px !important;
}
File diff suppressed because it is too large. Load diff
@@ -76,7 +76,6 @@ const createColumns = (
accessorKey: "ports", accessorKey: "ports",
header: t("docker.field.ports.label"), header: t("docker.field.ports.label"),
Cell({ cell }) { Cell({ cell }) {
if (!cell.row.original.ports.length) return null;
return ( return (
<OverflowBadge overflowCount={1} data={cell.row.original.ports.map((port) => port.PrivatePort.toString())} /> <OverflowBadge overflowCount={1} data={cell.row.original.ports.map((port) => port.PrivatePort.toString())} />
); );
+1 -1
View File
@@ -49,7 +49,7 @@
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^24.10.4", "@types/node": "^24.10.4",
"dotenv-cli": "^11.0.0", "dotenv-cli": "^11.0.0",
"esbuild": "^0.27.2", "esbuild": "^0.27.1",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"tsx": "4.20.4", "tsx": "4.20.4",
+1 -1
View File
@@ -34,7 +34,7 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"esbuild": "^0.27.2", "esbuild": "^0.27.1",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"typescript": "^5.9.3" "typescript": "^5.9.3"
+2 -2
View File
@@ -4,10 +4,10 @@
"dependencies": { "dependencies": {
"better-sqlite3": "^12.5.0" "better-sqlite3": "^12.5.0"
}, },
"packageManager": "pnpm@10.26.2", "packageManager": "pnpm@10.25.0",
"engines": { "engines": {
"node": ">=24.12.0", "node": ">=24.12.0",
"pnpm": ">=10.26.2" "pnpm": ">=10.25.0"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
-7
View File
@@ -314,13 +314,6 @@
<br/> <br/>
<p align="center">Unifi<br/>Controller</p> <p align="center">Unifi<br/>Controller</p>
</a> </a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/unraid" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/unraid.svg" alt="Unraid" width="90" height="90" />
<br/>
<p align="center">Unraid</p>
</a>
</td></tr> </td></tr>
</tbody> </tbody>
</table> </table>
+16 -16
View File
@@ -1,6 +1,6 @@
{ {
"name": "homarr", "name": "homarr",
"version": "1.49.0", "version": "1.46.0",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "cross-env CI=true turbo build", "build": "cross-env CI=true turbo build",
@@ -42,27 +42,27 @@
"@semantic-release/github": "^12.0.2", "@semantic-release/github": "^12.0.2",
"@semantic-release/npm": "^13.1.3", "@semantic-release/npm": "^13.1.3",
"@semantic-release/release-notes-generator": "^14.1.0", "@semantic-release/release-notes-generator": "^14.1.0",
"@testcontainers/redis": "^11.11.0", "@testcontainers/redis": "^11.10.0",
"@turbo/gen": "^2.7.2", "@turbo/gen": "^2.6.3",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^5.1.2",
"@vitest/coverage-v8": "^4.0.16", "@vitest/coverage-v8": "^4.0.15",
"@vitest/ui": "^4.0.16", "@vitest/ui": "^4.0.15",
"conventional-changelog-conventionalcommits": "^9.1.0", "conventional-changelog-conventionalcommits": "^9.1.0",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"jsdom": "^27.4.0", "jsdom": "^27.3.0",
"json5": "^2.2.3", "json5": "^2.2.3",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"semantic-release": "^25.0.2", "semantic-release": "^25.0.2",
"testcontainers": "^11.11.0", "testcontainers": "^11.10.0",
"turbo": "^2.7.2", "turbo": "^2.6.3",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite-tsconfig-paths": "^6.0.3", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.0.16" "vitest": "^4.0.15"
}, },
"packageManager": "pnpm@10.26.2", "packageManager": "pnpm@10.25.0",
"engines": { "engines": {
"node": ">=24.12.0", "node": ">=24.12.0",
"pnpm": ">=10.26.2" "pnpm": ">=10.25.0"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
@@ -82,20 +82,20 @@
"axios@>=1.0.0 <1.8.2": ">=1.13.2", "axios@>=1.0.0 <1.8.2": ">=1.13.2",
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1", "brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1", "brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
"esbuild@<=0.24.2": ">=0.27.2", "esbuild@<=0.24.2": ">=0.27.1",
"form-data@>=4.0.0 <4.0.4": ">=4.0.5", "form-data@>=4.0.0 <4.0.4": ">=4.0.5",
"hono@<4.6.5": ">=4.11.3", "hono@<4.6.5": ">=4.11.0",
"linkifyjs@<4.3.2": ">=4.3.2", "linkifyjs@<4.3.2": ">=4.3.2",
"nanoid@>=4.0.0 <5.0.9": ">=5.1.6", "nanoid@>=4.0.0 <5.0.9": ">=5.1.6",
"prismjs@<1.30.0": ">=1.30.0", "prismjs@<1.30.0": ">=1.30.0",
"proxmox-api>undici": "7.16.0", "proxmox-api>undici": "7.16.0",
"react-is": "^19.2.3", "react-is": "^19.2.3",
"rollup@>=4.0.0 <4.22.4": ">=4.54.0", "rollup@>=4.0.0 <4.22.4": ">=4.53.3",
"sha.js@<=2.4.11": ">=2.4.12", "sha.js@<=2.4.11": ">=2.4.12",
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1", "tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1", "tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
"tmp@<=0.2.3": ">=0.2.5", "tmp@<=0.2.3": ">=0.2.5",
"vite@>=5.0.0 <=5.4.18": ">=7.3.0" "vite@>=5.0.0 <=5.4.18": ">=7.2.7"
}, },
"patchedDependencies": { "patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch", "@types/node-unifi": "patches/@types__node-unifi.patch",
+7 -7
View File
@@ -41,18 +41,18 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.4.0", "@kubernetes/client-node": "^1.4.0",
"@tanstack/react-query": "^5.90.14", "@tanstack/react-query": "^5.90.12",
"@trpc/client": "^11.8.1", "@trpc/client": "^11.8.0",
"@trpc/react-query": "^11.8.1", "@trpc/react-query": "^11.8.0",
"@trpc/server": "^11.8.1", "@trpc/server": "^11.8.0",
"@trpc/tanstack-react-query": "^11.8.1", "@trpc/tanstack-react-query": "^11.8.0",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"next": "16.1.1", "next": "16.0.10",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"superjson": "2.2.6", "superjson": "2.2.6",
"trpc-to-openapi": "^3.1.0", "trpc-to-openapi": "^3.1.0",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+2
View File
@@ -11,6 +11,7 @@ import { importRouter } from "./router/import/import-router";
import { infoRouter } from "./router/info"; import { infoRouter } from "./router/info";
import { integrationRouter } from "./router/integration/integration-router"; import { integrationRouter } from "./router/integration/integration-router";
import { inviteRouter } from "./router/invite"; import { inviteRouter } from "./router/invite";
import { itemsRouter } from "./router/item/item-router";
import { kubernetesRouter } from "./router/kubernetes/router/kubernetes-router"; import { kubernetesRouter } from "./router/kubernetes/router/kubernetes-router";
import { locationRouter } from "./router/location"; import { locationRouter } from "./router/location";
import { logRouter } from "./router/log"; import { logRouter } from "./router/log";
@@ -49,6 +50,7 @@ export const appRouter = createTRPCRouter({
updateChecker: updateCheckerRouter, updateChecker: updateCheckerRouter,
certificates: certificateRouter, certificates: certificateRouter,
info: infoRouter, info: infoRouter,
item: itemsRouter,
}); });
// export type definition of API // export type definition of API
@@ -17,6 +17,7 @@ export const throwIfActionForbiddenAsync = async (
ctx: { db: Database; session: Session | null }, ctx: { db: Database; session: Session | null },
boardWhere: SQL<unknown>, boardWhere: SQL<unknown>,
permission: BoardPermission, permission: BoardPermission,
error?: TRPCError,
) => { ) => {
const { db, session } = ctx; const { db, session } = ctx;
const groupsOfCurrentUser = await db.query.groupMembers.findMany({ const groupsOfCurrentUser = await db.query.groupMembers.findMany({
@@ -40,7 +41,7 @@ export const throwIfActionForbiddenAsync = async (
}); });
if (!board) { if (!board) {
notAllowed(); notAllowed(error);
} }
const { hasViewAccess, hasChangeAccess, hasFullAccess } = constructBoardPermissions(board, session); const { hasViewAccess, hasChangeAccess, hasFullAccess } = constructBoardPermissions(board, session);
@@ -57,14 +58,18 @@ export const throwIfActionForbiddenAsync = async (
return; // As view access is required and user has view access, allow return; // As view access is required and user has view access, allow
} }
notAllowed(); notAllowed(error);
}; };
/** /**
* This method returns NOT_FOUND to prevent snooping on board existence * This method returns NOT_FOUND to prevent snooping on board existence
* A function is used to use the method without return statement * A function is used to use the method without return statement
*/ */
function notAllowed(): never { function notAllowed(error?: TRPCError): never {
if (error) {
throw error;
}
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "Board not found", message: "Board not found",
+104 -19
View File
@@ -6,7 +6,8 @@ import type { Database } from "@homarr/db";
import { and, eq, handleTransactionsAsync, like, not } from "@homarr/db"; import { and, eq, handleTransactionsAsync, like, not } from "@homarr/db";
import { getMaxGroupPositionAsync } from "@homarr/db/queries"; import { getMaxGroupPositionAsync } from "@homarr/db/queries";
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema"; import { groupMembers, groupPermissions, groups } from "@homarr/db/schema";
import { everyoneGroup } from "@homarr/definitions"; import { selectGroupSchema, selectUserSchema } from "@homarr/db/validationSchemas";
import { everyoneGroup, groupPermissionKeys } from "@homarr/definitions";
import { byIdSchema, paginatedSchema } from "@homarr/validation/common"; import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
import { import {
groupCreateSchema, groupCreateSchema,
@@ -22,33 +23,65 @@ import { throwIfCredentialsDisabled } from "./invite/checks";
import { nextOnboardingStepAsync } from "./onboard/onboard-queries"; import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
export const groupRouter = createTRPCRouter({ export const groupRouter = createTRPCRouter({
getAll: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => { getAll: permissionRequiredProcedure
const dbGroups = await ctx.db.query.groups.findMany({ .requiresPermission("admin")
with: { .input(z.void())
members: { .output(
with: { z.array(
user: { selectGroupSchema.and(
columns: { z.object({ members: z.array(selectUserSchema.pick({ id: true, name: true, email: true, image: true })) }),
id: true, ),
name: true, ),
email: true, )
image: true, .meta({
openapi: { method: "GET", path: "/api/groups", tags: ["groups"], protect: true, summary: "Retrieve all groups" },
})
.query(async ({ ctx }) => {
const dbGroups = await ctx.db.query.groups.findMany({
with: {
members: {
with: {
user: {
columns: {
id: true,
name: true,
email: true,
image: true,
},
}, },
}, },
}, },
}, },
}, });
});
return dbGroups.map((group) => ({ return dbGroups.map((group) => ({
...group, ...group,
members: group.members.map((member) => member.user), members: group.members.map((member) => member.user),
})); }));
}), }),
getPaginated: permissionRequiredProcedure getPaginated: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(paginatedSchema) .input(paginatedSchema)
.output(
z.object({
items: z.array(
selectGroupSchema.and(
z.object({ members: z.array(selectUserSchema.pick({ id: true, name: true, email: true, image: true })) }),
),
),
totalCount: z.number(),
}),
)
.meta({
openapi: {
method: "GET",
path: "/api/groups/paginated",
tags: ["groups"],
protect: true,
summary: "Retrieve groups with pagination",
},
})
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined; const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined;
const groupCount = await ctx.db.$count(groups, whereQuery); const groupCount = await ctx.db.$count(groups, whereQuery);
@@ -84,6 +117,24 @@ export const groupRouter = createTRPCRouter({
getById: permissionRequiredProcedure getById: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(byIdSchema) .input(byIdSchema)
.output(
selectGroupSchema.and(
z.object({
owner: selectUserSchema.pick({ id: true, name: true, image: true, email: true }).nullable(),
members: z.array(selectUserSchema.pick({ id: true, name: true, email: true, image: true, provider: true })),
permissions: z.array(z.enum(groupPermissionKeys)),
}),
),
)
.meta({
openapi: {
method: "GET",
path: "/api/groups/{id}",
tags: ["groups"],
protect: true,
summary: "Retrieve group details",
},
})
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const group = await ctx.db.query.groups.findFirst({ const group = await ctx.db.query.groups.findFirst({
where: eq(groups.id, input.id), where: eq(groups.id, input.id),
@@ -201,6 +252,10 @@ export const groupRouter = createTRPCRouter({
createGroup: permissionRequiredProcedure createGroup: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(groupCreateSchema) .input(groupCreateSchema)
.output(z.string())
.meta({
openapi: { method: "POST", path: "/api/groups", tags: ["groups"], protect: true, summary: "Create group" },
})
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkSimilarNameAndThrowAsync(ctx.db, input.name); await checkSimilarNameAndThrowAsync(ctx.db, input.name);
@@ -219,6 +274,10 @@ export const groupRouter = createTRPCRouter({
updateGroup: permissionRequiredProcedure updateGroup: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(groupUpdateSchema) .input(groupUpdateSchema)
.output(z.void())
.meta({
openapi: { method: "PUT", path: "/api/groups/{id}", tags: ["groups"], protect: true, summary: "Update group" },
})
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.id); await throwIfGroupNotFoundAsync(ctx.db, input.id);
await throwIfGroupNameIsReservedAsync(ctx.db, input.id); await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
@@ -303,6 +362,10 @@ export const groupRouter = createTRPCRouter({
deleteGroup: permissionRequiredProcedure deleteGroup: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(byIdSchema) .input(byIdSchema)
.output(z.void())
.meta({
openapi: { method: "DELETE", path: "/api/groups/{id}", tags: ["groups"], protect: true, summary: "Delete group" },
})
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.id); await throwIfGroupNotFoundAsync(ctx.db, input.id);
await throwIfGroupNameIsReservedAsync(ctx.db, input.id); await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
@@ -312,6 +375,16 @@ export const groupRouter = createTRPCRouter({
addMember: permissionRequiredProcedure addMember: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(groupUserSchema) .input(groupUserSchema)
.output(z.void())
.meta({
openapi: {
method: "POST",
path: "/api/groups/{groupId}/members/{userId}",
tags: ["groups"],
protect: true,
summary: "Add member to group",
},
})
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.groupId); await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId); await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
@@ -328,6 +401,8 @@ export const groupRouter = createTRPCRouter({
}); });
} }
// TODO: Create another pr to only allow adding users with provider=credentials
await ctx.db.insert(groupMembers).values({ await ctx.db.insert(groupMembers).values({
groupId: input.groupId, groupId: input.groupId,
userId: input.userId, userId: input.userId,
@@ -336,6 +411,16 @@ export const groupRouter = createTRPCRouter({
removeMember: permissionRequiredProcedure removeMember: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(groupUserSchema) .input(groupUserSchema)
.output(z.void())
.meta({
openapi: {
method: "DELETE",
path: "/api/groups/{groupId}/members/{userId}",
tags: ["groups"],
protect: true,
summary: "Remove member from group",
},
})
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.groupId); await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId); await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
+366
View File
@@ -0,0 +1,366 @@
import { TRPCError } from "@trpc/server";
import SuperJSON from "superjson";
import z from "zod";
import { createId, objectEntries } from "@homarr/common";
import type { InferInsertModel } from "@homarr/db";
import { and, eq, handleTransactionsAsync } from "@homarr/db";
import { createDbInsertCollectionWithoutTransaction } from "@homarr/db/collection";
import { boards, itemLayouts, items } from "@homarr/db/schema";
import { selectItemLayoutSchema, selectitemSchema } from "@homarr/db/validationSchemas";
import type { WidgetKind } from "@homarr/definitions";
import { widgetKinds } from "@homarr/definitions";
import { byIdSchema } from "@homarr/validation/common";
import { zodEnumFromArray } from "@homarr/validation/enums";
import { itemAdvancedOptionsSchema } from "@homarr/validation/shared";
import { reduceWidgetOptionsWithDefaultValues, widgetImports } from "../../../../widgets/src";
import type { WidgetOptionDefinition } from "../../../../widgets/src/options";
import { convertIntersectionToZodObject } from "../../schema-merger";
import { createTRPCRouter, protectedProcedure } from "../../trpc";
import { throwIfActionForbiddenAsync } from "../board/board-access";
const createItemOptionsSchema = (kind: WidgetKind): z.ZodObject =>
z.object(
objectEntries(
widgetImports[kind].definition.createOptions({
enableStatusByDefault: true,
firstDayOfWeek: 1,
forceDisableStatus: false,
}),
).reduce(
(previous, [key, value]: [string, WidgetOptionDefinition]) => {
previous[key] = value.validate;
return previous;
},
{} as Record<string, z.ZodType>,
),
);
const outputItemSchema = z.union([
...widgetKinds.map((kind) =>
z
.object({
kind: z.literal(kind),
options: createItemOptionsSchema(kind),
advancedOptions: itemAdvancedOptionsSchema,
layouts: z.array(selectItemLayoutSchema.omit({ itemId: true })),
})
.and(
selectitemSchema.pick({
id: true,
boardId: true,
}),
),
),
]);
const manageItemSchema = z.object({
boardId: z.string(),
kind: zodEnumFromArray(widgetKinds),
options: z.record(z.string(), z.unknown().optional()),
advancedOptions: itemAdvancedOptionsSchema,
layouts: z.array(selectItemLayoutSchema.omit({ itemId: true })),
});
export const itemsRouter = createTRPCRouter({
getAll: protectedProcedure
.input(
z.object({
boardId: z.string(),
}),
)
.output(z.array(outputItemSchema))
.meta({
openapi: {
method: "GET",
path: "/api/boards/{boardId}/items",
tags: ["items"],
protect: true,
summary: "Retrieve all items for board",
},
})
.query(async ({ ctx, input }) => {
const board = await ctx.db.query.boards.findFirst({
where: (fields, { eq }) => eq(fields.id, input.boardId),
with: {
items: {
with: {
layouts: true,
},
},
},
});
const error = new TRPCError({
code: "NOT_FOUND",
message: "Board not found",
});
if (!board) throw error;
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.boardId), "view", error);
return board.items.map((item) => ({
...item,
options: reduceWidgetOptionsWithDefaultValues(
item.kind,
{
enableStatusByDefault: true,
firstDayOfWeek: 1,
forceDisableStatus: false,
},
SuperJSON.parse(item.options),
),
advancedOptions: itemAdvancedOptionsSchema.parse(SuperJSON.parse(item.advancedOptions)),
layouts: item.layouts.map(({ itemId: _, ...layout }) => layout),
}));
}),
createItem: protectedProcedure
.input(manageItemSchema)
.output(outputItemSchema)
.meta({
openapi: {
method: "POST",
path: "/api/items",
tags: ["items"],
protect: true,
summary: "Create item on board",
description: "Options available can be viewed in response options object. Input options are optional",
},
})
.mutation(async ({ ctx, input }) => {
const board = await ctx.db.query.boards.findFirst({
where: (fields, { eq }) => eq(fields.id, input.boardId),
});
const error = new TRPCError({
code: "NOT_FOUND",
message: "Board not found",
});
if (!board) throw error;
await throwIfActionForbiddenAsync(ctx, eq(boards.id, board.id), "modify", error);
const itemOptionsSchema = createItemOptionsSchema(input.kind);
const fullOptions = reduceWidgetOptionsWithDefaultValues(
input.kind,
{ enableStatusByDefault: true, firstDayOfWeek: 1, forceDisableStatus: false },
input.options,
);
const result = await itemOptionsSchema.safeParseAsync(fullOptions);
if (!result.success) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Failed to parse item options",
cause: result.error,
});
}
const item = {
id: createId(),
boardId: input.boardId,
kind: input.kind,
advancedOptions: input.advancedOptions,
options: result.data,
};
const itemInsert: InferInsertModel<typeof items> = {
...item,
advancedOptions: SuperJSON.stringify(item.advancedOptions),
options: SuperJSON.stringify(item.options),
};
const layoutInserts: InferInsertModel<typeof itemLayouts>[] = input.layouts.map((layout) => ({
...layout,
itemId: item.id,
}));
const insertCollection = createDbInsertCollectionWithoutTransaction(["items", "itemLayouts"]);
insertCollection.items.push(itemInsert);
insertCollection.itemLayouts.push(...layoutInserts);
await insertCollection.insertAllAsync(ctx.db);
return {
...item,
layouts: layoutInserts,
};
}),
getItem: protectedProcedure
.input(byIdSchema)
.output(outputItemSchema)
.meta({
openapi: {
method: "GET",
path: "/api/items/{id}",
tags: ["items"],
protect: true,
summary: "Retrieve item from a board",
},
})
.query(async ({ ctx, input }) => {
const item = await ctx.db.query.items.findFirst({
where: (fields, { eq }) => eq(fields.id, input.id),
with: {
layouts: true,
},
});
const error = new TRPCError({
code: "NOT_FOUND",
message: "Item not found",
});
if (!item) throw error;
await throwIfActionForbiddenAsync(ctx, eq(boards.id, item.boardId), "view", error);
return {
...item,
options: reduceWidgetOptionsWithDefaultValues(
item.kind,
{
enableStatusByDefault: true,
firstDayOfWeek: 1,
forceDisableStatus: false,
},
SuperJSON.parse(item.options),
),
advancedOptions: itemAdvancedOptionsSchema.parse(SuperJSON.parse(item.advancedOptions)),
layouts: item.layouts.map(({ itemId: _, ...layout }) => layout),
};
}),
updateItem: protectedProcedure
.input(convertIntersectionToZodObject(manageItemSchema.omit({ boardId: true, kind: true }).and(byIdSchema)))
.output(outputItemSchema)
.meta({
openapi: {
method: "PUT",
path: "/api/items/{id}",
tags: ["items"],
protect: true,
summary: "Update an item on the board",
},
})
.mutation(async ({ ctx, input }) => {
const item = await ctx.db.query.items.findFirst({
where: (fields, { eq }) => eq(fields.id, input.id),
with: {
layouts: true,
},
});
const error = new TRPCError({
code: "NOT_FOUND",
message: "Item not found",
});
if (!item) throw error;
await throwIfActionForbiddenAsync(ctx, eq(boards.id, item.boardId), "modify", error);
const previousOptions = SuperJSON.parse<Record<string, unknown>>(item.options);
const combinedOptions = {
...previousOptions,
...input.options,
};
const itemOptionsSchema = createItemOptionsSchema(item.kind);
const fullOptions = reduceWidgetOptionsWithDefaultValues(
item.kind,
{ enableStatusByDefault: true, firstDayOfWeek: 1, forceDisableStatus: false },
combinedOptions,
);
const optionsResult = await itemOptionsSchema.safeParseAsync(fullOptions);
if (!optionsResult.success) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Failed to parse item options",
cause: optionsResult.error,
});
}
await handleTransactionsAsync(ctx.db, {
async handleAsync(db, schema) {
await db.transaction(async (transaction) => {
await transaction
.update(schema.items)
.set({
advancedOptions: SuperJSON.stringify(input.advancedOptions),
options: SuperJSON.stringify(optionsResult),
})
.where(eq(schema.items.id, input.id));
for (const itemLayout of input.layouts) {
await transaction
.update(schema.itemLayouts)
.set({
height: itemLayout.height,
width: itemLayout.width,
sectionId: itemLayout.sectionId,
xOffset: itemLayout.xOffset,
yOffset: itemLayout.yOffset,
})
.where(
and(eq(schema.itemLayouts.itemId, input.id), eq(schema.itemLayouts.layoutId, itemLayout.layoutId)),
);
}
});
},
handleSync(db) {
db.transaction((transaction) => {
transaction
.update(items)
.set({
advancedOptions: SuperJSON.stringify(input.advancedOptions),
options: SuperJSON.stringify(optionsResult),
})
.where(eq(items.id, input.id));
for (const itemLayout of input.layouts) {
transaction
.update(itemLayouts)
.set({
height: itemLayout.height,
width: itemLayout.width,
sectionId: itemLayout.sectionId,
xOffset: itemLayout.xOffset,
yOffset: itemLayout.yOffset,
})
.where(and(eq(itemLayouts.itemId, input.id), eq(itemLayouts.layoutId, itemLayout.layoutId)));
}
});
},
});
return {
id: input.id,
kind: item.kind,
boardId: item.boardId,
advancedOptions: input.advancedOptions,
options: optionsResult,
layouts: input.layouts,
};
}),
deleteItem: protectedProcedure
.input(byIdSchema)
.output(z.void())
.meta({
openapi: {
method: "DELETE",
path: "/api/items/{id}",
tags: ["items"],
protect: true,
summary: "Delete an item from board",
},
})
.mutation(async ({ ctx, input }) => {
const item = await ctx.db.query.items.findFirst({
where: (fields, { eq }) => eq(fields.id, input.id),
});
const error = new TRPCError({
code: "NOT_FOUND",
message: "Item not found",
});
if (!item) throw error;
await throwIfActionForbiddenAsync(ctx, eq(boards.id, item.boardId), "modify", error);
await ctx.db.delete(items).where(eq(items.id, input.id));
}),
});
+2 -8
View File
@@ -1,7 +1,6 @@
import { z } from "zod/v4"; import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createTRPCRouter, publicProcedure } from "../trpc"; import { createTRPCRouter, publicProcedure } from "../trpc";
@@ -37,12 +36,7 @@ export const locationRouter = createTRPCRouter({
.input(locationSearchCityInput) .input(locationSearchCityInput)
.output(locationSearchCityOutput) .output(locationSearchCityOutput)
.query(async ({ input }) => { .query(async ({ input }) => {
const res = await withTimeoutAsync(async (signal) => { const res = await fetchWithTimeoutAsync(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`);
return await fetchWithTrustedCertificatesAsync(
`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`,
{ signal },
);
});
return (await res.json()) as z.infer<typeof locationSearchCityOutput>; return (await res.json()) as z.infer<typeof locationSearchCityOutput>;
}), }),
}); });
@@ -9,7 +9,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
export const healthMonitoringRouter = createTRPCRouter({ export const healthMonitoringRouter = createTRPCRouter({
getSystemHealthStatus: publicProcedure getSystemHealthStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "unraid", "mock")) .concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "mock"))
.query(async ({ ctx }) => { .query(async ({ ctx }) => {
return await Promise.all( return await Promise.all(
ctx.integrations.map(async (integration) => { ctx.integrations.map(async (integration) => {
@@ -26,7 +26,7 @@ export const healthMonitoringRouter = createTRPCRouter({
); );
}), }),
subscribeSystemHealthStatus: publicProcedure subscribeSystemHealthStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "unraid", "mock")) .concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "mock"))
.subscription(({ ctx }) => { .subscription(({ ctx }) => {
return observable<{ integrationId: string; healthInfo: SystemHealthMonitoring; timestamp: Date }>((emit) => { return observable<{ integrationId: string; healthInfo: SystemHealthMonitoring; timestamp: Date }>((emit) => {
const unsubscribes: (() => void)[] = []; const unsubscribes: (() => void)[] = [];
+3 -3
View File
@@ -32,12 +32,12 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"cookies": "^0.9.1", "cookies": "^0.9.1",
"ldapts": "8.0.35", "ldapts": "8.0.23",
"next": "16.1.1", "next": "16.0.10",
"next-auth": "5.0.0-beta.30", "next-auth": "5.0.0-beta.30",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+1 -1
View File
@@ -34,7 +34,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"esbuild": "^0.27.2", "esbuild": "^0.27.1",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
+2 -2
View File
@@ -29,12 +29,12 @@
"@homarr/core": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0",
"@paralleldrive/cuid2": "^3.1.0", "@paralleldrive/cuid2": "^3.1.0",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"next": "16.1.1", "next": "16.0.10",
"octokit": "^5.0.5", "octokit": "^5.0.5",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"undici": "7.16.0", "undici": "7.16.0",
"zod": "^4.2.1", "zod": "^4.1.13",
"zod-validation-error": "^5.0.0" "zod-validation-error": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
+3 -3
View File
@@ -37,16 +37,16 @@
}, },
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@t3-oss/env-nextjs": "^0.13.10", "@t3-oss/env-nextjs": "^0.13.8",
"better-sqlite3": "^12.5.0", "better-sqlite3": "^12.5.0",
"dns-caching": "^0.2.9", "dns-caching": "^0.2.9",
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.1",
"ioredis": "5.8.2", "ioredis": "5.8.2",
"mysql2": "3.16.0", "mysql2": "3.15.3",
"pg": "^8.16.3", "pg": "^8.16.3",
"superjson": "2.2.6", "superjson": "2.2.6",
"winston": "3.19.0", "winston": "3.19.0",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+1 -1
View File
@@ -2,7 +2,7 @@ import { createBooleanSchema, createEnv } from "../env";
export const dnsEnv = createEnv({ export const dnsEnv = createEnv({
server: { server: {
ENABLE_DNS_CACHING: createBooleanSchema(true), ENABLE_DNS_CACHING: createBooleanSchema(false),
}, },
experimental__runtimeEnv: process.env, experimental__runtimeEnv: process.env,
}); });
@@ -1,5 +1,5 @@
import type { Dispatcher } from "undici"; import type { Dispatcher } from "undici";
import { EnvHttpProxyAgent } from "undici"; import { Agent } from "undici";
import type { ILogger } from "@homarr/core/infrastructure/logs"; import type { ILogger } from "@homarr/core/infrastructure/logs";
import { createLogger } from "@homarr/core/infrastructure/logs"; import { createLogger } from "@homarr/core/infrastructure/logs";
@@ -7,15 +7,16 @@ import { createLogger } from "@homarr/core/infrastructure/logs";
// The below import statement initializes dns-caching // The below import statement initializes dns-caching
import "@homarr/core/infrastructure/dns/init"; import "@homarr/core/infrastructure/dns/init";
interface HttpAgentOptions extends EnvHttpProxyAgent.Options { interface HttpAgentOptions extends Agent.Options {
logger?: ILogger; logger?: ILogger;
} }
export class UndiciHttpAgent extends EnvHttpProxyAgent { export class UndiciHttpAgent extends Agent {
private logger: ILogger; private logger: ILogger;
constructor(props?: HttpAgentOptions) { constructor(props?: HttpAgentOptions) {
super(props); super(props);
this.logger = props?.logger ?? createLogger({ module: "httpAgent" }); this.logger = props?.logger ?? createLogger({ module: "httpAgent" });
} }
@@ -42,13 +42,12 @@ export const createCertificateAgentAsync = async (override?: {
}; };
export const createHttpsAgentAsync = async (override?: Pick<AgentOptions, "ca" | "checkServerIdentity">) => { export const createHttpsAgentAsync = async (override?: Pick<AgentOptions, "ca" | "checkServerIdentity">) => {
return new HttpsAgent({ return new HttpsAgent(
ca: await getAllTrustedCertificatesAsync(), override ?? {
checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()), ca: await getAllTrustedCertificatesAsync(),
// Override the ca and checkServerIdentity if provided checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()),
...override, },
proxyEnv: process.env, );
});
}; };
export const createAxiosCertificateInstanceAsync = async ( export const createAxiosCertificateInstanceAsync = async (
@@ -7,7 +7,7 @@ import { TestLogger } from "../logs";
vi.mock("undici", () => { vi.mock("undici", () => {
return { return {
EnvHttpProxyAgent: class EnvHttpProxyAgent { Agent: class Agent {
dispatch(_options: Dispatcher.DispatchOptions, _handler: Dispatcher.DispatchHandler): boolean { dispatch(_options: Dispatcher.DispatchOptions, _handler: Dispatcher.DispatchHandler): boolean {
return true; return true;
} }
+5 -5
View File
@@ -28,13 +28,13 @@
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0",
"@tanstack/react-query": "^5.90.14", "@tanstack/react-query": "^5.90.12",
"@trpc/client": "^11.8.1", "@trpc/client": "^11.8.0",
"@trpc/server": "^11.8.1", "@trpc/server": "^11.8.0",
"@trpc/tanstack-react-query": "^11.8.1", "@trpc/tanstack-react-query": "^11.8.0",
"node-cron": "^4.2.1", "node-cron": "^4.2.1",
"react": "19.2.3", "react": "19.2.3",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+4 -4
View File
@@ -49,14 +49,14 @@
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^8.3.10", "@mantine/core": "^8.3.10",
"@paralleldrive/cuid2": "^3.1.0", "@paralleldrive/cuid2": "^3.1.0",
"@testcontainers/mysql": "^11.11.0", "@testcontainers/mysql": "^11.10.0",
"@testcontainers/postgresql": "^11.11.0", "@testcontainers/postgresql": "^11.10.0",
"better-sqlite3": "^12.5.0", "better-sqlite3": "^12.5.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"drizzle-kit": "^0.31.8", "drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3", "drizzle-zod": "^0.8.3",
"mysql2": "3.16.0", "mysql2": "3.15.3",
"pg": "^8.16.3", "pg": "^8.16.3",
"superjson": "2.2.6" "superjson": "2.2.6"
}, },
@@ -67,7 +67,7 @@
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
"@types/pg": "^8.16.0", "@types/pg": "^8.16.0",
"dotenv-cli": "^11.0.0", "dotenv-cli": "^11.0.0",
"esbuild": "^0.27.2", "esbuild": "^0.27.1",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"tsx": "4.20.4", "tsx": "4.20.4",
+5 -2
View File
@@ -1,9 +1,12 @@
import { createSelectSchema } from "drizzle-zod"; import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { apps, boards, groups, invites, searchEngines, serverSettings, users } from "./schema"; import { apps, boards, groups, invites, itemLayouts, items, searchEngines, serverSettings, users } from "./schema";
export const selectAppSchema = createSelectSchema(apps); export const selectAppSchema = createSelectSchema(apps);
export const selectBoardSchema = createSelectSchema(boards); export const selectBoardSchema = createSelectSchema(boards);
export const selectitemSchema = createSelectSchema(items);
export const insertItemSchema = createInsertSchema(items);
export const selectItemLayoutSchema = createSelectSchema(itemLayouts);
export const selectGroupSchema = createSelectSchema(groups); export const selectGroupSchema = createSelectSchema(groups);
export const selectInviteSchema = createSelectSchema(invites); export const selectInviteSchema = createSelectSchema(invites);
export const selectSearchEnginesSchema = createSelectSchema(searchEngines); export const selectSearchEnginesSchema = createSelectSchema(searchEngines);
+1 -1
View File
@@ -25,7 +25,7 @@
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "^5.3.3", "fast-xml-parser": "^5.3.3",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
-7
View File
@@ -298,13 +298,6 @@ export const integrationDefs = {
category: ["healthMonitoring"], category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/truenas"), documentationUrl: createDocumentationLink("/docs/integrations/truenas"),
}, },
unraid: {
name: "Unraid",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/unraid.svg",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/unraid"),
},
// This integration only returns mock data, it is used during development (but can also be used in production by directly going to the create page) // This integration only returns mock data, it is used during development (but can also be used in production by directly going to the create page)
mock: { mock: {
name: "Mock", name: "Mock",
+1 -1
View File
@@ -28,7 +28,7 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^8.3.10", "@mantine/form": "^8.3.10",
"mantine-form-zod-resolver": "^1.3.0", "mantine-form-zod-resolver": "^1.3.0",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.3.10", "@mantine/core": "^8.3.10",
"react": "19.2.3", "react": "19.2.3",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
@@ -1,7 +1,6 @@
import { parse } from "path"; import { parse } from "path";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import type { IconRepositoryLicense } from "../types/icon-repository-license"; import type { IconRepositoryLicense } from "../types/icon-repository-license";
import type { RepositoryIconGroup } from "../types/repository-icon-group"; import type { RepositoryIconGroup } from "../types/repository-icon-group";
@@ -20,12 +19,11 @@ export class GitHubIconRepository extends IconRepository {
} }
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> { protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
const url = this.repositoryIndexingUrl; if (!this.repositoryIndexingUrl || !this.repositoryBlobUrlTemplate) {
if (!url || !this.repositoryBlobUrlTemplate) {
throw new Error("Repository URLs are required for this repository"); throw new Error("Repository URLs are required for this repository");
} }
const response = await withTimeoutAsync(async (signal) => fetchWithTrustedCertificatesAsync(url, { signal })); const response = await fetchWithTimeoutAsync(this.repositoryIndexingUrl);
const listOfFiles = (await response.json()) as GitHubApiResponse; const listOfFiles = (await response.json()) as GitHubApiResponse;
return { return {
@@ -1,7 +1,6 @@
import { parse } from "path"; import { parse } from "path";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import type { IconRepositoryLicense } from "../types/icon-repository-license"; import type { IconRepositoryLicense } from "../types/icon-repository-license";
import type { RepositoryIconGroup } from "../types/repository-icon-group"; import type { RepositoryIconGroup } from "../types/repository-icon-group";
@@ -20,9 +19,7 @@ export class JsdelivrIconRepository extends IconRepository {
} }
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> { protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
const response = await withTimeoutAsync(async (signal) => const response = await fetchWithTimeoutAsync(this.repositoryIndexingUrl);
fetchWithTrustedCertificatesAsync(this.repositoryIndexingUrl, { signal }),
);
const listOfFiles = (await response.json()) as JsdelivrApiResponse; const listOfFiles = (await response.json()) as JsdelivrApiResponse;
return { return {
+1 -1
View File
@@ -48,7 +48,7 @@
"tsdav": "^2.1.6", "tsdav": "^2.1.6",
"undici": "7.16.0", "undici": "7.16.0",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
@@ -38,7 +38,6 @@ import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
import { QuayIntegration } from "../quay/quay-integration"; import { QuayIntegration } from "../quay/quay-integration";
import { TrueNasIntegration } from "../truenas/truenas-integration"; import { TrueNasIntegration } from "../truenas/truenas-integration";
import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration"; import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration";
import { UnraidIntegration } from "../unraid/unraid-integration";
import type { Integration, IntegrationInput } from "./integration"; import type { Integration, IntegrationInput } from "./integration";
export const createIntegrationAsync = async <TKind extends keyof typeof integrationCreators>( export const createIntegrationAsync = async <TKind extends keyof typeof integrationCreators>(
@@ -102,7 +101,6 @@ export const integrationCreators = {
ntfy: NTFYIntegration, ntfy: NTFYIntegration,
mock: MockIntegration, mock: MockIntegration,
truenas: TrueNasIntegration, truenas: TrueNasIntegration,
unraid: UnraidIntegration,
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>; } satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = { type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
-1
View File
@@ -22,7 +22,6 @@ export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6";
export { PlexIntegration } from "./plex/plex-integration"; export { PlexIntegration } from "./plex/plex-integration";
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration"; export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
export { TrueNasIntegration } from "./truenas/truenas-integration"; export { TrueNasIntegration } from "./truenas/truenas-integration";
export { UnraidIntegration } from "./unraid/unraid-integration";
export { OPNsenseIntegration } from "./opnsense/opnsense-integration"; export { OPNsenseIntegration } from "./opnsense/opnsense-integration";
export { ICalIntegration } from "./ical/ical-integration"; export { ICalIntegration } from "./ical/ical-integration";
@@ -1,189 +0,0 @@
import dayjs from "dayjs";
import type { fetch as undiciFetch } from "undici/types/fetch";
import { humanFileSize } from "@homarr/common";
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { HandleIntegrationErrors } from "../base/errors/decorator";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-monitoring/health-monitoring-integration";
import type { SystemHealthMonitoring } from "../interfaces/health-monitoring/health-monitoring-types";
import type { UnraidSystemInfo } from "./unraid-types";
import { unraidSystemInfoSchema } from "./unraid-types";
const logger = createLogger({ module: "UnraidIntegration" });
@HandleIntegrationErrors([])
export class UnraidIntegration extends Integration implements ISystemHealthMonitoringIntegration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
await this.queryGraphQLAsync<{ info: UnraidSystemInfo }>(
`
query {
info {
os { platform }
}
}
`,
input.fetchAsync,
);
return { success: true };
}
public async getSystemInfoAsync(): Promise<SystemHealthMonitoring> {
const systemInfo = await this.getSystemInformationAsync();
const cpuUtilization = systemInfo.metrics.cpu.cpus.reduce((acc, val) => acc + val.percentTotal, 0);
const cpuCount = systemInfo.info.cpu.cores;
// We use "info" object instead of the stats since this is the exact amount the kernel sees, which is what Unraid displays.
const totalMemory = systemInfo.info.memory.layout.reduce((acc, layout) => layout.size + acc, 0);
const usedMemory = totalMemory * (systemInfo.metrics.memory.percentTotal / 100);
const uptime = dayjs(systemInfo.info.os.uptime);
return {
version: systemInfo.info.os.release,
cpuModelName: systemInfo.info.cpu.brand,
cpuUtilization: cpuUtilization / cpuCount,
memUsedInBytes: usedMemory,
memAvailableInBytes: totalMemory - usedMemory,
uptime: dayjs().diff(uptime, "seconds"),
network: null, // Not implemented, see https://github.com/unraid/api/issues/1602
loadAverage: null,
rebootRequired: false,
availablePkgUpdates: 0,
cpuTemp: undefined, // Not implemented, see https://github.com/unraid/api/issues/1597
fileSystem: systemInfo.array.disks.map((disk) => ({
deviceName: disk.name,
used: humanFileSize(disk.fsUsed * 1024), // API is in KiB (kibibytes), covert to bytes
available: `${disk.size * 1024}`, // API is in KiB (kibibytes), covert to bytes
percentage: (disk.fsUsed / disk.size) * 100, // The units are the same, therefore the actual unit is irrelevant
})),
smart: systemInfo.array.disks.map((disk) => ({
deviceName: disk.name,
temperature: disk.temp,
overallStatus: disk.status,
})),
};
}
private async getSystemInformationAsync(): Promise<UnraidSystemInfo> {
logger.debug("Retrieving system information", {
url: this.url("/graphql"),
});
const query = `
query {
metrics {
cpu {
percentTotal
cpus {
percentTotal
}
},
memory {
available
used
free
total
swapFree
swapTotal
swapUsed
percentTotal
}
}
array {
state
capacity {
disks {
free
total
used
}
}
disks {
name
size
fsFree
fsUsed
status
temp
}
}
info {
devices {
network {
speed
dhcp
model
model
}
}
os {
platform,
distro,
release,
uptime
},
cpu {
manufacturer,
brand,
cores,
threads
},
memory {
layout {
size
}
}
}
}
`;
const response = await this.queryGraphQLAsync<UnraidSystemInfo>(query);
const result = await unraidSystemInfoSchema.parseAsync(response);
logger.debug("Retrieved system information", {
url: this.url("/graphql"),
});
return result;
}
private async queryGraphQLAsync<T>(
query: string,
fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync,
): Promise<T> {
const url = this.url("/graphql");
const apiKey = this.getSecretValue("apiKey");
logger.debug("Sending GraphQL query", {
url: url.toString(),
});
const response = await fetchAsync(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({ query }),
});
if (!response.ok) {
throw new ResponseError(response);
}
const json = (await response.json()) as { data: T; errors?: { message: string }[] };
if (json.errors) {
throw new Error(`GraphQL errors: ${json.errors.map((error) => error.message).join(", ")}`);
}
return json.data;
}
}
@@ -1,73 +0,0 @@
import z from "zod";
export const unraidSystemInfoSchema = z.object({
metrics: z.object({
cpu: z.object({
percentTotal: z.number(),
cpus: z.array(
z.object({
percentTotal: z.number(),
}),
),
}),
memory: z.object({
available: z.number(),
used: z.number(),
free: z.number(),
total: z.number().min(0),
percentTotal: z.number().min(0).max(100),
}),
}),
array: z.object({
state: z.string(),
capacity: z.object({
disks: z.object({
free: z.coerce.number(),
total: z.coerce.number(),
used: z.coerce.number(),
}),
}),
disks: z.array(
z.object({
name: z.string(),
size: z.number(),
fsFree: z.number(),
fsUsed: z.number(),
status: z.string(),
temp: z.number(),
}),
),
}),
info: z.object({
devices: z.object({
network: z.array(
z.object({
speed: z.number(),
dhcp: z.boolean(),
model: z.string(),
}),
),
}),
os: z.object({
platform: z.string(),
distro: z.string(),
release: z.string(),
uptime: z.coerce.date(),
}),
cpu: z.object({
manufacturer: z.string(),
brand: z.string(),
cores: z.number(),
threads: z.number(),
}),
memory: z.object({
layout: z.array(
z.object({
size: z.number(),
}),
),
}),
}),
});
export type UnraidSystemInfo = z.infer<typeof unraidSystemInfoSchema>;
+2 -2
View File
@@ -36,10 +36,10 @@
"@mantine/core": "^8.3.10", "@mantine/core": "^8.3.10",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"next": "16.1.1", "next": "16.0.10",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+2 -2
View File
@@ -40,11 +40,11 @@
"@mantine/core": "^8.3.10", "@mantine/core": "^8.3.10",
"@mantine/hooks": "^8.3.10", "@mantine/hooks": "^8.3.10",
"adm-zip": "0.5.16", "adm-zip": "0.5.16",
"next": "16.1.1", "next": "16.0.10",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"superjson": "2.2.6", "superjson": "2.2.6",
"zod": "^4.2.1", "zod": "^4.1.13",
"zod-form-data": "^3.0.1" "zod-form-data": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {
+1 -1
View File
@@ -23,7 +23,7 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+1 -1
View File
@@ -34,7 +34,7 @@
"octokit": "^5.0.5", "octokit": "^5.0.5",
"superjson": "2.2.6", "superjson": "2.2.6",
"undici": "7.16.0", "undici": "7.16.0",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+20 -47
View File
@@ -12,7 +12,9 @@ export const dockerContainersRequestHandler = createCachedWidgetRequestHandler({
queryKey: "dockerContainersResult", queryKey: "dockerContainersResult",
widgetKind: "dockerContainers", widgetKind: "dockerContainers",
async requestAsync() { async requestAsync() {
return await getContainersWithStatsAsync(); const containers = await getContainersWithStatsAsync();
return containers;
}, },
cacheDuration: dayjs.duration(20, "seconds"), cacheDuration: dayjs.duration(20, "seconds"),
}); });
@@ -26,7 +28,7 @@ async function getContainersWithStatsAsync() {
dockerInstances.map(async ({ instance, host }) => { dockerInstances.map(async ({ instance, host }) => {
const instanceContainers = await instance.listContainers({ all: true }); const instanceContainers = await instance.listContainers({ all: true });
return instanceContainers return instanceContainers
.filter((container) => !(dockerLabels.hide in container.Labels)) .filter((container) => dockerLabels.hide in container.Labels === false)
.map((container) => ({ ...container, instance: host })); .map((container) => ({ ...container, instance: host }));
}), }),
).then((res) => res.flat()); ).then((res) => res.flat());
@@ -43,21 +45,7 @@ async function getContainersWithStatsAsync() {
const instance = dockerInstances.find(({ host }) => host === container.instance)?.instance; const instance = dockerInstances.find(({ host }) => host === container.instance)?.instance;
if (!instance) return null; if (!instance) return null;
// Get stats, falling back to an empty stats object if fetch fails const stats = await instance.getContainer(container.Id).stats({ stream: false, "one-shot": true });
// calculateCpuUsage and calculateMemoryUsage will return 0 for invalid/missing stats
const stats = await instance
.getContainer(container.Id)
.stats({ stream: false, "one-shot": true })
.catch(
() =>
({
cpu_stats: { online_cpus: 0, cpu_usage: { total_usage: 0 }, system_cpu_usage: 0 },
memory_stats: { usage: 0 },
}) as ContainerStats,
);
const cpuUsage = calculateCpuUsage(stats);
const memoryUsage = calculateMemoryUsage(stats);
return { return {
id: container.Id, id: container.Id,
@@ -69,8 +57,19 @@ async function getContainersWithStatsAsync() {
if (!extractedImage) return false; if (!extractedImage) return false;
return icon.name.toLowerCase().includes(extractedImage.toLowerCase()); return icon.name.toLowerCase().includes(extractedImage.toLowerCase());
})?.url ?? null, })?.url ?? null,
cpuUsage, cpuUsage: calculateCpuUsage(stats),
memoryUsage, // memory usage by default includes cache, which should not be shown as it is also not shown with docker stats command
// The below type is probably wrong, sometimes stats can be undefined
// See https://docs.docker.com/reference/cli/docker/container/stats/ how it is / was calculated
memoryUsage:
stats.memory_stats.usage -
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
(stats.memory_stats.stats?.cache ??
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
stats.memory_stats.stats?.total_inactive_file ??
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
stats.memory_stats.stats?.inactive_file ??
0),
image: container.Image, image: container.Image,
ports: container.Ports, ports: container.Ports,
}; };
@@ -80,36 +79,10 @@ async function getContainersWithStatsAsync() {
} }
function calculateCpuUsage(stats: ContainerStats): number { function calculateCpuUsage(stats: ContainerStats): number {
// Handle containers with missing or invalid stats (e.g., exited, dead containers) const numberOfCpus = stats.cpu_stats.online_cpus || 1;
if (!stats.cpu_stats.online_cpus || stats.cpu_stats.online_cpus === 0 || !stats.cpu_stats.cpu_usage.total_usage) {
return 0;
}
const numberOfCpus = stats.cpu_stats.online_cpus;
const usage = stats.cpu_stats.system_cpu_usage; const usage = stats.cpu_stats.system_cpu_usage;
if (!usage || usage === 0) { if (usage === 0) return 0;
return 0;
}
return (stats.cpu_stats.cpu_usage.total_usage / usage) * numberOfCpus * 100; return (stats.cpu_stats.cpu_usage.total_usage / usage) * numberOfCpus * 100;
} }
function calculateMemoryUsage(stats: ContainerStats): number {
// Handle containers with missing or invalid stats (e.g., exited, dead containers)
if (!stats.memory_stats.usage) {
return 0;
}
// memory usage by default includes cache, which should not be shown as it is also not shown with docker stats command
// See https://docs.docker.com/reference/cli/docker/container/stats/ how it is / was calculated
return (
stats.memory_stats.usage -
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
(stats.memory_stats.stats?.cache ??
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
stats.memory_stats.stats?.total_inactive_file ??
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
stats.memory_stats.stats?.inactive_file ??
0)
);
}
@@ -57,12 +57,7 @@ export const createRequestIntegrationJobHandler = <
reduceWidgetOptionsWithDefaultValues( reduceWidgetOptionsWithDefaultValues(
itemForIntegration.kind, itemForIntegration.kind,
{ {
defaultSearchEngineId: serverSettings.search.defaultSearchEngineId,
openSearchInNewTab: true,
firstDayOfWeek: 1, firstDayOfWeek: 1,
homeBoardId: serverSettings.board.homeBoardId,
mobileHomeBoardId: serverSettings.board.mobileHomeBoardId,
pingIconsEnabled: true,
enableStatusByDefault: serverSettings.board.enableStatusByDefault, enableStatusByDefault: serverSettings.board.enableStatusByDefault,
forceDisableStatus: serverSettings.board.forceDisableStatus, forceDisableStatus: serverSettings.board.forceDisableStatus,
}, },
@@ -1,8 +1,7 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
@@ -12,9 +11,7 @@ export const minecraftServerStatusRequestHandler = createCachedWidgetRequestHand
async requestAsync(input: { domain: string; isBedrockServer: boolean }) { async requestAsync(input: { domain: string; isBedrockServer: boolean }) {
const path = `${input.isBedrockServer ? "/bedrock" : ""}/3/${input.domain}`; const path = `${input.isBedrockServer ? "/bedrock" : ""}/3/${input.domain}`;
const response = await withTimeoutAsync(async (signal) => const response = await fetchWithTimeoutAsync(`https://api.mcsrvstat.us${path}`);
fetchWithTrustedCertificatesAsync(`https://api.mcsrvstat.us${path}`, { signal }),
);
return responseSchema.parse(await response.json()); return responseSchema.parse(await response.json());
}, },
cacheDuration: dayjs.duration(5, "minutes"), cacheDuration: dayjs.duration(5, "minutes"),
+4 -8
View File
@@ -1,8 +1,7 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
@@ -10,12 +9,9 @@ export const fetchStockPriceHandler = createCachedWidgetRequestHandler({
queryKey: "fetchStockPriceResult", queryKey: "fetchStockPriceResult",
widgetKind: "stockPrice", widgetKind: "stockPrice",
async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) { async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) {
const response = await withTimeoutAsync(async (signal) => { const response = await fetchWithTimeoutAsync(
return await fetchWithTrustedCertificatesAsync( `https://query1.finance.yahoo.com/v8/finance/chart/${input.stock}?range=${input.timeRange}&interval=${input.timeInterval}`,
`https://query1.finance.yahoo.com/v8/finance/chart/${input.stock}?range=${input.timeRange}&interval=${input.timeInterval}`, );
{ signal },
);
});
const data = dataSchema.parse(await response.json()); const data = dataSchema.parse(await response.json());
if ("error" in data) { if ("error" in data) {
@@ -3,7 +3,7 @@ import { Octokit } from "octokit";
import { compareSemVer, isValidSemVer } from "semver-parser"; import { compareSemVer, isValidSemVer } from "semver-parser";
import { env } from "@homarr/common/env"; import { env } from "@homarr/common/env";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createLogger } from "@homarr/core/infrastructure/logs"; import { createLogger } from "@homarr/core/infrastructure/logs";
import { createChannelWithLatestAndEvents } from "@homarr/redis"; import { createChannelWithLatestAndEvents } from "@homarr/redis";
import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler"; import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler";
@@ -23,7 +23,7 @@ export const updateCheckerRequestHandler = createCachedRequestHandler({
const octokit = new Octokit({ const octokit = new Octokit({
request: { request: {
fetch: fetchWithTrustedCertificatesAsync, fetch: fetchWithTimeoutAsync,
}, },
}); });
const releases = await octokit.rest.repos.listReleases({ const releases = await octokit.rest.repos.listReleases({
+4 -8
View File
@@ -1,8 +1,7 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { z } from "zod"; import { z } from "zod";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
@@ -10,12 +9,9 @@ export const weatherRequestHandler = createCachedWidgetRequestHandler({
queryKey: "weatherAtLocation", queryKey: "weatherAtLocation",
widgetKind: "weather", widgetKind: "weather",
async requestAsync(input: { latitude: number; longitude: number }) { async requestAsync(input: { latitude: number; longitude: number }) {
const res = await withTimeoutAsync(async (signal) => { const res = await fetchWithTimeoutAsync(
return await fetchWithTrustedCertificatesAsync( `https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max&current_weather=true&timezone=auto`,
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max&current_weather=true&timezone=auto`, );
{ signal },
);
});
const json: unknown = await res.json(); const json: unknown = await res.json();
const weather = await atLocationOutput.parseAsync(json); const weather = await atLocationOutput.parseAsync(json);
return { return {
+1 -1
View File
@@ -27,7 +27,7 @@
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@mantine/dates": "^8.3.10", "@mantine/dates": "^8.3.10",
"next": "16.1.1", "next": "16.0.10",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3"
}, },
+2 -2
View File
@@ -37,8 +37,8 @@
"@mantine/hooks": "^8.3.10", "@mantine/hooks": "^8.3.10",
"@mantine/spotlight": "^8.3.10", "@mantine/spotlight": "^8.3.10",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"jotai": "^2.16.1", "jotai": "^2.16.0",
"next": "16.1.1", "next": "16.0.10",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"use-deep-compare-effect": "^1.8.1" "use-deep-compare-effect": "^1.8.1"
+2 -2
View File
@@ -32,8 +32,8 @@
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"deepmerge": "4.3.1", "deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.1.1", "next": "16.0.10",
"next-intl": "4.6.1", "next-intl": "4.6.0",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3" "react-dom": "19.2.3"
}, },
+13 -13
View File
@@ -312,7 +312,7 @@
}, },
"mobileBoard": { "mobileBoard": {
"label": "Mobiles Board", "label": "Mobiles Board",
"description": "Nur Boards, welche für die Gruppe zugänglich sind, können ausgewählt werden" "description": "Nur Boards welche für die Gruppe zugänglich sind, können ausgewählt werden"
} }
}, },
"permission": { "permission": {
@@ -355,7 +355,7 @@
}, },
"view-all": { "view-all": {
"label": "Alle Boards anzeigen", "label": "Alle Boards anzeigen",
"description": "Mitgliedern das Ansehen von Boards erlauben" "description": "Mitgliedern das Ansehen von allen Boards erlauben"
}, },
"modify-all": { "modify-all": {
"label": "Alle Boards ändern", "label": "Alle Boards ändern",
@@ -743,7 +743,7 @@
}, },
"authorization": { "authorization": {
"title": "Autorisierungsfehler", "title": "Autorisierungsfehler",
"description": "Die Anfrage wurde nicht autorisiert. Bitte überprüfen Sie, ob die Anmeldedaten korrekt sind und Sie diese mit den entsprechenden Berechtigungen konfiguriert haben." "description": "Die Anfrage wurde nicht autorisiert. Bitte überprüfen Sie, ob die Anmeldedaten korrekt sind und Sie diese mit mit den entsprechenden Berechtigungen konfiguriert haben."
}, },
"statusCode": { "statusCode": {
"title": "Antwort-Fehler", "title": "Antwort-Fehler",
@@ -964,7 +964,7 @@
}, },
"personalAccessToken": { "personalAccessToken": {
"label": "Persönlicher Zugangs-Token", "label": "Persönlicher Zugangs-Token",
"newLabel": "Neuer persönlicher Zugangs-Token" "newLabel": "Neuer Persönliches Zugangs-Token"
}, },
"topic": { "topic": {
"label": "Thema", "label": "Thema",
@@ -1237,7 +1237,7 @@
}, },
"openAllInNewTabs": { "openAllInNewTabs": {
"title": "Alle in tabs öffnen", "title": "Alle in tabs öffnen",
"text": "Einige Browser können aus Sicherheitsgründen das gleichzeitige Öffnen von mehreren Tabs blockieren. Homarr konnte nicht alle Fenster öffnen, weil Ihr Browser diese Aktion blockiert hat. Bitte erlauben Sie \"Pop-up Fenster öffnen\" und versuchen Sie es erneut." "text": "Einige Browser können aus Sicherheitsgründen das Öffnen von mehreren Tabs aufeinmal blockieren. Homarr konnte nicht alle Fenster öffnen, weil Ihr Browser diese Aktion blockiert hat. Bitte erlauben Sie \"Pop-up Fenster öffnen\" und versuchen Sie es erneut."
} }
} }
}, },
@@ -1949,7 +1949,7 @@
"name": "Name", "name": "Name",
"state": "Status", "state": "Status",
"cpuUsage": "CPU-Auslastung", "cpuUsage": "CPU-Auslastung",
"memoryUsage": "Speichernutzung" "memoryUsage": "Speicherverbrauch"
} }
}, },
"descendingDefaultSort": { "descendingDefaultSort": {
@@ -2639,7 +2639,7 @@
"notification": { "notification": {
"success": { "success": {
"title": "Änderungen erfolgreich angewendet", "title": "Änderungen erfolgreich angewendet",
"message": "Das Board wurde erfolgreich gespeichert" "message": "Das Board wurde erfolgreich dupliziert"
}, },
"error": { "error": {
"title": "Änderungen konnten nicht angewendet werden", "title": "Änderungen konnten nicht angewendet werden",
@@ -2676,7 +2676,7 @@
}, },
"onlyImportApps": { "onlyImportApps": {
"label": "Nur Apps importieren", "label": "Nur Apps importieren",
"description": "Fügt nur die Apps hinzu. Das Board selbst muss manuell neu erstellt werden" "description": "Fügt nur die Apps hinzu, das Board muss manuell neu erstellt werden"
} }
}, },
"name": { "name": {
@@ -2926,7 +2926,7 @@
"error": { "error": {
"noBoard": { "noBoard": {
"title": "Willkommen bei Homarr", "title": "Willkommen bei Homarr",
"description": "Ein schlankes, modernes Dashboard, das alle Ihre Apps und Dienste bereitstellt.", "description": "Ein schlankens, modernes Dashboard, das alle Ihre Apps und Dienste bereit stellt.",
"link": "Erstellen Sie Ihr erstes Board", "link": "Erstellen Sie Ihr erstes Board",
"notice": "Um diese Seite verschwinden zu lassen, erstellen Sie ein Board und setzen es als Home Board" "notice": "Um diese Seite verschwinden zu lassen, erstellen Sie ein Board und setzen es als Home Board"
}, },
@@ -3366,7 +3366,7 @@
"label": "Index Manager" "label": "Index Manager"
}, },
"healthMonitoring": { "healthMonitoring": {
"label": "Überwachung des Zustandes" "label": "Überwachung des Gesundheit"
}, },
"dnsHole": { "dnsHole": {
"label": "DNS Hole Daten" "label": "DNS Hole Daten"
@@ -3512,7 +3512,7 @@
}, },
"about": { "about": {
"version": "Version {version}", "version": "Version {version}",
"text": "Homarr ist ein gemeinschaftsorientiertes Open-Source-Projekt, das von Freiwilligen betreut wird. Dank dieser Menschen ist Homarr seit 2021 ein wachsendes Projekt. Unser Team, dessen Mitglieder in vielen verschiedenen Ländern ansässig sind, arbeitet in seiner Freizeit und ohne Vergütung.", "text": "Homarr ist ein gemeinschaftsorientiertes Open-Source Projekt, das von Freiwilligen betreut wird. Dank dieser Menschen ist Homarr seit 2021 ein wachsendes Projekt. Unser Team, sitzhaft mit ihren Migliedern in vielen verschiedenen Ländern, arbeitet in ihrer Freizeit ohne Vergütung.",
"accordion": { "accordion": {
"contributors": { "contributors": {
"title": "Mitwirkende", "title": "Mitwirkende",
@@ -4395,7 +4395,7 @@
"configControl": "Konfiguration", "configControl": "Konfiguration",
"searchEngineType": { "searchEngineType": {
"generic": "Allgemein", "generic": "Allgemein",
"fromIntegration": "Aus der Integration" "fromIntegration": "Aus den Integration"
} }
}, },
"delete": { "delete": {
@@ -4498,7 +4498,7 @@
"notification": { "notification": {
"success": { "success": {
"title": "Zertifikat entfernt", "title": "Zertifikat entfernt",
"message": "Das Zertifikat wurde erfolgreich entfernt" "message": "Das Zertifikat wurde erfolgreich entfern"
}, },
"error": { "error": {
"title": "Zertifikat nicht entfernt", "title": "Zertifikat nicht entfernt",
+7 -7
View File
@@ -1941,19 +1941,19 @@
"description": "Estadísticas de tus contenedores (Este widget sólo puede ser añadido con privilegios de administrador)", "description": "Estadísticas de tus contenedores (Este widget sólo puede ser añadido con privilegios de administrador)",
"option": { "option": {
"enableRowSorting": { "enableRowSorting": {
"label": "Habilitar ordenar elementos" "label": ""
}, },
"defaultSort": { "defaultSort": {
"label": "Columna usada para ordenar por defecto", "label": "",
"option": { "option": {
"name": "Nombre", "name": "",
"state": "Estado", "state": "",
"cpuUsage": "Uso de CPU", "cpuUsage": "",
"memoryUsage": "Uso de memoria" "memoryUsage": ""
} }
}, },
"descendingDefaultSort": { "descendingDefaultSort": {
"label": "Invertir orden" "label": ""
} }
}, },
"error": { "error": {
+114 -114
View File
@@ -7,7 +7,7 @@
"description": "Pour commencer, veuillez selectionner comment vous souhaitez configurer votre instance Homarr.", "description": "Pour commencer, veuillez selectionner comment vous souhaitez configurer votre instance Homarr.",
"action": { "action": {
"scratch": "Partir de zéro", "scratch": "Partir de zéro",
"importOldmarr": "Importer depuis Homarr avant la 1.0" "importOldmarr": "Importer à partir d'Homarr avant la 1.0"
} }
}, },
"import": { "import": {
@@ -27,7 +27,7 @@
"description": "Configurer le comportement d'importation" "description": "Configurer le comportement d'importation"
}, },
"boardSelection": { "boardSelection": {
"title": "Tableaux {count} trouvés", "title": "{count} tableaux trouvés",
"description": "Choisissez tous les tableaux avec la taille que vous souhaitez importer", "description": "Choisissez tous les tableaux avec la taille que vous souhaitez importer",
"action": { "action": {
"selectAll": "Tout sélectionner", "selectAll": "Tout sélectionner",
@@ -113,7 +113,7 @@
"subtitle": "Bon retour parmi nous ! Veuillez entrer vos identifiants" "subtitle": "Bon retour parmi nous ! Veuillez entrer vos identifiants"
}, },
"invite": { "invite": {
"title": "Rejoignez Homarr", "title": "Rejoindre Homarr",
"subtitle": "Bienvenue sur Homarr! Veuillez créer votre compte", "subtitle": "Bienvenue sur Homarr! Veuillez créer votre compte",
"description": "Vous avez été invité par {username}" "description": "Vous avez été invité par {username}"
}, },
@@ -649,14 +649,14 @@
"app": { "app": {
"option": { "option": {
"existing": { "existing": {
"title": "Existant", "title": "",
"label": "Sélectionner une application existante" "label": ""
}, },
"new": { "new": {
"title": "Nouveau", "title": "",
"url": { "url": {
"label": "Lien de l'application", "label": "",
"description": "Lien d'ouverture de l'application depuis le tableau de bord" "description": ""
} }
} }
} }
@@ -676,9 +676,9 @@
}, },
"app": { "app": {
"action": { "action": {
"add": "Lier une application", "add": "",
"remove": "Délier", "remove": "",
"select": "Sélectionnez une application à lier" "select": ""
} }
} }
}, },
@@ -709,7 +709,7 @@
"description": "L'intégration \"{kind}\" peut être utilisée avec les moteurs de recherche. Cochez ceci pour configurer automatiquement le moteur de recherche." "description": "L'intégration \"{kind}\" peut être utilisée avec les moteurs de recherche. Cochez ceci pour configurer automatiquement le moteur de recherche."
}, },
"app": { "app": {
"sectionTitle": "Application liée" "sectionTitle": ""
}, },
"createApp": { "createApp": {
"label": "Créer une application", "label": "Créer une application",
@@ -991,8 +991,8 @@
"newLabel": "Nouvel ID d'installation" "newLabel": "Nouvel ID d'installation"
}, },
"privateKey": { "privateKey": {
"label": "Clé privée", "label": "",
"newLabel": "Nouvelle clé privée" "newLabel": ""
} }
} }
}, },
@@ -1044,14 +1044,14 @@
} }
}, },
"common": { "common": {
"success": "Succès", "success": "",
"beta": "Bêta", "beta": "Bêta",
"error": "Erreur", "error": "Erreur",
"action": { "action": {
"add": "Ajouter", "add": "Ajouter",
"apply": "Appliquer", "apply": "Appliquer",
"backToOverview": "Retourner à l'aperçu", "backToOverview": "Retourner à l'aperçu",
"change": "Modifier", "change": "",
"create": "Créer", "create": "Créer",
"createAnother": "Créer et recommencer", "createAnother": "Créer et recommencer",
"edit": "Modifier", "edit": "Modifier",
@@ -1174,8 +1174,8 @@
}, },
"unit": { "unit": {
"speed": { "speed": {
"kilometersPerHour": "km/h", "kilometersPerHour": "",
"milesPerHour": "mph" "milesPerHour": ""
} }
} }
}, },
@@ -1190,7 +1190,7 @@
"label": "Titre" "label": "Titre"
}, },
"customCssClasses": { "customCssClasses": {
"label": "Classes Css personnalisées" "label": ""
}, },
"borderColor": { "borderColor": {
"label": "Couleur de la bordure" "label": "Couleur de la bordure"
@@ -1321,21 +1321,21 @@
"label": "Activer la vérification du statut" "label": "Activer la vérification du statut"
}, },
"layout": { "layout": {
"label": "Mise en page", "label": "",
"option": { "option": {
"row": "Horizontale", "row": "",
"row-reverse": "Horizontal (inversé)", "row-reverse": "",
"column": "Vertical", "column": "",
"column-reverse": "Vertical (inversé)" "column-reverse": ""
} }
}, },
"descriptionDisplayMode": { "descriptionDisplayMode": {
"label": "Mode d'affichage de la description", "label": "",
"description": "Choisissez comment afficher la description de l'application", "description": "",
"option": { "option": {
"normal": "Au sein du widget", "normal": "",
"tooltip": "Comme infobulle", "tooltip": "",
"hidden": "Masquer" "hidden": ""
} }
} }
}, },
@@ -1584,11 +1584,11 @@
}, },
"placeholder": "Commencer à écrire vos notes", "placeholder": "Commencer à écrire vos notes",
"dismiss": { "dismiss": {
"title": "Abandonner les modifications ?", "title": "",
"message": "Vous avez des modifications non enregistrées dans votre bloc-notes. Êtes-vous sûr de vouloir les supprimer ?", "message": "",
"action": { "action": {
"discard": "Ignorer les modifications", "discard": "",
"keepEditing": "Continuer les modifications" "keepEditing": ""
} }
} }
}, },
@@ -1743,7 +1743,7 @@
"name": "Calendrier", "name": "Calendrier",
"description": "Afficher les événements de vos intégrations dans une vue calendrier pendant une certaine période de temps relative", "description": "Afficher les événements de vos intégrations dans une vue calendrier pendant une certaine période de temps relative",
"duration": { "duration": {
"allDay": "Tous les jours" "allDay": ""
}, },
"option": { "option": {
"releaseType": { "releaseType": {
@@ -1780,7 +1780,7 @@
"description": "Uniquement sur la météo actuelle" "description": "Uniquement sur la météo actuelle"
}, },
"useImperialSpeed": { "useImperialSpeed": {
"label": "Utiliser mph pour la vitesse du vent" "label": ""
}, },
"location": { "location": {
"label": "Lieu de la météo" "label": "Lieu de la météo"
@@ -1941,19 +1941,19 @@
"description": "Statistiques de vos conteneurs (Ce widget ne peut être ajouté qu'avec les privilèges d'administrateur)", "description": "Statistiques de vos conteneurs (Ce widget ne peut être ajouté qu'avec les privilèges d'administrateur)",
"option": { "option": {
"enableRowSorting": { "enableRowSorting": {
"label": "Activer le tri des éléments" "label": ""
}, },
"defaultSort": { "defaultSort": {
"label": "Colonne de tri par défaut", "label": "",
"option": { "option": {
"name": "Nom", "name": "",
"state": "État", "state": "",
"cpuUsage": "Utilisation du CPU", "cpuUsage": "",
"memoryUsage": "Utilisation mémoire" "memoryUsage": ""
} }
}, },
"descendingDefaultSort": { "descendingDefaultSort": {
"label": "Inverser le tri" "label": ""
} }
}, },
"error": { "error": {
@@ -2036,21 +2036,21 @@
"name": "Nom", "name": "Nom",
"id": "Id", "id": "Id",
"metadata": { "metadata": {
"title": "Statistiques détaillées", "title": "",
"video": { "video": {
"title": "Vidéo", "title": "",
"resolution": "Résolution" "resolution": ""
}, },
"audio": { "audio": {
"title": "Audio", "title": "",
"channelCount": "Canaux audio", "channelCount": "",
"codec": "Codec audio" "codec": ""
}, },
"transcoding": { "transcoding": {
"title": "Transcodage", "title": "",
"container": "Conteneur", "container": "",
"resolution": "Résolution", "resolution": "Résolution",
"target": "Codec cible" "target": ""
} }
} }
} }
@@ -2248,7 +2248,7 @@
"unknown": "Inconnu", "unknown": "Inconnu",
"pending": "En attente", "pending": "En attente",
"processing": "Traitement en cours", "processing": "Traitement en cours",
"requested": "En attente", "requested": "",
"partiallyAvailable": "Partiel", "partiallyAvailable": "Partiel",
"available": "Disponible", "available": "Disponible",
"blacklisted": "Sur la liste noire", "blacklisted": "Sur la liste noire",
@@ -2361,7 +2361,7 @@
"label": "Nombre maximum de publications" "label": "Nombre maximum de publications"
}, },
"hideDescription": { "hideDescription": {
"label": "Masquer la description" "label": ""
} }
} }
}, },
@@ -2542,7 +2542,7 @@
}, },
"firewall": { "firewall": {
"name": "Surveillance du pare-feu", "name": "Surveillance du pare-feu",
"description": "Affiche un résumé des pare-feu", "description": "",
"tab": { "tab": {
"system": "Système", "system": "Système",
"interfaces": "Interfaces" "interfaces": "Interfaces"
@@ -2556,16 +2556,16 @@
"widget": { "widget": {
"fwname": "Nom", "fwname": "Nom",
"version": "Version", "version": "Version",
"versiontitle": "Versions", "versiontitle": "",
"cputitle": "Utilisation du CPU", "cputitle": "",
"memorytitle": "Utilisation de la mémoire", "memorytitle": "",
"cpu": "CPU", "cpu": "",
"memory": "Mémoire", "memory": "",
"interfaces": { "interfaces": {
"name": "nom", "name": "",
"trans": "Transmis", "trans": "",
"recv": "Reçus", "recv": "",
"title": "Interfaces réseau" "title": ""
} }
} }
}, },
@@ -2576,37 +2576,37 @@
"option": {} "option": {}
}, },
"systemResources": { "systemResources": {
"name": "Ressources du système", "name": "",
"description": "CPU, Mémoire, Disque et autre utilisation matérielle de votre système", "description": "",
"option": { "option": {
"hasShadow": { "hasShadow": {
"label": "Activer l'ombrage des cartes" "label": ""
}, },
"visibleCharts": { "visibleCharts": {
"label": "Graphiques visibles", "label": "",
"description": "Sélectionnez les graphiques à afficher.", "description": "",
"option": { "option": {
"cpu": "CPU", "cpu": "",
"memory": "Mémoire", "memory": "",
"network": "Réseau" "network": ""
} }
}, },
"labelDisplayMode": { "labelDisplayMode": {
"label": "Mode d'affichage de l'étiquette", "label": "",
"option": { "option": {
"textWithIcon": "Afficher le texte avec l'icône", "textWithIcon": "",
"text": "Afficher uniquement le texte", "text": "",
"icon": "Afficher uniquement l'icône", "icon": "",
"hidden": "Masquer l'étiquette" "hidden": ""
} }
} }
}, },
"card": { "card": {
"cpu": "CPU", "cpu": "",
"memory": "RAM", "memory": "",
"network": "NET", "network": "",
"up": "UP", "up": "",
"down": "HORS SERVICE" "down": ""
} }
} }
}, },
@@ -3030,8 +3030,8 @@
"integration": "Intégrations", "integration": "Intégrations",
"app": "Applications", "app": "Applications",
"group": "Groupes", "group": "Groupes",
"searchEngine": "Moteurs de recherche", "searchEngine": "",
"media": "Médias" "media": ""
}, },
"statisticLabel": { "statisticLabel": {
"boards": "Tableaux de bord", "boards": "Tableaux de bord",
@@ -3040,8 +3040,8 @@
"authorization": "Autorisation" "authorization": "Autorisation"
}, },
"heroBanner": { "heroBanner": {
"title": "Bienvenue sur votre", "title": "",
"subtitle": "Tableau {app}" "subtitle": ""
} }
}, },
"board": { "board": {
@@ -3387,19 +3387,19 @@
"label": "Conteneurs Docker" "label": "Conteneurs Docker"
}, },
"firewallCpu": { "firewallCpu": {
"label": "Processeur du pare-feu" "label": ""
}, },
"firewallMemory": { "firewallMemory": {
"label": "Mémoire du pare-feu" "label": ""
}, },
"firewallVersion": { "firewallVersion": {
"label": "Version du pare-feu" "label": ""
}, },
"firewallInterfaces": { "firewallInterfaces": {
"label": "Interfaces du pare-feu" "label": ""
}, },
"weather": { "weather": {
"label": "Météo" "label": ""
} }
}, },
"interval": { "interval": {
@@ -3410,10 +3410,10 @@
"weeklyMonday": "Chaque semaine le lundi", "weeklyMonday": "Chaque semaine le lundi",
"update": { "update": {
"success": { "success": {
"message": "Intervalle mis à jour avec succès" "message": ""
}, },
"error": { "error": {
"message": "L'intervalle de mise à jour a échoué" "message": ""
} }
} }
}, },
@@ -3422,55 +3422,55 @@
}, },
"field": { "field": {
"name": { "name": {
"label": "Nom" "label": ""
}, },
"interval": { "interval": {
"label": "Intervalle de planification" "label": "Intervalle de planification"
}, },
"lastExecution": { "lastExecution": {
"label": "Dernière exécution" "label": ""
}, },
"actions": { "actions": {
"label": "Actions" "label": ""
} }
}, },
"table": { "table": {
"search": "Rechercher des tâches {count}..." "search": ""
}, },
"action": { "action": {
"refresh": { "refresh": {
"label": "Rafraîchir" "label": ""
} }
}, },
"refresh": { "refresh": {
"success": { "success": {
"message": "Tâches actualisées avec succès" "message": ""
}, },
"error": { "error": {
"message": "Échec de l'actualisation des tâches" "message": ""
} }
}, },
"trigger": { "trigger": {
"success": { "success": {
"message": "Tâche déclenchée avec succès" "message": ""
}, },
"error": { "error": {
"message": "Échec du déclenchement de la tâche" "message": ""
} }
}, },
"enable": { "enable": {
"success": { "success": {
"message": "Tâche activée avec succès" "message": ""
} }
}, },
"disable": { "disable": {
"success": { "success": {
"message": "Tâche désactivée avec succès" "message": ""
} }
}, },
"toggle": { "toggle": {
"error": { "error": {
"message": "Impossible de basculer le statut de la tâche" "message": ""
} }
} }
}, },
@@ -3527,19 +3527,19 @@
"subtitle": "{count} utilisés dans le Code d'Homarr" "subtitle": "{count} utilisés dans le Code d'Homarr"
}, },
"hotkeys": { "hotkeys": {
"title": "Raccourcis clavier", "title": "",
"subtitle": "Raccourcis clavier pour améliorer votre flux de travail", "subtitle": "",
"field": { "field": {
"shortcut": "Raccourci", "shortcut": "",
"action": "Action" "action": ""
}, },
"action": { "action": {
"toggleBoardEdit": "Activer/désactiver le mode d'édition du tableau", "toggleBoardEdit": "",
"toggleColorScheme": "Basculer en mode clair/sombre", "toggleColorScheme": "",
"saveNotebook": "Enregistrer le bloc-notes (uniquement à l'intérieur du widget notebook)", "saveNotebook": "",
"openSpotlight": "Ouvrir la recherche" "openSpotlight": ""
}, },
"note": "Astuce : Mod correspond à la touche Ctrl ou ⌘ sur macOS" "note": ""
} }
} }
} }
@@ -3995,7 +3995,7 @@
"tools": { "tools": {
"label": "Outils", "label": "Outils",
"tasks": { "tasks": {
"label": "Tâches" "label": ""
}, },
"docker": { "docker": {
"label": "Docker" "label": "Docker"
File diff suppressed because it is too large. Load diff
+34 -34
View File
@@ -177,8 +177,8 @@
} }
}, },
"forgotPassword": { "forgotPassword": {
"label": "Ste pozabili geslo?", "label": "",
"description": "Administrator lahko zažene ukaz:" "description": ""
} }
}, },
"register": { "register": {
@@ -622,7 +622,7 @@
"description": "", "description": "",
"action": "" "action": ""
}, },
"add": "Dodaj aplikacijo" "add": ""
} }
}, },
"integration": { "integration": {
@@ -1059,10 +1059,10 @@
"insert": "Vstavite", "insert": "Vstavite",
"remove": "Odstrani", "remove": "Odstrani",
"save": "Shrani", "save": "Shrani",
"saveChanges": "Shrani spremembe", "saveChanges": "Shranjevanje sprememb",
"cancel": "Prekliči", "cancel": "Prekliči",
"delete": "Izbriši", "delete": "Izbriši",
"discard": "Zapusti", "discard": "",
"close": "", "close": "",
"confirm": "Potrdi", "confirm": "Potrdi",
"continue": "", "continue": "",
@@ -1125,13 +1125,13 @@
"userAvatar": { "userAvatar": {
"menu": { "menu": {
"switchToDarkMode": "", "switchToDarkMode": "",
"switchToLightMode": "Svetli način", "switchToLightMode": "",
"management": "Nastavitve", "management": "",
"preferences": "Prilagoditev", "preferences": "Vaše želje",
"logout": "Odjava", "logout": "",
"login": "Prijava", "login": "Prijava",
"homeBoard": "Vaša nadzorna plošča", "homeBoard": "",
"loggedOut": "Odjava uspešna", "loggedOut": "",
"updateAvailable": "" "updateAvailable": ""
} }
}, },
@@ -1182,7 +1182,7 @@
"section": { "section": {
"dynamic": { "dynamic": {
"action": { "action": {
"create": "Nova dinamična vsebina", "create": "",
"remove": "" "remove": ""
}, },
"option": { "option": {
@@ -1208,7 +1208,7 @@
} }
}, },
"action": { "action": {
"create": "Nova kategorija", "create": "",
"edit": "", "edit": "",
"remove": "", "remove": "",
"moveUp": "Premaknite se navzgor", "moveUp": "Premaknite se navzgor",
@@ -1243,12 +1243,12 @@
}, },
"item": { "item": {
"action": { "action": {
"create": "Nova vsebina", "create": "",
"import": "", "import": "",
"edit": "Uredi gradnik", "edit": "",
"moveResize": "Premakni/spremeni", "moveResize": "",
"duplicate": "Podvoji", "duplicate": "",
"remove": "Izbriši" "remove": ""
}, },
"menu": { "menu": {
"label": { "label": {
@@ -2638,8 +2638,8 @@
"edit": { "edit": {
"notification": { "notification": {
"success": { "success": {
"title": "Spremembe so uveljavljene", "title": "",
"message": "Vsebina je bila uspšeno shranjena" "message": ""
}, },
"error": { "error": {
"title": "", "title": "",
@@ -2647,8 +2647,8 @@
} }
}, },
"confirmLeave": { "confirmLeave": {
"title": "Neshranjene spremembe", "title": "",
"message": "Vsebina ni shranjena, želite nadaljevati?" "message": ""
} }
}, },
"oldImport": { "oldImport": {
@@ -2716,16 +2716,16 @@
}, },
"field": { "field": {
"pageTitle": { "pageTitle": {
"label": "Naslov strani" "label": ""
}, },
"metaTitle": { "metaTitle": {
"label": "Meta naslov" "label": ""
}, },
"logoImageUrl": { "logoImageUrl": {
"label": "URL logotipa" "label": ""
}, },
"faviconImageUrl": { "faviconImageUrl": {
"label": "URL ikone" "label": ""
}, },
"backgroundImageUrl": { "backgroundImageUrl": {
"label": "", "label": "",
@@ -2835,7 +2835,7 @@
"metaTitle": "" "metaTitle": ""
}, },
"setting": { "setting": {
"title": "Nastavitve za {boardName} nadzorno ploščo", "title": "",
"section": { "section": {
"general": { "general": {
"title": "Splošno", "title": "Splošno",
@@ -2844,9 +2844,9 @@
"layout": { "layout": {
"title": "Postavitev", "title": "Postavitev",
"responsive": { "responsive": {
"title": "Predloga", "title": "",
"action": { "action": {
"add": "Dodaj predlogo" "add": ""
} }
} }
}, },
@@ -2959,14 +2959,14 @@
"layout": { "layout": {
"field": { "field": {
"name": { "name": {
"label": "Ime" "label": ""
}, },
"columnCount": { "columnCount": {
"label": "Število stolpcev" "label": ""
}, },
"breakpoint": { "breakpoint": {
"label": "Prekinitvene točke", "label": "",
"description": "Postavitev bo uporabljena na vseh zaslonih, večjih od te prelomne točke, do naslednje večje prelomne točke." "description": ""
} }
} }
}, },
@@ -4046,7 +4046,7 @@
} }
}, },
"search": { "search": {
"placeholder": "Iskanje vsebine", "placeholder": "",
"nothingFound": "", "nothingFound": "",
"error": { "error": {
"fetch": "" "fetch": ""
+1 -1
View File
@@ -34,7 +34,7 @@
"@mantine/hooks": "^8.3.10", "@mantine/hooks": "^8.3.10",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.1.1", "next": "16.0.10",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"svgson": "^5.3.1" "svgson": "^5.3.1"
+1 -1
View File
@@ -24,7 +24,7 @@
"dependencies": { "dependencies": {
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"zod": "^4.2.1", "zod": "^4.1.13",
"zod-form-data": "^3.0.1" "zod-form-data": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {
+18 -18
View File
@@ -52,33 +52,33 @@
"@mantine/core": "^8.3.10", "@mantine/core": "^8.3.10",
"@mantine/hooks": "^8.3.10", "@mantine/hooks": "^8.3.10",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"@tiptap/extension-color": "3.14.0", "@tiptap/extension-color": "3.13.0",
"@tiptap/extension-highlight": "3.14.0", "@tiptap/extension-highlight": "3.13.0",
"@tiptap/extension-image": "3.14.0", "@tiptap/extension-image": "3.13.0",
"@tiptap/extension-link": "^3.14.0", "@tiptap/extension-link": "^3.13.0",
"@tiptap/extension-placeholder": "^3.14.0", "@tiptap/extension-placeholder": "^3.13.0",
"@tiptap/extension-table": "3.14.0", "@tiptap/extension-table": "3.13.0",
"@tiptap/extension-table-cell": "3.14.0", "@tiptap/extension-table-cell": "3.13.0",
"@tiptap/extension-table-header": "3.14.0", "@tiptap/extension-table-header": "3.13.0",
"@tiptap/extension-table-row": "3.14.0", "@tiptap/extension-table-row": "3.13.0",
"@tiptap/extension-task-item": "3.14.0", "@tiptap/extension-task-item": "3.13.0",
"@tiptap/extension-task-list": "3.14.0", "@tiptap/extension-task-list": "3.13.0",
"@tiptap/extension-text-align": "3.14.0", "@tiptap/extension-text-align": "3.13.0",
"@tiptap/extension-text-style": "3.14.0", "@tiptap/extension-text-style": "3.13.0",
"@tiptap/extension-underline": "3.14.0", "@tiptap/extension-underline": "3.13.0",
"@tiptap/react": "^3.14.0", "@tiptap/react": "^3.13.0",
"@tiptap/starter-kit": "^3.14.0", "@tiptap/starter-kit": "^3.13.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"mantine-form-zod-resolver": "^1.3.0", "mantine-form-zod-resolver": "^1.3.0",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.1.1", "next": "16.0.10",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"recharts": "^2.15.4", "recharts": "^2.15.4",
"video.js": "^8.23.4", "video.js": "^8.23.4",
"zod": "^4.2.1" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
@@ -27,24 +27,13 @@ export const WidgetMultiTextInput = ({ property, kind, options }: CommonWidgetIn
}; };
const currentValidationResult = React.useMemo(() => { const currentValidationResult = React.useMemo(() => {
if (!options.validate) { return options.validate.safeParse(search);
return {
success: false,
result: null,
};
}
const validationResult = options.validate.safeParse(search);
return {
success: validationResult.success,
result: validationResult,
};
}, [options.validate, search]); }, [options.validate, search]);
const error = React.useMemo(() => { const error = React.useMemo(() => {
/* hide the error when nothing is being typed since "" is not valid but is not an explicit error */ /* hide the error when nothing is being typed since "" is not valid but is not an explicit error */
if (!currentValidationResult.success && currentValidationResult.result && search.length !== 0) { if (!currentValidationResult.success && search.length !== 0) {
return currentValidationResult.result.error?.issues[0]?.message; return currentValidationResult.error.issues[0]?.message;
} }
return null; return null;
}, [currentValidationResult, search]); }, [currentValidationResult, search]);
+2
View File
@@ -1,5 +1,6 @@
import { ActionIcon, Avatar, Group, Stack, Text } from "@mantine/core"; import { ActionIcon, Avatar, Group, Stack, Text } from "@mantine/core";
import { IconBookmark, IconX } from "@tabler/icons-react"; import { IconBookmark, IconX } from "@tabler/icons-react";
import z from "zod";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
@@ -56,6 +57,7 @@ export const { definition, componentLoader } = createWidgetDefinition("bookmarks
isLoading, isLoading,
}; };
}, },
validate: z.array(z.string()),
}), }),
})); }));
}, },
+3 -1
View File
@@ -42,7 +42,9 @@ export interface WidgetDefinition {
icon: TablerIcon; icon: TablerIcon;
supportedIntegrations?: IntegrationKind[]; supportedIntegrations?: IntegrationKind[];
integrationsRequired?: boolean; integrationsRequired?: boolean;
createOptions: (settings: SettingsContextProps) => WidgetOptionsRecord; createOptions: (
settings: Pick<SettingsContextProps, "enableStatusByDefault" | "firstDayOfWeek" | "forceDisableStatus">,
) => WidgetOptionsRecord;
errors?: Partial< errors?: Partial<
Record< Record<
DefaultErrorData["code"], DefaultErrorData["code"],
+1 -1
View File
@@ -115,7 +115,7 @@ export type inferSupportedIntegrationsStrict<TKind extends WidgetKind> = (Widget
export const reduceWidgetOptionsWithDefaultValues = ( export const reduceWidgetOptionsWithDefaultValues = (
kind: WidgetKind, kind: WidgetKind,
settings: SettingsContextProps, settings: Pick<SettingsContextProps, "enableStatusByDefault" | "firstDayOfWeek" | "forceDisableStatus">,
currentValue: Record<string, unknown> = {}, currentValue: Record<string, unknown> = {},
) => { ) => {
const definition = widgetImports[kind].definition; const definition = widgetImports[kind].definition;
+15 -6
View File
@@ -5,6 +5,7 @@ import { z } from "zod/v4";
import type { ZodType } from "zod/v4"; import type { ZodType } from "zod/v4";
import type { IntegrationKind } from "@homarr/definitions"; import type { IntegrationKind } from "@homarr/definitions";
import { zodEnumFromArray } from "@homarr/validation/enums";
import type { inferSelectOptionValue, SelectOption } from "./_inputs/widget-select-input"; import type { inferSelectOptionValue, SelectOption } from "./_inputs/widget-select-input";
import type { ReleasesRepository } from "./releases/releases-repository"; import type { ReleasesRepository } from "./releases/releases-repository";
@@ -38,6 +39,7 @@ export interface SortableItemListInput<TItem, TOptionValue extends UniqueIdentif
}) => React.ReactNode; }) => React.ReactNode;
uniqueIdentifier: (item: TItem) => TOptionValue; uniqueIdentifier: (item: TItem) => TOptionValue;
useData: (values: TOptionValue[]) => { data: TItem[] | undefined; isLoading: boolean; error: unknown }; useData: (values: TOptionValue[]) => { data: TItem[] | undefined; isLoading: boolean; error: unknown };
validate: z.ZodArray<z.ZodType>;
} }
interface SelectInput<TOptions extends readonly SelectOption[]> extends CommonInput< interface SelectInput<TOptions extends readonly SelectOption[]> extends CommonInput<
@@ -68,12 +70,13 @@ const optionsFactory = {
type: "switch" as const, type: "switch" as const,
defaultValue: input?.defaultValue ?? false, defaultValue: input?.defaultValue ?? false,
withDescription: input?.withDescription ?? false, withDescription: input?.withDescription ?? false,
validate: z.boolean(),
}), }),
text: (input?: TextInput) => ({ text: (input?: TextInput) => ({
type: "text" as const, type: "text" as const,
defaultValue: input?.defaultValue ?? "", defaultValue: input?.defaultValue ?? "",
withDescription: input?.withDescription ?? false, withDescription: input?.withDescription ?? false,
validate: input?.validate, validate: input?.validate ?? z.string(),
}), }),
multiSelect: <const TOptions extends SelectOption[]>(input: MultiSelectInput<TOptions>) => ({ multiSelect: <const TOptions extends SelectOption[]>(input: MultiSelectInput<TOptions>) => ({
type: "multiSelect" as const, type: "multiSelect" as const,
@@ -81,6 +84,9 @@ const optionsFactory = {
options: input.options, options: input.options,
searchable: input.searchable ?? false, searchable: input.searchable ?? false,
withDescription: input.withDescription ?? false, withDescription: input.withDescription ?? false,
validate: z.array(
zodEnumFromArray(input.options.map((option) => (typeof option === "string" ? option : option.value))),
),
}), }),
select: <const TOptions extends SelectOption[]>(input: SelectInput<TOptions>) => ({ select: <const TOptions extends SelectOption[]>(input: SelectInput<TOptions>) => ({
type: "select" as const, type: "select" as const,
@@ -88,6 +94,7 @@ const optionsFactory = {
options: input.options, options: input.options,
searchable: input.searchable ?? false, searchable: input.searchable ?? false,
withDescription: input.withDescription ?? false, withDescription: input.withDescription ?? false,
validate: zodEnumFromArray(input.options.map((option) => (typeof option === "string" ? option : option.value))),
}), }),
number: (input: NumberInput) => ({ number: (input: NumberInput) => ({
type: "number" as const, type: "number" as const,
@@ -122,19 +129,20 @@ const optionsFactory = {
defaultValue: input?.defaultValue ?? [], defaultValue: input?.defaultValue ?? [],
withDescription: input?.withDescription ?? false, withDescription: input?.withDescription ?? false,
values: [] as string[], values: [] as string[],
validate: input?.validate, validate: input?.validate ?? z.array(z.string()),
}), }),
multiReleasesRepositories: (input?: CommonInput<ReleasesRepository[]> & { validate?: ZodType }) => ({ multiReleasesRepositories: (input: CommonInput<ReleasesRepository[]> & { validate: ZodType }) => ({
type: "multiReleasesRepositories" as const, type: "multiReleasesRepositories" as const,
defaultValue: input?.defaultValue ?? [], defaultValue: input.defaultValue ?? [],
withDescription: input?.withDescription ?? false, withDescription: input.withDescription ?? false,
values: [] as ReleasesRepository[], values: [] as ReleasesRepository[],
validate: input?.validate, validate: input.validate,
}), }),
app: () => ({ app: () => ({
type: "app" as const, type: "app" as const,
defaultValue: "", defaultValue: "",
withDescription: false, withDescription: false,
validate: z.string(),
}), }),
sortableItemList: <const TItem, const TOptionValue extends UniqueIdentifier>( sortableItemList: <const TItem, const TOptionValue extends UniqueIdentifier>(
input: SortableItemListInput<TItem, TOptionValue>, input: SortableItemListInput<TItem, TOptionValue>,
@@ -146,6 +154,7 @@ const optionsFactory = {
uniqueIdentifier: input.uniqueIdentifier, uniqueIdentifier: input.uniqueIdentifier,
useData: input.useData, useData: input.useData,
withDescription: false, withDescription: false,
validate: input.validate,
}), }),
}; };
@@ -47,7 +47,7 @@ export const SystemResourceMemoryChart = ({
return ( return (
<Paper px={3} py={2} withBorder shadow="md" radius="md"> <Paper px={3} py={2} withBorder shadow="md" radius="md">
<Text c="dimmed" size="xs"> <Text c="dimmed" size="xs">
{humanFileSize(Math.round(value))} / {humanFileSize(totalCapacityInBytes)} ( {humanFileSize(value)} / {humanFileSize(totalCapacityInBytes)} (
{Math.round((value / totalCapacityInBytes) * 100)}%) {Math.round((value / totalCapacityInBytes) * 100)}%)
</Text> </Text>
</Paper> </Paper>
@@ -22,7 +22,6 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo
}); });
const memoryCapacityInBytes = const memoryCapacityInBytes =
(data[0]?.healthInfo.memAvailableInBytes ?? 0) + (data[0]?.healthInfo.memUsedInBytes ?? 0); (data[0]?.healthInfo.memAvailableInBytes ?? 0) + (data[0]?.healthInfo.memUsedInBytes ?? 0);
const [items, setItems] = useState<{ cpu: number; memory: number; network: { up: number; down: number } | null }[]>( const [items, setItems] = useState<{ cpu: number; memory: number; network: { up: number; down: number } | null }[]>(
data.map((item) => ({ data.map((item) => ({
cpu: item.healthInfo.cpuUtilization, cpu: item.healthInfo.cpuUtilization,
@@ -14,7 +14,7 @@ const labelDisplayModeOptions = {
export const { definition, componentLoader } = createWidgetDefinition("systemResources", { export const { definition, componentLoader } = createWidgetDefinition("systemResources", {
icon: IconGraphFilled, icon: IconGraphFilled,
supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid"], supportedIntegrations: ["dashDot", "openmediavault", "truenas"],
createOptions() { createOptions() {
return optionsBuilder.from((factory) => ({ return optionsBuilder.from((factory) => ({
hasShadow: factory.switch({ defaultValue: true }), hasShadow: factory.switch({ defaultValue: true }),
+2611 -2217
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1 +1 @@
[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"tancak","avatar_url":"https://avatars.githubusercontent.com/u/11760749?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/uLine truncated [{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"tancak","avatar_url":"https://avatars.githubusercontent.com/u/11760749?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/uLine truncated
+1 -1
View File
@@ -1 +1 @@
[{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/249b02e3368752cb175446082015737c_default.png"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14861042/medium/2d5c4e62613f03082f3e645fa92efd59.jpeg"},{"username":"djismgaming","avatarUrl":"httpsLine truncated [{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"Skick","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12619811/medium/0331f1de413a645b4516bc4e037f31bb.jpg"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/249b02e3368752cb175446082015737c_default.png"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowLine truncated
+3 -3
View File
@@ -17,14 +17,14 @@
}, },
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@next/eslint-plugin-next": "16.1.1", "@next/eslint-plugin-next": "16.0.10",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-config-turbo": "^2.7.2", "eslint-config-turbo": "^2.6.3",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^6.1.1", "eslint-plugin-react-hooks": "^6.1.1",
"typescript-eslint": "^8.50.1" "typescript-eslint": "^8.49.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",