Compare commits
No files matched your search
@@ -33,6 +33,9 @@ body:
|
|||||||
options:
|
options:
|
||||||
# The below comment is used to insert a new version with on-release.yml
|
# The below comment is used to insert a new version with on-release.yml
|
||||||
#NEXT_VERSION#
|
#NEXT_VERSION#
|
||||||
|
- 1.49.0
|
||||||
|
- 1.48.0
|
||||||
|
- 1.47.0
|
||||||
- 1.46.0
|
- 1.46.0
|
||||||
- 1.45.3
|
- 1.45.3
|
||||||
- 1.45.2
|
- 1.45.2
|
||||||
|
|||||||
+102
@@ -1,3 +1,105 @@
|
|||||||
|
## [1.50.1](https://github.com/homarr-labs/homarr/compare/v1.50.0...v1.50.1) (2026-01-16)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** update dependency ioredis to v5.9.0 ([#4827](https://github.com/homarr-labs/homarr/issues/4827)) ([155d4f2](https://github.com/homarr-labs/homarr/commit/155d4f2270b3b35c4ad696499b0aaa0b10100989))
|
||||||
|
* **deps:** update dependency ioredis to v5.9.1 ([#4844](https://github.com/homarr-labs/homarr/issues/4844)) ([5d00642](https://github.com/homarr-labs/homarr/commit/5d006422688cf3fe7440ae3ab66ad3b2cc9286a8))
|
||||||
|
* **deps:** update dependency ldapts to v8.1.3 ([#4841](https://github.com/homarr-labs/homarr/issues/4841)) ([7fa9907](https://github.com/homarr-labs/homarr/commit/7fa990712bddc88bbb3bbe01e758bfef7cb4dea3))
|
||||||
|
* **deps:** update dependency react-error-boundary to ^6.0.2 ([#4821](https://github.com/homarr-labs/homarr/issues/4821)) ([e9ba102](https://github.com/homarr-labs/homarr/commit/e9ba102cf668c0ef54628d3dbc6e6cbda2d059b5))
|
||||||
|
* **deps:** update dependency react-error-boundary to ^6.0.3 ([#4847](https://github.com/homarr-labs/homarr/issues/4847)) ([c24006d](https://github.com/homarr-labs/homarr/commit/c24006dca691f26a7187c0ba112d9ed2ec424bb0))
|
||||||
|
* **deps:** update dependency sass to ^1.97.2 ([#4835](https://github.com/homarr-labs/homarr/issues/4835)) ([07ba531](https://github.com/homarr-labs/homarr/commit/07ba53165c90d675aa54c5ad3fca930fef5cd35e))
|
||||||
|
* **deps:** update dependency typescript-eslint to ^8.52.0 ([#4830](https://github.com/homarr-labs/homarr/issues/4830)) ([fa8af6a](https://github.com/homarr-labs/homarr/commit/fa8af6a453b846af2305db3c318c8c1c303a3a88))
|
||||||
|
* **deps:** update dependency undici to v7.17.0 ([#4826](https://github.com/homarr-labs/homarr/issues/4826)) ([21a91b1](https://github.com/homarr-labs/homarr/commit/21a91b1489ff1a1f0a1428a3f50f00db8932da72))
|
||||||
|
* **deps:** update dependency undici to v7.18.0 ([#4829](https://github.com/homarr-labs/homarr/issues/4829)) ([b76ab29](https://github.com/homarr-labs/homarr/commit/b76ab2961d84081c9ef04d76193f21a1068e72d9))
|
||||||
|
* **deps:** update dependency undici to v7.18.2 ([#4837](https://github.com/homarr-labs/homarr/issues/4837)) ([16733c2](https://github.com/homarr-labs/homarr/commit/16733c2a0efabcbc5b693a8b6f82833f6fc344b7))
|
||||||
|
* **deps:** update dependency ws to ^8.19.0 ([#4833](https://github.com/homarr-labs/homarr/issues/4833)) ([629359e](https://github.com/homarr-labs/homarr/commit/629359e3e44ddad880842cfd576edb514f80d61a))
|
||||||
|
* **deps:** update mantine monorepo to ^8.3.11 ([#4822](https://github.com/homarr-labs/homarr/issues/4822)) ([0364e10](https://github.com/homarr-labs/homarr/commit/0364e102cd6c0f560bc49e0b9ccd71df0b75eed6))
|
||||||
|
* **deps:** update mantine monorepo to ^8.3.12 ([#4848](https://github.com/homarr-labs/homarr/issues/4848)) ([275a346](https://github.com/homarr-labs/homarr/commit/275a346afaaebfa894ef2193f70966986a138dbb))
|
||||||
|
* **deps:** update tiptap monorepo to v3.15.0 ([#4832](https://github.com/homarr-labs/homarr/issues/4832)) ([ab3ee4f](https://github.com/homarr-labs/homarr/commit/ab3ee4f78a739bdb3b49a81652420df6ec143b62))
|
||||||
|
* **deps:** update tiptap monorepo to v3.15.1 ([#4838](https://github.com/homarr-labs/homarr/issues/4838)) ([d479b63](https://github.com/homarr-labs/homarr/commit/d479b63a803f2295ccdf20fcd31c418a4aab148c))
|
||||||
|
* **deps:** update tiptap monorepo to v3.15.2 ([#4842](https://github.com/homarr-labs/homarr/issues/4842)) ([55c2666](https://github.com/homarr-labs/homarr/commit/55c2666f9d27ca34b6d028070f0d7b84e4c40e97))
|
||||||
|
* **deps:** update tiptap monorepo to v3.15.3 ([#4843](https://github.com/homarr-labs/homarr/issues/4843)) ([b26cf0e](https://github.com/homarr-labs/homarr/commit/b26cf0e65fd8150e241465628df2cb8b5a72088e))
|
||||||
|
* **deps:** update turbo monorepo to ^2.7.3 ([#4831](https://github.com/homarr-labs/homarr/issues/4831)) ([50410fe](https://github.com/homarr-labs/homarr/commit/50410fedb3f33ed131f923257e61ae2dca4e0288))
|
||||||
|
|
||||||
|
## [1.50.0](https://github.com/homarr-labs/homarr/compare/v1.49.1...v1.50.0) (2026-01-09)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **auth:** extend API key authentication to tRPC endpoints ([#4732](https://github.com/homarr-labs/homarr/issues/4732)) ([0dc61a8](https://github.com/homarr-labs/homarr/commit/0dc61a88b8d12c61fd436adf0693c9fa5087a8c0))
|
||||||
|
* **users:** add libravatar / gravatar support ([#4277](https://github.com/homarr-labs/homarr/issues/4277)) ([a2a3412](https://github.com/homarr-labs/homarr/commit/a2a34124ae11b0b9242b9c2579f7376768d35d52))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** update dependency @tabler/icons-react to ^3.36.0 ([#4707](https://github.com/homarr-labs/homarr/issues/4707)) ([35e0198](https://github.com/homarr-labs/homarr/commit/35e0198b5df2a315ea2efb584752c5af062f960d))
|
||||||
|
* **deps:** update dependency @tabler/icons-react to ^3.36.1 ([#4812](https://github.com/homarr-labs/homarr/issues/4812)) ([dabb2bb](https://github.com/homarr-labs/homarr/commit/dabb2bb7821f6a68d41c4a931bb70c41fdd21a72))
|
||||||
|
* **deps:** update dependency @tanstack/react-query to ^5.90.15 ([#4794](https://github.com/homarr-labs/homarr/issues/4794)) ([ea848bd](https://github.com/homarr-labs/homarr/commit/ea848bdd0bc1268675d00213bb86dc1d1b298162))
|
||||||
|
* **deps:** update dependency @tanstack/react-query to ^5.90.16 ([#4799](https://github.com/homarr-labs/homarr/issues/4799)) ([413e846](https://github.com/homarr-labs/homarr/commit/413e846133f13a800f509273b8b87cf677f0024d))
|
||||||
|
* **deps:** update dependency ldapts to v8.1.0 ([#4804](https://github.com/homarr-labs/homarr/issues/4804)) ([c8ecf5f](https://github.com/homarr-labs/homarr/commit/c8ecf5f0a13b1c74f24cebced76a60728eb2d543))
|
||||||
|
* **deps:** update dependency ldapts to v8.1.2 ([#4805](https://github.com/homarr-labs/homarr/issues/4805)) ([9e63e34](https://github.com/homarr-labs/homarr/commit/9e63e3443af7946199999104024f00991cc1cba2))
|
||||||
|
* **deps:** update dependency next-intl to v4.7.0 ([#4806](https://github.com/homarr-labs/homarr/issues/4806)) ([35042a7](https://github.com/homarr-labs/homarr/commit/35042a783ce5c381425bb96766adbefcac75bfdd))
|
||||||
|
* **deps:** update dependency typescript-eslint to ^8.51.0 ([#4795](https://github.com/homarr-labs/homarr/issues/4795)) ([f0a2551](https://github.com/homarr-labs/homarr/commit/f0a2551fdf5b5e02ece789d473411ad3edcfb093))
|
||||||
|
|
||||||
|
## [1.49.1](https://github.com/homarr-labs/homarr/compare/v1.49.0...v1.49.1) (2026-01-03)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **db:** connection issue do to missing host and name of db when connecting with host ([#4786](https://github.com/homarr-labs/homarr/issues/4786)) ([7d33225](https://github.com/homarr-labs/homarr/commit/7d3322576c3ed1b9917873bf707d77d2a4eef1a8))
|
||||||
|
* **deps:** update dependency ldapts to v8.0.36 ([#4790](https://github.com/homarr-labs/homarr/issues/4790)) ([1842e5f](https://github.com/homarr-labs/homarr/commit/1842e5fff5c072c8da45a01be860e63d12d5062a))
|
||||||
|
* **deps:** update dependency react-error-boundary to ^6.0.1 ([#4791](https://github.com/homarr-labs/homarr/issues/4791)) ([1b70973](https://github.com/homarr-labs/homarr/commit/1b70973084c9e9ebed34e8d7d9690a9e5530f60f))
|
||||||
|
* **unraid:** temperature of disks can be null ([#4788](https://github.com/homarr-labs/homarr/issues/4788)) ([60f9d4e](https://github.com/homarr-labs/homarr/commit/60f9d4e6e69df2190171e5d8e263b2030447ab2e))
|
||||||
|
|
||||||
|
## [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)
|
## [1.47.0](https://github.com/homarr-labs/homarr/compare/v1.46.0...v1.47.0) (2025-12-19)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
+5
-5
@@ -2,8 +2,8 @@
|
|||||||
* @homarr-labs/maintainers
|
* @homarr-labs/maintainers
|
||||||
|
|
||||||
# Exempt Renovate‑managed files (no owners)
|
# Exempt Renovate‑managed files (no owners)
|
||||||
package.json @homarr-labs/none
|
package.json
|
||||||
package-lock.json @homarr-labs/none
|
package-lock.json
|
||||||
pnpm-lock.yaml @homarr-labs/none
|
pnpm-lock.yaml
|
||||||
Dockerfile @homarr-labs/none
|
Dockerfile
|
||||||
docker-compose.yml @homarr-labs/none
|
docker-compose.yml
|
||||||
+23
-23
@@ -48,24 +48,24 @@
|
|||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@homarr/widgets": "workspace:^0.1.0",
|
"@homarr/widgets": "workspace:^0.1.0",
|
||||||
"@mantine/colors-generator": "^8.3.10",
|
"@mantine/colors-generator": "^8.3.12",
|
||||||
"@mantine/core": "^8.3.10",
|
"@mantine/core": "^8.3.12",
|
||||||
"@mantine/dropzone": "^8.3.10",
|
"@mantine/dropzone": "^8.3.12",
|
||||||
"@mantine/hooks": "^8.3.10",
|
"@mantine/hooks": "^8.3.12",
|
||||||
"@mantine/modals": "^8.3.10",
|
"@mantine/modals": "^8.3.12",
|
||||||
"@mantine/tiptap": "^8.3.10",
|
"@mantine/tiptap": "^8.3.12",
|
||||||
"@million/lint": "1.0.14",
|
"@million/lint": "1.0.14",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.36.1",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.16",
|
||||||
"@tanstack/react-query-devtools": "^5.91.1",
|
"@tanstack/react-query-devtools": "^5.91.2",
|
||||||
"@tanstack/react-query-next-experimental": "^5.91.0",
|
"@tanstack/react-query-next-experimental": "^5.91.0",
|
||||||
"@trpc/client": "^11.8.0",
|
"@trpc/client": "^11.8.1",
|
||||||
"@trpc/next": "^11.8.0",
|
"@trpc/next": "^11.8.1",
|
||||||
"@trpc/react-query": "^11.8.0",
|
"@trpc/react-query": "^11.8.1",
|
||||||
"@trpc/server": "^11.8.0",
|
"@trpc/server": "^11.8.1",
|
||||||
"@xterm/addon-canvas": "^0.7.0",
|
"@xterm/addon-canvas": "^0.7.0",
|
||||||
"@xterm/addon-fit": "0.10.0",
|
"@xterm/addon-fit": "0.11.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^6.0.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,30 +73,30 @@
|
|||||||
"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.34.0",
|
"isomorphic-dompurify": "^2.35.0",
|
||||||
"jotai": "^2.16.0",
|
"jotai": "^2.16.1",
|
||||||
"mantine-react-table": "2.0.0-beta.9",
|
"mantine-react-table": "2.0.0-beta.9",
|
||||||
"next": "16.0.10",
|
"next": "16.1.1",
|
||||||
"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.3",
|
||||||
"react-simple-code-editor": "^0.14.1",
|
"react-simple-code-editor": "^0.14.1",
|
||||||
"sass": "^1.96.0",
|
"sass": "^1.97.2",
|
||||||
"superjson": "2.2.6",
|
"superjson": "2.2.6",
|
||||||
"swagger-ui-react": "^5.31.0",
|
"swagger-ui-react": "^5.31.0",
|
||||||
"use-deep-compare-effect": "^1.8.1",
|
"use-deep-compare-effect": "^1.8.1",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/chroma-js": "3.1.2",
|
"@types/chroma-js": "3.1.2",
|
||||||
"@types/node": "^24.10.4",
|
"@types/node": "^24.10.7",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/react": "19.2.7",
|
"@types/react": "19.2.8",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/swagger-ui-react": "^5.18.0",
|
"@types/swagger-ui-react": "^5.18.0",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export default async function Layout(props: {
|
|||||||
forceDisableStatus: serverSettings.board.forceDisableStatus,
|
forceDisableStatus: serverSettings.board.forceDisableStatus,
|
||||||
},
|
},
|
||||||
search: { defaultSearchEngineId: serverSettings.search.defaultSearchEngineId },
|
search: { defaultSearchEngineId: serverSettings.search.defaultSearchEngineId },
|
||||||
|
user: { enableGravatar: serverSettings.user.enableGravatar },
|
||||||
}}
|
}}
|
||||||
{...innerProps}
|
{...innerProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Switch } from "@mantine/core";
|
||||||
|
|
||||||
|
import type { ServerSettings } from "@homarr/server-settings";
|
||||||
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
|
import { CommonSettingsForm } from "./common-form";
|
||||||
|
|
||||||
|
export const UserSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["user"] }) => {
|
||||||
|
const tUser = useScopedI18n("management.page.settings.section.user");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommonSettingsForm settingKey="user" defaultValues={defaultValues}>
|
||||||
|
{(form) => (
|
||||||
|
<>
|
||||||
|
<Switch
|
||||||
|
{...form.getInputProps("enableGravatar", { type: "checkbox" })}
|
||||||
|
label={tUser("enableGravatar.label")}
|
||||||
|
description={tUser("enableGravatar.description")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CommonSettingsForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@ import { AppearanceSettingsForm } from "./_components/appearance-settings-form";
|
|||||||
import { BoardSettingsForm } from "./_components/board-settings-form";
|
import { BoardSettingsForm } from "./_components/board-settings-form";
|
||||||
import { CultureSettingsForm } from "./_components/culture-settings-form";
|
import { CultureSettingsForm } from "./_components/culture-settings-form";
|
||||||
import { SearchSettingsForm } from "./_components/search-settings-form";
|
import { SearchSettingsForm } from "./_components/search-settings-form";
|
||||||
|
import { UserSettingsForm } from "./_components/user-settings-form";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
export async function generateMetadata() {
|
||||||
const t = await getScopedI18n("management");
|
const t = await getScopedI18n("management");
|
||||||
@@ -42,6 +43,10 @@ export default async function SettingsPage() {
|
|||||||
<Title order={2}>{tSettings("section.board.title")}</Title>
|
<Title order={2}>{tSettings("section.board.title")}</Title>
|
||||||
<BoardSettingsForm defaultValues={serverSettings.board} />
|
<BoardSettingsForm defaultValues={serverSettings.board} />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Title order={2}>{tSettings("section.user.title")}</Title>
|
||||||
|
<UserSettingsForm defaultValues={serverSettings.user} />
|
||||||
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Title order={2}>{tSettings("section.search.title")}</Title>
|
<Title order={2}>{tSettings("section.search.title")}</Title>
|
||||||
<SearchSettingsForm defaultValues={serverSettings.search} />
|
<SearchSettingsForm defaultValues={serverSettings.search} />
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ 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())} />
|
||||||
);
|
);
|
||||||
|
|||||||
+4
-1
@@ -200,7 +200,10 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
|
|||||||
<Stepper.Step label={t("step.review.label")} allowStepSelect={false} allowStepClick={false}>
|
<Stepper.Step label={t("step.review.label")} allowStepSelect={false} allowStepClick={false}>
|
||||||
<Card p="xl" shadow="md" withBorder>
|
<Card p="xl" shadow="md" withBorder>
|
||||||
<Stack maw={300} align="center" mx="auto">
|
<Stack maw={300} align="center" mx="auto">
|
||||||
<UserAvatar size="xl" user={{ name: generalForm.values.username, image: null }} />
|
<UserAvatar
|
||||||
|
size="xl"
|
||||||
|
user={{ name: generalForm.values.username, email: generalForm.values.email ?? null, image: null }}
|
||||||
|
/>
|
||||||
<Text tt="uppercase" fw="bolder" size="xl">
|
<Text tt="uppercase" fw="bolder" size="xl">
|
||||||
{generalForm.values.username}
|
{generalForm.values.username}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default async function GroupsDetailPage(props: GroupsDetailPageProps) {
|
|||||||
<Card>
|
<Card>
|
||||||
{group.owner ? (
|
{group.owner ? (
|
||||||
<Group>
|
<Group>
|
||||||
<UserAvatar user={{ name: group.owner.name, image: group.owner.image }} size={"lg"} />
|
<UserAvatar user={group.owner} size={"lg"} />
|
||||||
<Stack align={"start"} gap={3}>
|
<Stack align={"start"} gap={3}>
|
||||||
<Text fw={"bold"}>{group.owner.name}</Text>
|
<Text fw={"bold"}>{group.owner.name}</Text>
|
||||||
<Text>{group.owner.email}</Text>
|
<Text>{group.owner.email}</Text>
|
||||||
|
|||||||
@@ -3,21 +3,24 @@ import { userAgent } from "next/server";
|
|||||||
import { createOpenApiFetchHandler } from "trpc-to-openapi";
|
import { createOpenApiFetchHandler } from "trpc-to-openapi";
|
||||||
|
|
||||||
import { appRouter, createTRPCContext } from "@homarr/api";
|
import { appRouter, createTRPCContext } from "@homarr/api";
|
||||||
import type { Session } from "@homarr/auth";
|
import { API_KEY_HEADER_NAME, getSessionFromApiKeyAsync } from "@homarr/auth/api-key";
|
||||||
import { hashPasswordAsync } from "@homarr/auth";
|
import { ipAddressFromHeaders } from "@homarr/common/server";
|
||||||
import { createSessionAsync } from "@homarr/auth/server";
|
|
||||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
import { db, eq } from "@homarr/db";
|
import { db } from "@homarr/db";
|
||||||
import { apiKeys } from "@homarr/db/schema";
|
|
||||||
|
|
||||||
const logger = createLogger({ module: "trpcOpenApiRoute" });
|
const logger = createLogger({ module: "trpcOpenApiRoute" });
|
||||||
|
|
||||||
const handlerAsync = async (req: NextRequest) => {
|
const handlerAsync = async (req: NextRequest) => {
|
||||||
const apiKeyHeaderValue = req.headers.get("ApiKey");
|
const apiKeyHeaderValue = req.headers.get(API_KEY_HEADER_NAME);
|
||||||
const ipAddress = req.headers.get("x-forwarded-for");
|
const ipAddress = ipAddressFromHeaders(req.headers);
|
||||||
const { ua } = userAgent(req);
|
const { ua } = userAgent(req);
|
||||||
const session: Session | null = await getSessionOrDefaultFromHeadersAsync(apiKeyHeaderValue, ipAddress, ua);
|
|
||||||
|
logger.info(
|
||||||
|
`Creating OpenAPI fetch handler for user ${apiKeyHeaderValue ? "with an api key" : "without an api key"}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const session = await getSessionFromApiKeyAsync(db, apiKeyHeaderValue, ipAddress, ua);
|
||||||
|
|
||||||
// Fallback to JSON if no content type is set
|
// Fallback to JSON if no content type is set
|
||||||
if (!req.headers.has("Content-Type")) {
|
if (!req.headers.has("Content-Type")) {
|
||||||
@@ -35,67 +38,6 @@ const handlerAsync = async (req: NextRequest) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSessionOrDefaultFromHeadersAsync = async (
|
|
||||||
apiKeyHeaderValue: string | null,
|
|
||||||
ipAdress: string | null,
|
|
||||||
userAgent: string,
|
|
||||||
): Promise<Session | null> => {
|
|
||||||
logger.info(
|
|
||||||
`Creating OpenAPI fetch handler for user ${apiKeyHeaderValue ? "with an api key" : "without an api key"}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (apiKeyHeaderValue === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [apiKeyId, apiKey] = apiKeyHeaderValue.split(".");
|
|
||||||
|
|
||||||
if (!apiKeyId || !apiKey) {
|
|
||||||
logger.warn("An attempt to authenticate over API has failed due to invalid API key format", {
|
|
||||||
ipAdress,
|
|
||||||
userAgent,
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiKeyFromDb = await db.query.apiKeys.findFirst({
|
|
||||||
where: eq(apiKeys.id, apiKeyId),
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
apiKey: true,
|
|
||||||
salt: true,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
user: {
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
emailVerified: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!apiKeyFromDb) {
|
|
||||||
logger.warn("An attempt to authenticate over API has failed", { ipAdress, userAgent });
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashedApiKey = await hashPasswordAsync(apiKey, apiKeyFromDb.salt);
|
|
||||||
|
|
||||||
if (apiKeyFromDb.apiKey !== hashedApiKey) {
|
|
||||||
logger.warn("An attempt to authenticate over API has failed", { ipAdress, userAgent });
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Read session from API request and found user", {
|
|
||||||
name: apiKeyFromDb.user.name,
|
|
||||||
id: apiKeyFromDb.user.id,
|
|
||||||
});
|
|
||||||
return await createSessionAsync(db, apiKeyFromDb.user);
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
handlerAsync as DELETE,
|
handlerAsync as DELETE,
|
||||||
handlerAsync as GET,
|
handlerAsync as GET,
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
|
import { userAgent } from "next/server";
|
||||||
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||||
|
|
||||||
import { appRouter, createTRPCContext } from "@homarr/api";
|
import { appRouter, createTRPCContext } from "@homarr/api";
|
||||||
import { trpcPath } from "@homarr/api/shared";
|
import { trpcPath } from "@homarr/api/shared";
|
||||||
|
import { API_KEY_HEADER_NAME, getSessionFromApiKeyAsync } from "@homarr/auth/api-key";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
|
import { ipAddressFromHeaders } from "@homarr/common/server";
|
||||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
|
import { db } from "@homarr/db";
|
||||||
|
|
||||||
const logger = createLogger({ module: "trpcRoute" });
|
const logger = createLogger({ module: "trpcRoute" });
|
||||||
|
|
||||||
@@ -28,11 +32,20 @@ export function OPTIONS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handler = auth(async (req) => {
|
const handler = auth(async (req) => {
|
||||||
|
// Try API key auth first, fall back to session cookie
|
||||||
|
const apiKeyHeader = req.headers.get(API_KEY_HEADER_NAME);
|
||||||
|
const ipAddress = ipAddressFromHeaders(req.headers);
|
||||||
|
|
||||||
|
const { ua } = userAgent(req);
|
||||||
|
|
||||||
|
const apiKeySession = await getSessionFromApiKeyAsync(db, apiKeyHeader, ipAddress, ua);
|
||||||
|
const session = apiKeySession ?? req.auth;
|
||||||
|
|
||||||
const response = await fetchRequestHandler({
|
const response = await fetchRequestHandler({
|
||||||
endpoint: trpcPath,
|
endpoint: trpcPath,
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
req,
|
req,
|
||||||
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
|
createContext: () => createTRPCContext({ session, headers: req.headers }),
|
||||||
onError({ error, path, type }) {
|
onError({ error, path, type }) {
|
||||||
logger.error(new ErrorWithMetadata("tRPC Error occured", { path, type }, { cause: error }));
|
logger.error(new ErrorWithMetadata("tRPC Error occured", { path, type }, { cause: error }));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface UserAccessPermission<TPermission extends string> {
|
|||||||
user: {
|
user: {
|
||||||
name: string | null;
|
name: string | null;
|
||||||
image: string | null;
|
image: string | null;
|
||||||
|
email: string | null;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -66,6 +67,7 @@ interface Props<TPermission extends string> {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
image: string | null;
|
image: string | null;
|
||||||
|
email: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
translate: (key: TPermission) => string;
|
translate: (key: TPermission) => string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
import type { TablerIcon } from "@tabler/icons-react";
|
|
||||||
|
import type { TablerIcon } from "@homarr/ui";
|
||||||
|
|
||||||
const AccessContext = createContext<{
|
const AccessContext = createContext<{
|
||||||
permissions: readonly string[];
|
permissions: readonly string[];
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface FormProps<TPermission extends string> {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
image: string | null;
|
image: string | null;
|
||||||
|
email: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
accessQueryData: AccessQueryData<TPermission>;
|
accessQueryData: AccessQueryData<TPermission>;
|
||||||
@@ -118,6 +119,7 @@ interface UserItemContentProps {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
image: string | null;
|
image: string | null;
|
||||||
|
email: string | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { UserAvatar } from "@homarr/ui";
|
|||||||
interface InnerProps {
|
interface InnerProps {
|
||||||
presentUserIds: string[];
|
presentUserIds: string[];
|
||||||
excludeExternalProviders?: boolean;
|
excludeExternalProviders?: boolean;
|
||||||
onSelect: (props: { id: string; name: string; image: string }) => void | Promise<void>;
|
onSelect: (props: { id: string; name: string; image: string; email: string | null }) => void | Promise<void>;
|
||||||
confirmLabel?: string;
|
confirmLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ export const UserSelectModal = createModal<InnerProps>(({ actions, innerProps })
|
|||||||
id: currentUser.id,
|
id: currentUser.id,
|
||||||
name: currentUser.name ?? "",
|
name: currentUser.name ?? "",
|
||||||
image: currentUser.image ?? "",
|
image: currentUser.image ?? "",
|
||||||
|
email: currentUser.email ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export class BoardMockBuilder {
|
|||||||
id: createId(),
|
id: createId(),
|
||||||
image: null,
|
image: null,
|
||||||
name: "User",
|
name: "User",
|
||||||
|
email: null,
|
||||||
},
|
},
|
||||||
groupPermissions: [],
|
groupPermissions: [],
|
||||||
userPermissions: [],
|
userPermissions: [],
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Anchor, Card, Stack, Text } from "@mantine/core";
|
import { Anchor, Card, Stack, Text } from "@mantine/core";
|
||||||
import type { TablerIcon } from "@tabler/icons-react";
|
|
||||||
|
import type { TablerIcon } from "@homarr/ui";
|
||||||
|
|
||||||
interface NoResultsProps {
|
interface NoResultsProps {
|
||||||
icon: TablerIcon;
|
icon: TablerIcon;
|
||||||
|
|||||||
@@ -3,17 +3,21 @@ import type { MantineSize } from "@mantine/core";
|
|||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { UserAvatar } from "@homarr/ui";
|
import { UserAvatar } from "@homarr/ui";
|
||||||
|
|
||||||
interface UserAvatarProps {
|
interface CurrentUserAvatarProps {
|
||||||
size: MantineSize;
|
size: MantineSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CurrentUserAvatar = async ({ size }: UserAvatarProps) => {
|
export const CurrentUserAvatar = async ({ size }: CurrentUserAvatarProps) => {
|
||||||
const currentSession = await auth();
|
const currentSession = await auth();
|
||||||
|
|
||||||
const user = {
|
return (
|
||||||
name: currentSession?.user.name ?? null,
|
<UserAvatar
|
||||||
image: currentSession?.user.image ?? null,
|
user={{
|
||||||
};
|
name: currentSession?.user.name ?? null,
|
||||||
|
image: currentSession?.user.image ?? null,
|
||||||
return <UserAvatar user={user} size={size} />;
|
email: currentSession?.user.email ?? null,
|
||||||
|
}}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
@@ -41,15 +41,15 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"fastify": "^5.6.2",
|
"fastify": "^5.6.2",
|
||||||
"superjson": "2.2.6",
|
"superjson": "2.2.6",
|
||||||
"undici": "7.16.0"
|
"undici": "7.18.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node": "^24.10.4",
|
"@types/node": "^24.10.7",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"esbuild": "^0.27.1",
|
"esbuild": "^0.27.2",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"tsx": "4.20.4",
|
"tsx": "4.20.4",
|
||||||
|
|||||||
@@ -27,14 +27,14 @@
|
|||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"tsx": "4.20.4",
|
"tsx": "4.20.4",
|
||||||
"ws": "^8.18.3"
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"esbuild": "^0.27.1",
|
"esbuild": "^0.27.2",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^12.5.0"
|
"better-sqlite3": "^12.5.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.25.0",
|
"packageManager": "pnpm@10.28.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.12.0",
|
"node": ">=24.12.0",
|
||||||
"pnpm": ">=10.25.0"
|
"pnpm": ">=10.28.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|||||||
@@ -314,6 +314,13 @@
|
|||||||
<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>
|
||||||
|
|||||||
+17
-17
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "1.47.0",
|
"version": "1.50.1",
|
||||||
"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.10.0",
|
"@testcontainers/redis": "^11.11.0",
|
||||||
"@turbo/gen": "^2.6.3",
|
"@turbo/gen": "^2.7.3",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
"@vitest/coverage-v8": "^4.0.15",
|
"@vitest/coverage-v8": "^4.0.16",
|
||||||
"@vitest/ui": "^4.0.15",
|
"@vitest/ui": "^4.0.16",
|
||||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"jsdom": "^27.3.0",
|
"jsdom": "^27.4.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.10.0",
|
"testcontainers": "^11.11.0",
|
||||||
"turbo": "^2.6.3",
|
"turbo": "^2.7.3",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^6.0.4",
|
||||||
"vitest": "^4.0.15"
|
"vitest": "^4.0.16"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.25.0",
|
"packageManager": "pnpm@10.28.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.12.0",
|
"node": ">=24.12.0",
|
||||||
"pnpm": ">=10.25.0"
|
"pnpm": ">=10.28.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.1",
|
"esbuild@<=0.24.2": ">=0.27.2",
|
||||||
"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.0",
|
"hono@<4.6.5": ">=4.11.4",
|
||||||
"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.18.2",
|
||||||
"react-is": "^19.2.3",
|
"react-is": "^19.2.3",
|
||||||
"rollup@>=4.0.0 <4.22.4": ">=4.53.3",
|
"rollup@>=4.0.0 <4.22.4": ">=4.55.1",
|
||||||
"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.2.7"
|
"vite@>=5.0.0 <=5.4.18": ">=7.3.1"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@types/node-unifi": "patches/@types__node-unifi.patch",
|
"@types/node-unifi": "patches/@types__node-unifi.patch",
|
||||||
|
|||||||
@@ -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.12",
|
"@tanstack/react-query": "^5.90.16",
|
||||||
"@trpc/client": "^11.8.0",
|
"@trpc/client": "^11.8.1",
|
||||||
"@trpc/react-query": "^11.8.0",
|
"@trpc/react-query": "^11.8.1",
|
||||||
"@trpc/server": "^11.8.0",
|
"@trpc/server": "^11.8.1",
|
||||||
"@trpc/tanstack-react-query": "^11.8.0",
|
"@trpc/tanstack-react-query": "^11.8.1",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"next": "16.0.10",
|
"next": "16.1.1",
|
||||||
"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.1.13"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { generateOpenApiDocument } from "trpc-to-openapi";
|
import { generateOpenApiDocument } from "trpc-to-openapi";
|
||||||
|
|
||||||
|
import { API_KEY_HEADER_NAME } from "@homarr/auth/api-key";
|
||||||
|
|
||||||
import { appRouter } from "./root";
|
import { appRouter } from "./root";
|
||||||
|
|
||||||
export const openApiDocument = (base: string) =>
|
export const openApiDocument = (base: string) =>
|
||||||
@@ -11,7 +13,7 @@ export const openApiDocument = (base: string) =>
|
|||||||
securitySchemes: {
|
securitySchemes: {
|
||||||
apikey: {
|
apikey: {
|
||||||
type: "apiKey",
|
type: "apiKey",
|
||||||
name: "ApiKey",
|
name: API_KEY_HEADER_NAME,
|
||||||
description: "API key which can be obtained in the Homarr administration dashboard",
|
description: "API key which can be obtained in the Homarr administration dashboard",
|
||||||
in: "header",
|
in: "header",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const apiKeysRouter = createTRPCRouter({
|
|||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
image: true,
|
image: true,
|
||||||
|
email: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
image: true,
|
image: true,
|
||||||
|
email: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
userPermissions: {
|
userPermissions: {
|
||||||
@@ -1195,6 +1196,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
image: true,
|
image: true,
|
||||||
|
email: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1537,6 +1539,7 @@ const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, use
|
|||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
image: true,
|
image: true,
|
||||||
|
email: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sections: {
|
sections: {
|
||||||
|
|||||||
@@ -476,6 +476,7 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
image: true,
|
image: true,
|
||||||
|
email: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||||
|
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||||
|
|
||||||
@@ -36,7 +37,12 @@ export const locationRouter = createTRPCRouter({
|
|||||||
.input(locationSearchCityInput)
|
.input(locationSearchCityInput)
|
||||||
.output(locationSearchCityOutput)
|
.output(locationSearchCityOutput)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const res = await fetchWithTimeoutAsync(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`);
|
const res = await withTimeoutAsync(async (signal) => {
|
||||||
|
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>;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -39,6 +39,7 @@ export const mediaRouter = createTRPCRouter({
|
|||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
image: true,
|
image: true,
|
||||||
|
email: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1220,11 +1220,11 @@ describe("getBoardPermissions should return board permissions", () => {
|
|||||||
expect(result.users).toEqual(
|
expect(result.users).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
{
|
{
|
||||||
user: { id: user1, name: null, image: null },
|
user: { id: user1, name: null, image: null, email: null },
|
||||||
permission: "view",
|
permission: "view",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
user: { id: user2, name: null, image: null },
|
user: { id: user2, name: null, image: null, email: null },
|
||||||
permission: "modify",
|
permission: "modify",
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
// Is protected because also used in board access / integration access forms
|
// Is protected because also used in board access / integration access forms
|
||||||
selectable: protectedProcedure
|
selectable: protectedProcedure
|
||||||
.input(z.object({ excludeExternalProviders: z.boolean().default(false) }).optional())
|
.input(z.object({ excludeExternalProviders: z.boolean().default(false) }).optional())
|
||||||
.output(z.array(selectUserSchema.pick({ id: true, name: true, image: true })))
|
.output(z.array(selectUserSchema.pick({ id: true, name: true, image: true, email: true })))
|
||||||
.meta({ openapi: { method: "GET", path: "/api/users/selectable", tags: ["users"], protect: true } })
|
.meta({ openapi: { method: "GET", path: "/api/users/selectable", tags: ["users"], protect: true } })
|
||||||
.query(({ ctx, input }) => {
|
.query(({ ctx, input }) => {
|
||||||
return ctx.db.query.users.findMany({
|
return ctx.db.query.users.findMany({
|
||||||
@@ -182,6 +182,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
image: true,
|
image: true,
|
||||||
|
email: true,
|
||||||
},
|
},
|
||||||
where: input?.excludeExternalProviders ? eq(users.provider, "credentials") : undefined,
|
where: input?.excludeExternalProviders ? eq(users.provider, "credentials") : undefined,
|
||||||
});
|
});
|
||||||
@@ -194,7 +195,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
limit: z.number().min(1).max(100).default(10),
|
limit: z.number().min(1).max(100).default(10),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.array(selectUserSchema.pick({ id: true, name: true, image: true })))
|
.output(z.array(selectUserSchema.pick({ id: true, name: true, image: true, email: true })))
|
||||||
.meta({ openapi: { method: "POST", path: "/api/users/search", tags: ["users"], protect: true } })
|
.meta({ openapi: { method: "POST", path: "/api/users/search", tags: ["users"], protect: true } })
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const dbUsers = await ctx.db.query.users.findMany({
|
const dbUsers = await ctx.db.query.users.findMany({
|
||||||
@@ -202,6 +203,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
image: true,
|
image: true,
|
||||||
|
email: true,
|
||||||
},
|
},
|
||||||
where: like(users.name, `%${input.query}%`),
|
where: like(users.name, `%${input.query}%`),
|
||||||
limit: input.limit,
|
limit: input.limit,
|
||||||
@@ -210,6 +212,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name ?? "",
|
name: user.name ?? "",
|
||||||
image: user.image,
|
image: user.image,
|
||||||
|
email: user.email,
|
||||||
}));
|
}));
|
||||||
}),
|
}),
|
||||||
getById: protectedProcedure
|
getById: protectedProcedure
|
||||||
|
|||||||
@@ -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", "mock"))
|
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "unraid", "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", "mock"))
|
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "unraid", "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)[] = [];
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const notebookRouter = createTRPCRouter({
|
|||||||
where: eq(items.id, input.itemId),
|
where: eq(items.id, input.itemId),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!item || item.boardId !== input.boardId) {
|
if (item?.boardId !== input.boardId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "Specified item was not found",
|
message: "Specified item was not found",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const API_KEY_HEADER_NAME = "ApiKey";
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import type { Session } from "next-auth";
|
||||||
|
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import type { Database } from "@homarr/db";
|
||||||
|
import { eq } from "@homarr/db";
|
||||||
|
import { apiKeys } from "@homarr/db/schema";
|
||||||
|
|
||||||
|
import { hashPasswordAsync } from "../security";
|
||||||
|
import { createSessionAsync } from "../server";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "apiKeyAuth" });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate an API key from the request header and return a session if valid.
|
||||||
|
*
|
||||||
|
* @param db - The database instance
|
||||||
|
* @param apiKeyHeaderValue - The value of the ApiKey header (format: "id.token")
|
||||||
|
* @param ipAddress - The IP address of the request (for logging)
|
||||||
|
* @param userAgent - The user agent of the request (for logging)
|
||||||
|
* @returns A session if the API key is valid, null otherwise
|
||||||
|
*/
|
||||||
|
export const getSessionFromApiKeyAsync = async (
|
||||||
|
db: Database,
|
||||||
|
apiKeyHeaderValue: string | null,
|
||||||
|
ipAddress: string | null,
|
||||||
|
userAgent: string,
|
||||||
|
): Promise<Session | null> => {
|
||||||
|
if (apiKeyHeaderValue === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [apiKeyId, apiKey] = apiKeyHeaderValue.split(".");
|
||||||
|
|
||||||
|
if (!apiKeyId || !apiKey) {
|
||||||
|
logger.warn("Failed to authenticate with api-key", { ipAddress, userAgent, reason: "API_KEY_INVALID_FORMAT" });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKeyFromDb = await db.query.apiKeys.findFirst({
|
||||||
|
where: eq(apiKeys.id, apiKeyId),
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
apiKey: true,
|
||||||
|
salt: true,
|
||||||
|
},
|
||||||
|
with: {
|
||||||
|
user: {
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
emailVerified: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!apiKeyFromDb) {
|
||||||
|
logger.warn("Failed to authenticate with api-key", { ipAddress, userAgent, reason: "API_KEY_NOT_FOUND" });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedApiKey = await hashPasswordAsync(apiKey, apiKeyFromDb.salt);
|
||||||
|
|
||||||
|
if (apiKeyFromDb.apiKey !== hashedApiKey) {
|
||||||
|
logger.warn("Failed to authenticate with api-key", { ipAddress, userAgent, reason: "API_KEY_MISMATCH" });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Successfully authenticated with api-key", {
|
||||||
|
name: apiKeyFromDb.user.name,
|
||||||
|
id: apiKeyFromDb.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await createSessionAsync(db, apiKeyFromDb.user);
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { getSessionFromApiKeyAsync } from "./get-api-key-session";
|
||||||
|
export { API_KEY_HEADER_NAME } from "./constants";
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import { createId } from "@homarr/common";
|
||||||
|
import { apiKeys, users } from "@homarr/db/schema";
|
||||||
|
import { createDb } from "@homarr/db/test";
|
||||||
|
|
||||||
|
import { createSaltAsync, hashPasswordAsync } from "../../security";
|
||||||
|
import { getSessionFromApiKeyAsync } from "../get-api-key-session";
|
||||||
|
|
||||||
|
// Mock the logger to avoid console output during tests
|
||||||
|
vi.mock("@homarr/core/infrastructure/logs", () => ({
|
||||||
|
createLogger: () => ({
|
||||||
|
warn: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const defaultUserId = createId();
|
||||||
|
const defaultUsername = "testuser";
|
||||||
|
const defaultApiKeyId = createId();
|
||||||
|
const defaultIpAddress = "127.0.0.1";
|
||||||
|
const defaultUserAgent = "test-agent";
|
||||||
|
const defaultLogParams = [defaultIpAddress, defaultUserAgent] as const;
|
||||||
|
|
||||||
|
describe("getSessionFromApiKeyAsync", () => {
|
||||||
|
test("should return null when api key header is null", async () => {
|
||||||
|
// Arrange
|
||||||
|
const { db } = await setupAsync();
|
||||||
|
const apiKey = null;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await getSessionFromApiKeyAsync(db, apiKey, ...defaultLogParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
["invalidformat", "no dot"],
|
||||||
|
["keyid.", "missing token"],
|
||||||
|
[".token", "missing id"],
|
||||||
|
])("should return null when api key format is invalid key=%s reason=%s", async (apiKey) => {
|
||||||
|
// Arrange
|
||||||
|
const { db } = await setupAsync();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await getSessionFromApiKeyAsync(db, apiKey, ...defaultLogParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return null when api key is not found in database", async () => {
|
||||||
|
// Arrange
|
||||||
|
const { db } = await setupAsync();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await getSessionFromApiKeyAsync(db, "nonexistent.token", ...defaultLogParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return null when api key token does not match", async () => {
|
||||||
|
// Arrange
|
||||||
|
const { db } = await setupAsync({ token: "correcttoken" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await getSessionFromApiKeyAsync(db, `${defaultApiKeyId}.wrongtoken`, ...defaultLogParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return session when api key is valid", async () => {
|
||||||
|
// Arrange
|
||||||
|
const token = "validtesttoken123";
|
||||||
|
const { db } = await setupAsync({ token });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await getSessionFromApiKeyAsync(db, `${defaultApiKeyId}.${token}`, ...defaultLogParams);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.user.id).toEqual(defaultUserId);
|
||||||
|
expect(result!.user.name).toEqual(defaultUsername);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should work with null ip address", async () => {
|
||||||
|
// Arrange
|
||||||
|
const token = "validtesttoken456";
|
||||||
|
const { db } = await setupAsync({ token });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await getSessionFromApiKeyAsync(db, `${defaultApiKeyId}.${token}`, null, defaultUserAgent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.user.id).toEqual(defaultUserId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SetupOptions {
|
||||||
|
/**
|
||||||
|
* If provided, inserts an API key into the database for testing.
|
||||||
|
*/
|
||||||
|
token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupAsync = async (options?: SetupOptions) => {
|
||||||
|
const db = createDb();
|
||||||
|
|
||||||
|
await db.insert(users).values({
|
||||||
|
id: defaultUserId,
|
||||||
|
name: defaultUsername,
|
||||||
|
email: "test@example.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options?.token) {
|
||||||
|
const salt = await createSaltAsync();
|
||||||
|
await db.insert(apiKeys).values({
|
||||||
|
id: defaultApiKeyId,
|
||||||
|
apiKey: await hashPasswordAsync(options.token, salt),
|
||||||
|
salt,
|
||||||
|
userId: defaultUserId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
db,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
".": "./index.ts",
|
".": "./index.ts",
|
||||||
"./next": "./next.ts",
|
"./next": "./next.ts",
|
||||||
"./security": "./security.ts",
|
"./security": "./security.ts",
|
||||||
|
"./api-key": "./api-key/index.ts",
|
||||||
"./client": "./client.ts",
|
"./client": "./client.ts",
|
||||||
"./server": "./server.ts",
|
"./server": "./server.ts",
|
||||||
"./shared": "./shared.ts",
|
"./shared": "./shared.ts",
|
||||||
@@ -32,12 +33,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.23",
|
"ldapts": "8.1.3",
|
||||||
"next": "16.0.10",
|
"next": "16.1.1",
|
||||||
"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.1.13"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -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.1",
|
"esbuild": "^0.27.2",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.0.10",
|
"next": "16.1.1",
|
||||||
"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.18.2",
|
||||||
"zod": "^4.1.13",
|
"zod": "^4.2.1",
|
||||||
"zod-validation-error": "^5.0.0"
|
"zod-validation-error": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -9,3 +9,7 @@ export const userAgent = (headers: Headers) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DeviceType = "console" | "mobile" | "tablet" | "smarttv" | "wearable" | "embedded" | undefined;
|
export type DeviceType = "console" | "mobile" | "tablet" | "smarttv" | "wearable" | "embedded" | undefined;
|
||||||
|
|
||||||
|
export const ipAddressFromHeaders = (headers: Headers): string | null => {
|
||||||
|
return headers.get("x-forwarded-for");
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export * from "./security";
|
export * from "./security";
|
||||||
export * from "./encryption";
|
export * from "./encryption";
|
||||||
export * from "./user-agent";
|
export * from "./request";
|
||||||
export * from "./errors";
|
export * from "./errors";
|
||||||
@@ -37,16 +37,16 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@t3-oss/env-nextjs": "^0.13.8",
|
"@t3-oss/env-nextjs": "^0.13.10",
|
||||||
"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.9.1",
|
||||||
"mysql2": "3.15.3",
|
"mysql2": "3.16.0",
|
||||||
"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.1.13"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ const createMysqlDbConnection = () => {
|
|||||||
|
|
||||||
return mysql.createPool({
|
return mysql.createPool({
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
|
host: dbEnv.HOST,
|
||||||
port: dbEnv.PORT,
|
port: dbEnv.PORT,
|
||||||
|
database: dbEnv.NAME,
|
||||||
user: dbEnv.USER,
|
user: dbEnv.USER,
|
||||||
password: dbEnv.PASSWORD,
|
password: dbEnv.PASSWORD,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createBooleanSchema, createEnv } from "../env";
|
|||||||
|
|
||||||
export const dnsEnv = createEnv({
|
export const dnsEnv = createEnv({
|
||||||
server: {
|
server: {
|
||||||
ENABLE_DNS_CACHING: createBooleanSchema(false),
|
ENABLE_DNS_CACHING: createBooleanSchema(true),
|
||||||
},
|
},
|
||||||
experimental__runtimeEnv: process.env,
|
experimental__runtimeEnv: process.env,
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Dispatcher } from "undici";
|
import type { Dispatcher } from "undici";
|
||||||
import { Agent } from "undici";
|
import { EnvHttpProxyAgent } 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,16 +7,15 @@ 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 Agent.Options {
|
interface HttpAgentOptions extends EnvHttpProxyAgent.Options {
|
||||||
logger?: ILogger;
|
logger?: ILogger;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UndiciHttpAgent extends Agent {
|
export class UndiciHttpAgent extends EnvHttpProxyAgent {
|
||||||
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,12 +42,13 @@ 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({
|
||||||
override ?? {
|
ca: await getAllTrustedCertificatesAsync(),
|
||||||
ca: await getAllTrustedCertificatesAsync(),
|
checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()),
|
||||||
checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()),
|
// Override the ca and checkServerIdentity if provided
|
||||||
},
|
...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 {
|
||||||
Agent: class Agent {
|
EnvHttpProxyAgent: class EnvHttpProxyAgent {
|
||||||
dispatch(_options: Dispatcher.DispatchOptions, _handler: Dispatcher.DispatchHandler): boolean {
|
dispatch(_options: Dispatcher.DispatchOptions, _handler: Dispatcher.DispatchHandler): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,20 +28,20 @@
|
|||||||
"@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.12",
|
"@tanstack/react-query": "^5.90.16",
|
||||||
"@trpc/client": "^11.8.0",
|
"@trpc/client": "^11.8.1",
|
||||||
"@trpc/server": "^11.8.0",
|
"@trpc/server": "^11.8.1",
|
||||||
"@trpc/tanstack-react-query": "^11.8.0",
|
"@trpc/tanstack-react-query": "^11.8.1",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/react": "19.2.7",
|
"@types/react": "19.2.8",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,16 +47,16 @@
|
|||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.10",
|
"@mantine/core": "^8.3.12",
|
||||||
"@paralleldrive/cuid2": "^3.1.0",
|
"@paralleldrive/cuid2": "^3.1.0",
|
||||||
"@testcontainers/mysql": "^11.10.0",
|
"@testcontainers/mysql": "^11.11.0",
|
||||||
"@testcontainers/postgresql": "^11.10.0",
|
"@testcontainers/postgresql": "^11.11.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.15.3",
|
"mysql2": "3.16.0",
|
||||||
"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.1",
|
"esbuild": "^0.27.2",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"tsx": "4.20.4",
|
"tsx": "4.20.4",
|
||||||
|
|||||||
@@ -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.1.13"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/integrations/transmission"
|
| "/docs/integrations/transmission"
|
||||||
| "/docs/integrations/truenas"
|
| "/docs/integrations/truenas"
|
||||||
| "/docs/integrations/unifi-controller"
|
| "/docs/integrations/unifi-controller"
|
||||||
|
| "/docs/integrations/unraid"
|
||||||
| "/docs/management/api"
|
| "/docs/management/api"
|
||||||
| "/docs/management/apps"
|
| "/docs/management/apps"
|
||||||
| "/docs/management/boards"
|
| "/docs/management/boards"
|
||||||
|
|||||||
@@ -298,6 +298,13 @@ 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",
|
||||||
|
|||||||
@@ -26,9 +26,9 @@
|
|||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/form": "^8.3.10",
|
"@mantine/form": "^8.3.12",
|
||||||
"mantine-form-zod-resolver": "^1.3.0",
|
"mantine-form-zod-resolver": "^1.3.0",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -30,9 +30,9 @@
|
|||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.10",
|
"@mantine/core": "^8.3.12",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { parse } from "path";
|
import { parse } from "path";
|
||||||
|
|
||||||
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||||
|
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";
|
||||||
@@ -19,11 +20,12 @@ export class GitHubIconRepository extends IconRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
|
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
|
||||||
if (!this.repositoryIndexingUrl || !this.repositoryBlobUrlTemplate) {
|
const url = this.repositoryIndexingUrl;
|
||||||
|
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 fetchWithTimeoutAsync(this.repositoryIndexingUrl);
|
const response = await withTimeoutAsync(async (signal) => fetchWithTrustedCertificatesAsync(url, { signal }));
|
||||||
const listOfFiles = (await response.json()) as GitHubApiResponse;
|
const listOfFiles = (await response.json()) as GitHubApiResponse;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { parse } from "path";
|
import { parse } from "path";
|
||||||
|
|
||||||
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||||
|
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";
|
||||||
@@ -19,7 +20,9 @@ export class JsdelivrIconRepository extends IconRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
|
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
|
||||||
const response = await fetchWithTimeoutAsync(this.repositoryIndexingUrl);
|
const response = await withTimeoutAsync(async (signal) =>
|
||||||
|
fetchWithTrustedCertificatesAsync(this.repositoryIndexingUrl, { signal }),
|
||||||
|
);
|
||||||
const listOfFiles = (await response.json()) as JsdelivrApiResponse;
|
const listOfFiles = (await response.json()) as JsdelivrApiResponse;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -46,9 +46,9 @@
|
|||||||
"octokit": "^5.0.5",
|
"octokit": "^5.0.5",
|
||||||
"proxmox-api": "1.1.1",
|
"proxmox-api": "1.1.1",
|
||||||
"tsdav": "^2.1.6",
|
"tsdav": "^2.1.6",
|
||||||
"undici": "7.16.0",
|
"undici": "7.18.2",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ 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>(
|
||||||
@@ -101,6 +102,7 @@ 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> = {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ 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";
|
||||||
|
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni
|
|||||||
*/
|
*/
|
||||||
private async requestAsync(method: string, params: unknown[] = [], webSocketOverride?: WebSocket) {
|
private async requestAsync(method: string, params: unknown[] = [], webSocketOverride?: WebSocket) {
|
||||||
let webSocket = webSocketOverride ?? this.webSocket;
|
let webSocket = webSocketOverride ?? this.webSocket;
|
||||||
if (!webSocket || webSocket.readyState !== WebSocket.OPEN) {
|
if (webSocket?.readyState !== WebSocket.OPEN) {
|
||||||
logger.debug("Connecting to websocket", {
|
logger.debug("Connecting to websocket", {
|
||||||
url: this.wsUrl(),
|
url: this.wsUrl(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
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 ?? null,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
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().nullish(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
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>;
|
||||||
@@ -33,13 +33,13 @@
|
|||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.10",
|
"@mantine/core": "^8.3.12",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.36.1",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"next": "16.0.10",
|
"next": "16.1.1",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.10",
|
"@mantine/core": "^8.3.12",
|
||||||
"@mantine/hooks": "^8.3.10",
|
"@mantine/hooks": "^8.3.12",
|
||||||
"react": "19.2.3"
|
"react": "19.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/notifications": "^8.3.10",
|
"@mantine/notifications": "^8.3.12",
|
||||||
"@tabler/icons-react": "^3.35.0"
|
"@tabler/icons-react": "^3.36.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -37,14 +37,14 @@
|
|||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.10",
|
"@mantine/core": "^8.3.12",
|
||||||
"@mantine/hooks": "^8.3.10",
|
"@mantine/hooks": "^8.3.12",
|
||||||
"adm-zip": "0.5.16",
|
"adm-zip": "0.5.16",
|
||||||
"next": "16.0.10",
|
"next": "16.1.1",
|
||||||
"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.1.13",
|
"zod": "^4.2.1",
|
||||||
"zod-form-data": "^3.0.1"
|
"zod-form-data": "^3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -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.1.13"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"@homarr/core": "workspace:^",
|
"@homarr/core": "workspace:^",
|
||||||
"@homarr/db": "workspace:^",
|
"@homarr/db": "workspace:^",
|
||||||
"@homarr/definitions": "workspace:^",
|
"@homarr/definitions": "workspace:^",
|
||||||
"ioredis": "5.8.2",
|
"ioredis": "5.9.1",
|
||||||
"superjson": "2.2.6"
|
"superjson": "2.2.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"octokit": "^5.0.5",
|
"octokit": "^5.0.5",
|
||||||
"superjson": "2.2.6",
|
"superjson": "2.2.6",
|
||||||
"undici": "7.16.0",
|
"undici": "7.18.2",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ export const dockerContainersRequestHandler = createCachedWidgetRequestHandler({
|
|||||||
queryKey: "dockerContainersResult",
|
queryKey: "dockerContainersResult",
|
||||||
widgetKind: "dockerContainers",
|
widgetKind: "dockerContainers",
|
||||||
async requestAsync() {
|
async requestAsync() {
|
||||||
const containers = await getContainersWithStatsAsync();
|
return await getContainersWithStatsAsync();
|
||||||
|
|
||||||
return containers;
|
|
||||||
},
|
},
|
||||||
cacheDuration: dayjs.duration(20, "seconds"),
|
cacheDuration: dayjs.duration(20, "seconds"),
|
||||||
});
|
});
|
||||||
@@ -28,7 +26,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 === false)
|
.filter((container) => !(dockerLabels.hide in container.Labels))
|
||||||
.map((container) => ({ ...container, instance: host }));
|
.map((container) => ({ ...container, instance: host }));
|
||||||
}),
|
}),
|
||||||
).then((res) => res.flat());
|
).then((res) => res.flat());
|
||||||
@@ -45,7 +43,21 @@ 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;
|
||||||
|
|
||||||
const stats = await instance.getContainer(container.Id).stats({ stream: false, "one-shot": true });
|
// Get stats, falling back to an empty stats object if fetch fails
|
||||||
|
// 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,
|
||||||
@@ -57,19 +69,8 @@ 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: calculateCpuUsage(stats),
|
cpuUsage,
|
||||||
// memory usage by default includes cache, which should not be shown as it is also not shown with docker stats command
|
memoryUsage,
|
||||||
// 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,
|
||||||
};
|
};
|
||||||
@@ -79,10 +80,36 @@ async function getContainersWithStatsAsync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function calculateCpuUsage(stats: ContainerStats): number {
|
function calculateCpuUsage(stats: ContainerStats): number {
|
||||||
const numberOfCpus = stats.cpu_stats.online_cpus || 1;
|
// Handle containers with missing or invalid stats (e.g., exited, dead containers)
|
||||||
|
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 === 0) return 0;
|
if (!usage || usage === 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,6 @@ export const createRequestIntegrationJobHandler = <
|
|||||||
reduceWidgetOptionsWithDefaultValues(
|
reduceWidgetOptionsWithDefaultValues(
|
||||||
itemForIntegration.kind,
|
itemForIntegration.kind,
|
||||||
{
|
{
|
||||||
defaultSearchEngineId: serverSettings.search.defaultSearchEngineId,
|
|
||||||
openSearchInNewTab: true,
|
|
||||||
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,7 +1,8 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||||
|
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
|
||||||
|
|
||||||
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
|
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
|
||||||
|
|
||||||
@@ -11,7 +12,9 @@ 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 fetchWithTimeoutAsync(`https://api.mcsrvstat.us${path}`);
|
const response = await withTimeoutAsync(async (signal) =>
|
||||||
|
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"),
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||||
|
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
|
||||||
|
|
||||||
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
|
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
|
||||||
|
|
||||||
@@ -9,9 +10,12 @@ 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 fetchWithTimeoutAsync(
|
const response = await withTimeoutAsync(async (signal) => {
|
||||||
`https://query1.finance.yahoo.com/v8/finance/chart/${input.stock}?range=${input.timeRange}&interval=${input.timeInterval}`,
|
return await fetchWithTrustedCertificatesAsync(
|
||||||
);
|
`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 { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||||
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: fetchWithTimeoutAsync,
|
fetch: fetchWithTrustedCertificatesAsync,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const releases = await octokit.rest.repos.listReleases({
|
const releases = await octokit.rest.repos.listReleases({
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||||
|
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
|
||||||
|
|
||||||
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
|
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
|
||||||
|
|
||||||
@@ -9,9 +10,12 @@ 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 fetchWithTimeoutAsync(
|
const res = await withTimeoutAsync(async (signal) => {
|
||||||
`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¤t_weather=true&timezone=auto`,
|
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¤t_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 {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const defaultServerSettingsKeys = [
|
|||||||
"analytics",
|
"analytics",
|
||||||
"crawlingAndIndexing",
|
"crawlingAndIndexing",
|
||||||
"board",
|
"board",
|
||||||
|
"user",
|
||||||
"appearance",
|
"appearance",
|
||||||
"culture",
|
"culture",
|
||||||
"search",
|
"search",
|
||||||
@@ -31,6 +32,9 @@ export const defaultServerSettings = {
|
|||||||
enableStatusByDefault: true,
|
enableStatusByDefault: true,
|
||||||
forceDisableStatus: false,
|
forceDisableStatus: false,
|
||||||
},
|
},
|
||||||
|
user: {
|
||||||
|
enableGravatar: true,
|
||||||
|
},
|
||||||
appearance: {
|
appearance: {
|
||||||
defaultColorScheme: "light" as ColorScheme,
|
defaultColorScheme: "light" as ColorScheme,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,11 +23,10 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/api": "workspace:^0.1.0",
|
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@mantine/dates": "^8.3.10",
|
"@mantine/dates": "^8.3.12",
|
||||||
"next": "16.0.10",
|
"next": "16.1.1",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export type SettingsContextProps = Pick<
|
|||||||
| "openSearchInNewTab"
|
| "openSearchInNewTab"
|
||||||
| "pingIconsEnabled"
|
| "pingIconsEnabled"
|
||||||
> &
|
> &
|
||||||
Pick<ServerSettings["board"], "enableStatusByDefault" | "forceDisableStatus">;
|
Pick<ServerSettings["board"], "enableStatusByDefault" | "forceDisableStatus"> &
|
||||||
|
Pick<ServerSettings["user"], "enableGravatar">;
|
||||||
|
|
||||||
export interface PublicServerSettings {
|
export interface PublicServerSettings {
|
||||||
search: Pick<ServerSettings["search"], "defaultSearchEngineId">;
|
search: Pick<ServerSettings["search"], "defaultSearchEngineId">;
|
||||||
@@ -18,6 +19,7 @@ export interface PublicServerSettings {
|
|||||||
ServerSettings["board"],
|
ServerSettings["board"],
|
||||||
"homeBoardId" | "mobileHomeBoardId" | "enableStatusByDefault" | "forceDisableStatus"
|
"homeBoardId" | "mobileHomeBoardId" | "enableStatusByDefault" | "forceDisableStatus"
|
||||||
>;
|
>;
|
||||||
|
user: Pick<ServerSettings["user"], "enableGravatar">;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserSettings = Pick<
|
export type UserSettings = Pick<
|
||||||
@@ -45,4 +47,5 @@ export const createSettings = ({
|
|||||||
pingIconsEnabled: user?.pingIconsEnabled ?? false,
|
pingIconsEnabled: user?.pingIconsEnabled ?? false,
|
||||||
enableStatusByDefault: serverSettings.board.enableStatusByDefault,
|
enableStatusByDefault: serverSettings.board.enableStatusByDefault,
|
||||||
forceDisableStatus: serverSettings.board.forceDisableStatus,
|
forceDisableStatus: serverSettings.board.forceDisableStatus,
|
||||||
|
enableGravatar: serverSettings.user.enableGravatar,
|
||||||
});
|
});
|
||||||
@@ -33,12 +33,12 @@
|
|||||||
"@homarr/settings": "workspace:^0.1.0",
|
"@homarr/settings": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@mantine/core": "^8.3.10",
|
"@mantine/core": "^8.3.12",
|
||||||
"@mantine/hooks": "^8.3.10",
|
"@mantine/hooks": "^8.3.12",
|
||||||
"@mantine/spotlight": "^8.3.10",
|
"@mantine/spotlight": "^8.3.12",
|
||||||
"@tabler/icons-react": "^3.35.0",
|
"@tabler/icons-react": "^3.36.1",
|
||||||
"jotai": "^2.16.0",
|
"jotai": "^2.16.1",
|
||||||
"next": "16.0.10",
|
"next": "16.1.1",
|
||||||
"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"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Group, Text, useMantineColorScheme } from "@mantine/core";
|
import { Group, Text, useMantineColorScheme } from "@mantine/core";
|
||||||
import type { TablerIcon } from "@tabler/icons-react";
|
|
||||||
import {
|
import {
|
||||||
IconBox,
|
IconBox,
|
||||||
IconCategoryPlus,
|
IconCategoryPlus,
|
||||||
@@ -17,6 +16,7 @@ import { useSession } from "@homarr/auth/client";
|
|||||||
import { useModalAction } from "@homarr/modals";
|
import { useModalAction } from "@homarr/modals";
|
||||||
import { AddBoardModal, AddGroupModal, ImportBoardModal, InviteCreateModal } from "@homarr/modals-collection";
|
import { AddBoardModal, AddGroupModal, ImportBoardModal, InviteCreateModal } from "@homarr/modals-collection";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
import type { TablerIcon } from "@homarr/ui";
|
||||||
|
|
||||||
import { createGroup } from "../../lib/group";
|
import { createGroup } from "../../lib/group";
|
||||||
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
|
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Box, Group, Stack, Text } from "@mantine/core";
|
import { Box, Group, Stack, Text } from "@mantine/core";
|
||||||
import type { TablerIcon } from "@tabler/icons-react";
|
|
||||||
import { IconCaretUpDown, IconSearch, IconSearchOff } from "@tabler/icons-react";
|
import { IconCaretUpDown, IconSearch, IconSearchOff } from "@tabler/icons-react";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
@@ -9,6 +8,7 @@ import { useSession } from "@homarr/auth/client";
|
|||||||
import { useSettings } from "@homarr/settings";
|
import { useSettings } from "@homarr/settings";
|
||||||
import type { TranslationFunction } from "@homarr/translation";
|
import type { TranslationFunction } from "@homarr/translation";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import type { TablerIcon } from "@homarr/ui";
|
||||||
|
|
||||||
import { createGroup } from "../../lib/group";
|
import { createGroup } from "../../lib/group";
|
||||||
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
|
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { interaction } from "../../lib/interaction";
|
|||||||
|
|
||||||
// This has to be type so it can be interpreted as Record<string, unknown>.
|
// This has to be type so it can be interpreted as Record<string, unknown>.
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
type User = { id: string; name: string; image: string | null };
|
type User = { id: string; name: string; image: string | null; email: string | null };
|
||||||
|
|
||||||
const userChildrenOptions = createChildrenOptions<User>({
|
const userChildrenOptions = createChildrenOptions<User>({
|
||||||
useActions: () => [
|
useActions: () => [
|
||||||
|
|||||||
@@ -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.0.10",
|
"next": "16.1.1",
|
||||||
"next-intl": "4.6.0",
|
"next-intl": "4.7.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3294,6 +3294,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "",
|
||||||
|
"enableGravatar": {
|
||||||
|
"label": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "",
|
"title": "",
|
||||||
"defaultSearchEngine": {
|
"defaultSearchEngine": {
|
||||||
|
|||||||
@@ -1941,19 +1941,19 @@
|
|||||||
"description": "您的容器状态 (这个小部件只能用管理员权限添加)",
|
"description": "您的容器状态 (这个小部件只能用管理员权限添加)",
|
||||||
"option": {
|
"option": {
|
||||||
"enableRowSorting": {
|
"enableRowSorting": {
|
||||||
"label": ""
|
"label": "启用项目排序"
|
||||||
},
|
},
|
||||||
"defaultSort": {
|
"defaultSort": {
|
||||||
"label": "",
|
"label": "默认排序列",
|
||||||
"option": {
|
"option": {
|
||||||
"name": "",
|
"name": "名称",
|
||||||
"state": "",
|
"state": "状态",
|
||||||
"cpuUsage": "",
|
"cpuUsage": "CPU 利用率",
|
||||||
"memoryUsage": ""
|
"memoryUsage": "内存占用率"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"descendingDefaultSort": {
|
"descendingDefaultSort": {
|
||||||
"label": ""
|
"label": "倒序"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -2248,7 +2248,7 @@
|
|||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"pending": "等待处理",
|
"pending": "等待处理",
|
||||||
"processing": "处理中",
|
"processing": "处理中",
|
||||||
"requested": "",
|
"requested": "已请求",
|
||||||
"partiallyAvailable": "部分",
|
"partiallyAvailable": "部分",
|
||||||
"available": "可用",
|
"available": "可用",
|
||||||
"blacklisted": "已列入黑名单",
|
"blacklisted": "已列入黑名单",
|
||||||
@@ -3294,6 +3294,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "",
|
||||||
|
"enableGravatar": {
|
||||||
|
"label": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "搜索",
|
"title": "搜索",
|
||||||
"defaultSearchEngine": {
|
"defaultSearchEngine": {
|
||||||
|
|||||||
@@ -3294,6 +3294,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "crwdns3822:0crwdne3822:0",
|
||||||
|
"enableGravatar": {
|
||||||
|
"label": "crwdns3824:0crwdne3824:0",
|
||||||
|
"description": "crwdns3826:0crwdne3826:0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "crwdns2550:0crwdne2550:0",
|
"title": "crwdns2550:0crwdne2550:0",
|
||||||
"defaultSearchEngine": {
|
"defaultSearchEngine": {
|
||||||
|
|||||||
@@ -3294,6 +3294,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "",
|
||||||
|
"enableGravatar": {
|
||||||
|
"label": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "",
|
"title": "",
|
||||||
"defaultSearchEngine": {
|
"defaultSearchEngine": {
|
||||||
|
|||||||
@@ -3294,6 +3294,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "",
|
||||||
|
"enableGravatar": {
|
||||||
|
"label": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "Søg",
|
"title": "Søg",
|
||||||
"defaultSearchEngine": {
|
"defaultSearchEngine": {
|
||||||
|
|||||||
@@ -3294,6 +3294,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "",
|
||||||
|
"enableGravatar": {
|
||||||
|
"label": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "Suche",
|
"title": "Suche",
|
||||||
"defaultSearchEngine": {
|
"defaultSearchEngine": {
|
||||||
|
|||||||
@@ -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 allen Boards erlauben"
|
"description": "Mitgliedern das Ansehen von 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 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 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önliches Zugangs-Token"
|
"newLabel": "Neuer persönlicher 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 Ö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."
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1949,7 +1949,7 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"state": "Status",
|
"state": "Status",
|
||||||
"cpuUsage": "CPU-Auslastung",
|
"cpuUsage": "CPU-Auslastung",
|
||||||
"memoryUsage": "Speicherverbrauch"
|
"memoryUsage": "Speichernutzung"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"descendingDefaultSort": {
|
"descendingDefaultSort": {
|
||||||
@@ -2639,7 +2639,7 @@
|
|||||||
"notification": {
|
"notification": {
|
||||||
"success": {
|
"success": {
|
||||||
"title": "Änderungen erfolgreich angewendet",
|
"title": "Änderungen erfolgreich angewendet",
|
||||||
"message": "Das Board wurde erfolgreich dupliziert"
|
"message": "Das Board wurde erfolgreich gespeichert"
|
||||||
},
|
},
|
||||||
"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 muss manuell neu erstellt werden"
|
"description": "Fügt nur die Apps hinzu. Das Board selbst 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 schlankens, modernes Dashboard, das alle Ihre Apps und Dienste bereit stellt.",
|
"description": "Ein schlankes, modernes Dashboard, das alle Ihre Apps und Dienste bereitstellt.",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -3294,6 +3294,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "",
|
||||||
|
"enableGravatar": {
|
||||||
|
"label": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "Suche",
|
"title": "Suche",
|
||||||
"defaultSearchEngine": {
|
"defaultSearchEngine": {
|
||||||
@@ -3366,7 +3373,7 @@
|
|||||||
"label": "Index Manager"
|
"label": "Index Manager"
|
||||||
},
|
},
|
||||||
"healthMonitoring": {
|
"healthMonitoring": {
|
||||||
"label": "Überwachung des Gesundheit"
|
"label": "Überwachung des Zustandes"
|
||||||
},
|
},
|
||||||
"dnsHole": {
|
"dnsHole": {
|
||||||
"label": "DNS Hole Daten"
|
"label": "DNS Hole Daten"
|
||||||
@@ -3512,7 +3519,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, sitzhaft mit ihren Migliedern in vielen verschiedenen Ländern, arbeitet in ihrer Freizeit 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, dessen Mitglieder in vielen verschiedenen Ländern ansässig sind, arbeitet in seiner Freizeit und ohne Vergütung.",
|
||||||
"accordion": {
|
"accordion": {
|
||||||
"contributors": {
|
"contributors": {
|
||||||
"title": "Mitwirkende",
|
"title": "Mitwirkende",
|
||||||
@@ -4395,7 +4402,7 @@
|
|||||||
"configControl": "Konfiguration",
|
"configControl": "Konfiguration",
|
||||||
"searchEngineType": {
|
"searchEngineType": {
|
||||||
"generic": "Allgemein",
|
"generic": "Allgemein",
|
||||||
"fromIntegration": "Aus den Integration"
|
"fromIntegration": "Aus der Integration"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
@@ -4498,7 +4505,7 @@
|
|||||||
"notification": {
|
"notification": {
|
||||||
"success": {
|
"success": {
|
||||||
"title": "Zertifikat entfernt",
|
"title": "Zertifikat entfernt",
|
||||||
"message": "Das Zertifikat wurde erfolgreich entfern"
|
"message": "Das Zertifikat wurde erfolgreich entfernt"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"title": "Zertifikat nicht entfernt",
|
"title": "Zertifikat nicht entfernt",
|
||||||
|
|||||||
@@ -3294,6 +3294,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "",
|
||||||
|
"enableGravatar": {
|
||||||
|
"label": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "",
|
"title": "",
|
||||||
"defaultSearchEngine": {
|
"defaultSearchEngine": {
|
||||||
|
|||||||
@@ -3294,6 +3294,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "",
|
||||||
|
"enableGravatar": {
|
||||||
|
"label": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "",
|
"title": "",
|
||||||
"defaultSearchEngine": {
|
"defaultSearchEngine": {
|
||||||
|
|||||||
@@ -3294,6 +3294,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "Users",
|
||||||
|
"enableGravatar": {
|
||||||
|
"label": "Enable Gravatar",
|
||||||
|
"description": "Falls back to user avatars from Libravatar/Gravatar when no custom avatar is set and an email is configured"
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "Search",
|
"title": "Search",
|
||||||
"defaultSearchEngine": {
|
"defaultSearchEngine": {
|
||||||
|
|||||||
@@ -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": ""
|
"label": "Habilitar ordenar elementos"
|
||||||
},
|
},
|
||||||
"defaultSort": {
|
"defaultSort": {
|
||||||
"label": "",
|
"label": "Columna usada para ordenar por defecto",
|
||||||
"option": {
|
"option": {
|
||||||
"name": "",
|
"name": "Nombre",
|
||||||
"state": "",
|
"state": "Estado",
|
||||||
"cpuUsage": "",
|
"cpuUsage": "Uso de CPU",
|
||||||
"memoryUsage": ""
|
"memoryUsage": "Uso de memoria"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"descendingDefaultSort": {
|
"descendingDefaultSort": {
|
||||||
"label": ""
|
"label": "Invertir orden"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -3294,6 +3294,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "",
|
||||||
|
"enableGravatar": {
|
||||||
|
"label": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "Buscar",
|
"title": "Buscar",
|
||||||
"defaultSearchEngine": {
|
"defaultSearchEngine": {
|
||||||
|
|||||||
@@ -3294,6 +3294,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "",
|
||||||
|
"enableGravatar": {
|
||||||
|
"label": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "",
|
"title": "",
|
||||||
"defaultSearchEngine": {
|
"defaultSearchEngine": {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
"init": {
|
"init": {
|
||||||
"step": {
|
"step": {
|
||||||
"start": {
|
"start": {
|
||||||
"title": "Bienvenue dans Homarr",
|
"title": "Bienvenue sur Homarr",
|
||||||
"subtitle": "Commençons par configurer votre instance Homarr.",
|
"subtitle": "Commençons par configurer votre instance Homarr.",
|
||||||
"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 à partir d'Homarr avant la 1.0"
|
"importOldmarr": "Importer depuis 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": "{count} tableaux trouvés",
|
"title": "Tableaux {count} 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": "Rejoindre Homarr",
|
"title": "Rejoignez 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": "",
|
"title": "Existant",
|
||||||
"label": ""
|
"label": "Sélectionner une application existante"
|
||||||
},
|
},
|
||||||
"new": {
|
"new": {
|
||||||
"title": "",
|
"title": "Nouveau",
|
||||||
"url": {
|
"url": {
|
||||||
"label": "",
|
"label": "Lien de l'application",
|
||||||
"description": ""
|
"description": "Lien d'ouverture de l'application depuis le tableau de bord"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -676,9 +676,9 @@
|
|||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"action": {
|
"action": {
|
||||||
"add": "",
|
"add": "Lier une application",
|
||||||
"remove": "",
|
"remove": "Délier",
|
||||||
"select": ""
|
"select": "Sélectionnez une application à lier"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -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": ""
|
"sectionTitle": "Application liée"
|
||||||
},
|
},
|
||||||
"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": "",
|
"label": "Clé privée",
|
||||||
"newLabel": ""
|
"newLabel": "Nouvelle clé privée"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1044,14 +1044,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"success": "",
|
"success": "Succès",
|
||||||
"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": "",
|
"change": "Modifier",
|
||||||
"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": "",
|
"kilometersPerHour": "km/h",
|
||||||
"milesPerHour": ""
|
"milesPerHour": "mph"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1190,7 +1190,7 @@
|
|||||||
"label": "Titre"
|
"label": "Titre"
|
||||||
},
|
},
|
||||||
"customCssClasses": {
|
"customCssClasses": {
|
||||||
"label": ""
|
"label": "Classes Css personnalisées"
|
||||||
},
|
},
|
||||||
"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": "",
|
"label": "Mise en page",
|
||||||
"option": {
|
"option": {
|
||||||
"row": "",
|
"row": "Horizontale",
|
||||||
"row-reverse": "",
|
"row-reverse": "Horizontal (inversé)",
|
||||||
"column": "",
|
"column": "Vertical",
|
||||||
"column-reverse": ""
|
"column-reverse": "Vertical (inversé)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"descriptionDisplayMode": {
|
"descriptionDisplayMode": {
|
||||||
"label": "",
|
"label": "Mode d'affichage de la description",
|
||||||
"description": "",
|
"description": "Choisissez comment afficher la description de l'application",
|
||||||
"option": {
|
"option": {
|
||||||
"normal": "",
|
"normal": "Au sein du widget",
|
||||||
"tooltip": "",
|
"tooltip": "Comme infobulle",
|
||||||
"hidden": ""
|
"hidden": "Masquer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1584,11 +1584,11 @@
|
|||||||
},
|
},
|
||||||
"placeholder": "Commencer à écrire vos notes",
|
"placeholder": "Commencer à écrire vos notes",
|
||||||
"dismiss": {
|
"dismiss": {
|
||||||
"title": "",
|
"title": "Abandonner les modifications ?",
|
||||||
"message": "",
|
"message": "Vous avez des modifications non enregistrées dans votre bloc-notes. Êtes-vous sûr de vouloir les supprimer ?",
|
||||||
"action": {
|
"action": {
|
||||||
"discard": "",
|
"discard": "Ignorer les modifications",
|
||||||
"keepEditing": ""
|
"keepEditing": "Continuer les modifications"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -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": ""
|
"allDay": "Tous les jours"
|
||||||
},
|
},
|
||||||
"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": ""
|
"label": "Utiliser mph pour la vitesse du vent"
|
||||||
},
|
},
|
||||||
"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": ""
|
"label": "Activer le tri des éléments"
|
||||||
},
|
},
|
||||||
"defaultSort": {
|
"defaultSort": {
|
||||||
"label": "",
|
"label": "Colonne de tri par défaut",
|
||||||
"option": {
|
"option": {
|
||||||
"name": "",
|
"name": "Nom",
|
||||||
"state": "",
|
"state": "État",
|
||||||
"cpuUsage": "",
|
"cpuUsage": "Utilisation du CPU",
|
||||||
"memoryUsage": ""
|
"memoryUsage": "Utilisation mémoire"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"descendingDefaultSort": {
|
"descendingDefaultSort": {
|
||||||
"label": ""
|
"label": "Inverser le tri"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -2036,21 +2036,21 @@
|
|||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
"id": "Id",
|
"id": "Id",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"title": "",
|
"title": "Statistiques détaillées",
|
||||||
"video": {
|
"video": {
|
||||||
"title": "",
|
"title": "Vidéo",
|
||||||
"resolution": ""
|
"resolution": "Résolution"
|
||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
"title": "",
|
"title": "Audio",
|
||||||
"channelCount": "",
|
"channelCount": "Canaux audio",
|
||||||
"codec": ""
|
"codec": "Codec audio"
|
||||||
},
|
},
|
||||||
"transcoding": {
|
"transcoding": {
|
||||||
"title": "",
|
"title": "Transcodage",
|
||||||
"container": "",
|
"container": "Conteneur",
|
||||||
"resolution": "Résolution",
|
"resolution": "Résolution",
|
||||||
"target": ""
|
"target": "Codec cible"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2248,7 +2248,7 @@
|
|||||||
"unknown": "Inconnu",
|
"unknown": "Inconnu",
|
||||||
"pending": "En attente",
|
"pending": "En attente",
|
||||||
"processing": "Traitement en cours",
|
"processing": "Traitement en cours",
|
||||||
"requested": "",
|
"requested": "En attente",
|
||||||
"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": ""
|
"label": "Masquer la description"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2542,7 +2542,7 @@
|
|||||||
},
|
},
|
||||||
"firewall": {
|
"firewall": {
|
||||||
"name": "Surveillance du pare-feu",
|
"name": "Surveillance du pare-feu",
|
||||||
"description": "",
|
"description": "Affiche un résumé des pare-feu",
|
||||||
"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": "",
|
"versiontitle": "Versions",
|
||||||
"cputitle": "",
|
"cputitle": "Utilisation du CPU",
|
||||||
"memorytitle": "",
|
"memorytitle": "Utilisation de la mémoire",
|
||||||
"cpu": "",
|
"cpu": "CPU",
|
||||||
"memory": "",
|
"memory": "Mémoire",
|
||||||
"interfaces": {
|
"interfaces": {
|
||||||
"name": "",
|
"name": "nom",
|
||||||
"trans": "",
|
"trans": "Transmis",
|
||||||
"recv": "",
|
"recv": "Reçus",
|
||||||
"title": ""
|
"title": "Interfaces réseau"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2576,37 +2576,37 @@
|
|||||||
"option": {}
|
"option": {}
|
||||||
},
|
},
|
||||||
"systemResources": {
|
"systemResources": {
|
||||||
"name": "",
|
"name": "Ressources du système",
|
||||||
"description": "",
|
"description": "CPU, Mémoire, Disque et autre utilisation matérielle de votre système",
|
||||||
"option": {
|
"option": {
|
||||||
"hasShadow": {
|
"hasShadow": {
|
||||||
"label": ""
|
"label": "Activer l'ombrage des cartes"
|
||||||
},
|
},
|
||||||
"visibleCharts": {
|
"visibleCharts": {
|
||||||
"label": "",
|
"label": "Graphiques visibles",
|
||||||
"description": "",
|
"description": "Sélectionnez les graphiques à afficher.",
|
||||||
"option": {
|
"option": {
|
||||||
"cpu": "",
|
"cpu": "CPU",
|
||||||
"memory": "",
|
"memory": "Mémoire",
|
||||||
"network": ""
|
"network": "Réseau"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"labelDisplayMode": {
|
"labelDisplayMode": {
|
||||||
"label": "",
|
"label": "Mode d'affichage de l'étiquette",
|
||||||
"option": {
|
"option": {
|
||||||
"textWithIcon": "",
|
"textWithIcon": "Afficher le texte avec l'icône",
|
||||||
"text": "",
|
"text": "Afficher uniquement le texte",
|
||||||
"icon": "",
|
"icon": "Afficher uniquement l'icône",
|
||||||
"hidden": ""
|
"hidden": "Masquer l'étiquette"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"card": {
|
"card": {
|
||||||
"cpu": "",
|
"cpu": "CPU",
|
||||||
"memory": "",
|
"memory": "RAM",
|
||||||
"network": "",
|
"network": "NET",
|
||||||
"up": "",
|
"up": "UP",
|
||||||
"down": ""
|
"down": "HORS SERVICE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3030,8 +3030,8 @@
|
|||||||
"integration": "Intégrations",
|
"integration": "Intégrations",
|
||||||
"app": "Applications",
|
"app": "Applications",
|
||||||
"group": "Groupes",
|
"group": "Groupes",
|
||||||
"searchEngine": "",
|
"searchEngine": "Moteurs de recherche",
|
||||||
"media": ""
|
"media": "Médias"
|
||||||
},
|
},
|
||||||
"statisticLabel": {
|
"statisticLabel": {
|
||||||
"boards": "Tableaux de bord",
|
"boards": "Tableaux de bord",
|
||||||
@@ -3040,8 +3040,8 @@
|
|||||||
"authorization": "Autorisation"
|
"authorization": "Autorisation"
|
||||||
},
|
},
|
||||||
"heroBanner": {
|
"heroBanner": {
|
||||||
"title": "",
|
"title": "Bienvenue sur votre",
|
||||||
"subtitle": ""
|
"subtitle": "Tableau {app}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"board": {
|
"board": {
|
||||||
@@ -3294,6 +3294,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "Utilisateurs",
|
||||||
|
"enableGravatar": {
|
||||||
|
"label": "Activer Gravatar",
|
||||||
|
"description": "Revient aux avatars des utilisateurs de Libravatar/Gravatar quand aucun avatar personnalisé n'est défini et qu'un e-mail est configuré"
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "Rechercher",
|
"title": "Rechercher",
|
||||||
"defaultSearchEngine": {
|
"defaultSearchEngine": {
|
||||||
@@ -3387,19 +3394,19 @@
|
|||||||
"label": "Conteneurs Docker"
|
"label": "Conteneurs Docker"
|
||||||
},
|
},
|
||||||
"firewallCpu": {
|
"firewallCpu": {
|
||||||
"label": ""
|
"label": "Processeur du pare-feu"
|
||||||
},
|
},
|
||||||
"firewallMemory": {
|
"firewallMemory": {
|
||||||
"label": ""
|
"label": "Mémoire du pare-feu"
|
||||||
},
|
},
|
||||||
"firewallVersion": {
|
"firewallVersion": {
|
||||||
"label": ""
|
"label": "Version du pare-feu"
|
||||||
},
|
},
|
||||||
"firewallInterfaces": {
|
"firewallInterfaces": {
|
||||||
"label": ""
|
"label": "Interfaces du pare-feu"
|
||||||
},
|
},
|
||||||
"weather": {
|
"weather": {
|
||||||
"label": ""
|
"label": "Météo"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"interval": {
|
"interval": {
|
||||||
@@ -3410,10 +3417,10 @@
|
|||||||
"weeklyMonday": "Chaque semaine le lundi",
|
"weeklyMonday": "Chaque semaine le lundi",
|
||||||
"update": {
|
"update": {
|
||||||
"success": {
|
"success": {
|
||||||
"message": ""
|
"message": "Intervalle mis à jour avec succès"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"message": ""
|
"message": "L'intervalle de mise à jour a échoué"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3422,55 +3429,55 @@
|
|||||||
},
|
},
|
||||||
"field": {
|
"field": {
|
||||||
"name": {
|
"name": {
|
||||||
"label": ""
|
"label": "Nom"
|
||||||
},
|
},
|
||||||
"interval": {
|
"interval": {
|
||||||
"label": "Intervalle de planification"
|
"label": "Intervalle de planification"
|
||||||
},
|
},
|
||||||
"lastExecution": {
|
"lastExecution": {
|
||||||
"label": ""
|
"label": "Dernière exécution"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"label": ""
|
"label": "Actions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"search": ""
|
"search": "Rechercher des tâches {count}..."
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"refresh": {
|
"refresh": {
|
||||||
"label": ""
|
"label": "Rafraîchir"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"refresh": {
|
"refresh": {
|
||||||
"success": {
|
"success": {
|
||||||
"message": ""
|
"message": "Tâches actualisées avec succès"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"message": ""
|
"message": "Échec de l'actualisation des tâches"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"trigger": {
|
"trigger": {
|
||||||
"success": {
|
"success": {
|
||||||
"message": ""
|
"message": "Tâche déclenchée avec succès"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"message": ""
|
"message": "Échec du déclenchement de la tâche"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enable": {
|
"enable": {
|
||||||
"success": {
|
"success": {
|
||||||
"message": ""
|
"message": "Tâche activée avec succès"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"disable": {
|
"disable": {
|
||||||
"success": {
|
"success": {
|
||||||
"message": ""
|
"message": "Tâche désactivée avec succès"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toggle": {
|
"toggle": {
|
||||||
"error": {
|
"error": {
|
||||||
"message": ""
|
"message": "Impossible de basculer le statut de la tâche"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3527,19 +3534,19 @@
|
|||||||
"subtitle": "{count} utilisés dans le Code d'Homarr"
|
"subtitle": "{count} utilisés dans le Code d'Homarr"
|
||||||
},
|
},
|
||||||
"hotkeys": {
|
"hotkeys": {
|
||||||
"title": "",
|
"title": "Raccourcis clavier",
|
||||||
"subtitle": "",
|
"subtitle": "Raccourcis clavier pour améliorer votre flux de travail",
|
||||||
"field": {
|
"field": {
|
||||||
"shortcut": "",
|
"shortcut": "Raccourci",
|
||||||
"action": ""
|
"action": "Action"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"toggleBoardEdit": "",
|
"toggleBoardEdit": "Activer/désactiver le mode d'édition du tableau",
|
||||||
"toggleColorScheme": "",
|
"toggleColorScheme": "Basculer en mode clair/sombre",
|
||||||
"saveNotebook": "",
|
"saveNotebook": "Enregistrer le bloc-notes (uniquement à l'intérieur du widget notebook)",
|
||||||
"openSpotlight": ""
|
"openSpotlight": "Ouvrir la recherche"
|
||||||
},
|
},
|
||||||
"note": ""
|
"note": "Astuce : Mod correspond à la touche Ctrl ou ⌘ sur macOS"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3995,7 +4002,7 @@
|
|||||||
"tools": {
|
"tools": {
|
||||||
"label": "Outils",
|
"label": "Outils",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"label": ""
|
"label": "Tâches"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"label": "Docker"
|
"label": "Docker"
|
||||||
|
|||||||
@@ -3294,6 +3294,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"user": {
|
||||||
|
"title": "",
|
||||||
|
"enableGravatar": {
|
||||||
|
"label": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "חיפוש",
|
"title": "חיפוש",
|
||||||
"defaultSearchEngine": {
|
"defaultSearchEngine": {
|
||||||
|
|||||||
Loaded 100 of 136 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user