Compare commits

...
Author SHA1 Message Date
Meier Lukas 29293992ca fix: bytestring middleware issue 2025-06-07 14:25:09 +02:00
homarr-renovate[bot] 7928f15452 chore(deps): update dependency @types/dockerode to ^3.3.40 (#3347)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-07 10:21:45 +00:00
Meier Lukas 80327337ce chore(issue-template): add version 1.23.0 2025-06-07 12:11:33 +02:00
homarr-crowdin[bot] f72a994fb3 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-06-07 10:08:14 +00:00
homarr-renovate[bot] 1910786f5b chore(deps): update dependency @types/cookies to v0.9.1 (#3346)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-07 03:16:21 +00:00
homarr-renovate[bot] 6ec66b1fe2 fix(deps): update testcontainers-node monorepo to ^11.0.1 (#3343)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-06 21:43:01 +00:00
homarr-renovate[bot] aafd883c32 fix(deps): update dependency zod to ^3.25.56 (#3340)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-06 21:47:12 +02:00
Releases Homarr f32242bf79 chore(release): 1.23.0 [skip ci] 2025-06-06 19:16:16 +00:00
homarr-releases[bot] 62aeb043b5 chore(release): automatic release v1.23.0 2025-06-06 19:15:09 +00:00
Kaotic3andMeier Lukas ffd2f802bb fix(nextcloud): correct calendar event date mapping and support showU… (#3330)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-06-06 20:00:22 +02:00
lthomsen 3e7bf62500 fix: Allow nullable on cpuModelName and default to "Unknown CPU" if null in OMV Integration (#3310) 2025-06-06 20:00:09 +02:00
Andre Silva 7071d76c52 feat(releases-widget): add import from docker functionality (#3130) 2025-06-06 19:59:46 +02:00
Meier Lukas c7a21b1a5f fix: csp prevents external stylesheets (#3337) 2025-06-06 19:58:37 +02:00
homarr-renovate[bot] 6274a66048 fix(deps): update tiptap monorepo to v2.14.0 (#3334)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-06 15:20:46 +00:00
homarr-renovate[bot] 07f81cee92 fix(deps): update dependency dockerode to ^4.0.7 (#3331)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-06 15:19:53 +00:00
homarr-renovate[bot] 3ad72e63b1 fix(deps): update dependency fast-xml-parser to ^5.2.4 (#3336)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-06 14:21:15 +00:00
homarr-renovate[bot] ed3f848e8a fix(deps): update dependency zod to ^3.25.55 (#3332)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-06 08:26:48 +00:00
homarr-renovate[bot] 8484daaf75 fix(deps): update trpc monorepo to ^11.3.1 (#3329)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-05 21:20:00 +00:00
homarr-renovate[bot] aeed7dbfd0 chore(deps): update dependency @types/node to ^22.15.30 (#3328)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-05 19:18:20 +00:00
homarr-renovate[bot] 68881e7ee5 chore(deps): update vitest monorepo to ^3.2.2 (#3327)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-05 14:22:21 +00:00
homarr-renovate[bot] 61553cfc8e fix(deps): update tanstack-query monorepo to ^5.80.6 (#3326)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-05 13:35:26 +00:00
homarr-renovate[bot] cf7336ba87 fix(deps): update dependency swagger-ui-react to ^5.24.0 (#3325)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-05 10:23:04 +00:00
homarr-renovate[bot] dac82347e8 fix(deps): update tiptap monorepo to v2.13.0 (#3323)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-05 07:21:55 +00:00
homarr-renovate[bot] 74aa561f47 fix(deps): update trpc monorepo to ^11.3.0 (#3322)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-04 20:19:56 +00:00
homarr-renovate[bot] cb77a7dc6d fix(deps): update tanstack-query monorepo to ^5.80.5 (#3321)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-04 19:18:58 +00:00
homarr-renovate[bot] 81509fd96d fix(deps): update dependency video.js to ^8.23.3 (#3320)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-04 17:20:30 +00:00
homarr-renovate[bot] 960dc33af8 chore(deps): update dependency @types/react-dom to v19.1.6 (#3319)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-04 13:37:17 +00:00
homarr-renovate[bot] c051a42791 fix(deps): update tanstack-query monorepo to ^5.80.3 (#3317)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-04 10:22:38 +00:00
homarr-renovate[bot] a542ee0798 fix(deps): update dependency zod to ^3.25.51 (#3316)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-04 08:26:44 +00:00
homarr-renovate[bot] 9ac9042564 fix(deps): update dependency drizzle-orm to ^0.44.2 (#3315)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-04 07:21:31 +00:00
homarr-crowdin[bot] 2fe3dcb01b chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-06-04 00:52:41 +00:00
homarr-renovate[bot] a0a0449c70 fix(deps): update dependency zod to ^3.25.50 (#3312)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-03 22:21:28 +00:00
homarr-renovate[bot] d4afafe0f7 chore(deps): update vitest monorepo to ^3.2.1 (#3309)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-03 17:21:20 +00:00
homarr-renovate[bot] cae0cad92f fix(deps): update tanstack-query monorepo to ^5.80.2 (#3308)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-03 12:35:44 +00:00
homarr-renovate[bot] df2a9a5329 fix(deps): update tanstack-query monorepo to ^5.80.0 (#3307)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-03 10:23:13 +00:00
homarr-renovate[bot] 8f6cc6fa60 chore(deps): update dependency @vitejs/plugin-react to ^4.5.1 (#3306)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-03 04:28:26 +00:00
homarr-crowdin[bot] d699aad529 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-06-03 00:53:41 +00:00
homarr-renovate[bot] 0362487156 fix(deps): update dependency zod to ^3.25.49 (#3304)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-02 23:21:48 +00:00
homarr-renovate[bot] 5b354aca20 fix(deps): update dependency @tabler/icons-react to ^3.34.0 (#3303)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-02 18:27:59 +00:00
homarr-renovate[bot] 124e8de6c5 fix(deps): update dependency typescript-eslint to ^8.33.1 (#3302)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-02 18:27:20 +00:00
homarr-renovate[bot] a0585e3dd7 fix(deps): update tanstack-query monorepo to ^5.79.2 (#3301)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-02 17:20:23 +00:00
homarr-renovate[bot] a3deed3b09 fix(deps): update tanstack-query monorepo to ^5.79.1 (#3300)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-02 16:24:59 +00:00
homarr-renovate[bot] ae5aebda20 chore(deps): update pnpm to v10.11.1 (#3299)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-02 14:20:59 +00:00
homarr-renovate[bot]andMeier Lukas 362c663060 chore(deps): update vitest monorepo to ^3.2.0 (#3298)
* chore(deps): update vitest monorepo to ^3.2.0

* fix: reduce flakyness of nzbget test

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-06-02 12:07:37 +00:00
homarr-renovate[bot]andMeier Lukas 0d1e3a625a fix(deps): update testcontainers-node monorepo to v11 (major) (#3250)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-06-02 13:58:49 +02:00
homarr-renovate[bot] ccd8a85e0f fix(deps): update dependency zod to ^3.25.48 (#3297)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-02 11:17:12 +00:00
homarr-renovate[bot] f0ba693077 fix(deps): update trpc monorepo to ^11.2.0 (#3293)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-02 07:22:04 +00:00
homarr-renovate[bot] 94f176d544 fix(deps): update dependency zod to ^3.25.47 (#3296)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-02 06:28:59 +00:00
Meier Lukas a86fc5bcb8 chore(pr-template): add task for updating documentation 2025-06-01 10:43:32 +02:00
Meier Lukas 9bdbe6f425 chore(issue-template): add version 1.22.0 2025-06-01 10:40:32 +02:00
homarr-renovate[bot] 1b1671e0e6 fix(deps): update dependency zod to ^3.25.46 (#3292)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-01 08:24:52 +00:00
homarr-renovate[bot] 3337cfa3c7 fix(deps): update dependency zod to ^3.25.45 (#3291)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-06-01 03:35:28 +00:00
homarr-renovate[bot] 161fc86770 fix(deps): update dependency zod to ^3.25.43 (#3290)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-31 23:20:35 +00:00
homarr-renovate[bot] 76ae423fd9 chore(deps): update dependency prettier-plugin-packagejson to ^2.5.15 (#3289)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-31 12:32:21 +00:00
homarr-renovate[bot] 1f408685e6 fix(deps): update dependency node-cron to ^4.1.0 (#3286)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-31 03:14:01 +00:00
homarr-renovate[bot] b608ffe490 fix(deps): update dependency sass to ^1.89.1 (#3285)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-31 01:14:48 +00:00
homarr-crowdin[bot] f579b283f2 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-31 00:50:38 +00:00
homarr-renovate[bot] 9518ebb088 chore(deps): update dependency eslint to ^9.28.0 (#3283)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-30 21:20:21 +00:00
homarr-renovate[bot] 9895c1b3ff fix(deps): update dependency drizzle-orm to ^0.44.1 (#3280)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-30 20:22:34 +00:00
homarr-renovate[bot] 7e5ca418cc chore(deps): update dependency @types/node to ^22.15.29 (#3281)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-30 19:24:39 +00:00
Releases Homarr 6b76d55de2 chore(release): 1.22.0 [skip ci] 2025-05-30 19:17:12 +00:00
homarr-releases[bot] 09f821b95d chore(release): automatic release v1.22.0 2025-05-30 19:14:45 +00:00
Meier Lukas 11149348fc fix(cluster-health): storage items multiply (#3206) 2025-05-30 20:52:51 +02:00
homarr-renovate[bot] c8202ab3c6 fix(deps): update turbo monorepo to ^2.5.4 (#3279)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-30 16:24:36 +00:00
homarr-renovate[bot] 7a50829b6a chore(deps): update dependency @types/node to ^22.15.28 (#3277)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-30 15:21:51 +00:00
homarr-update-contributors[bot] 778cf861cf chore: update contributors (#3275)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2025-05-30 12:35:10 +00:00
homarr-renovate[bot] 6b8ffdfbc0 chore(deps): update dependency @types/node to ^22.15.27 (#3273)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-30 09:22:10 +00:00
homarr-renovate[bot] 608280e670 fix(deps): update dependency zod to ^3.25.42 (#3274)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-30 08:26:24 +00:00
homarr-renovate[bot] 5bf20aa353 fix(deps): update dependency zod to ^3.25.41 (#3272)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-30 03:15:18 +00:00
homarr-renovate[bot] 3ba9502b26 chore(deps): update dependency @types/node to ^22.15.26 (#3271)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-29 23:20:36 +00:00
homarr-renovate[bot] 862d07db68 fix(deps): update dependency zod to ^3.25.39 (#3270)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-29 22:21:03 +00:00
homarr-renovate[bot] d72a804ad9 fix(deps): update nextjs monorepo to v15.3.3 (#3269)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-29 20:24:32 +00:00
homarr-renovate[bot] 2320d3c567 fix(deps): update dependency flag-icons to ^7.5.0 (#3268)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-29 18:27:12 +00:00
homarr-renovate[bot] a0eda74fa4 fix(deps): update dependency flag-icons to ^7.4.0 (#3265)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-29 17:19:46 +00:00
homarr-renovate[bot] bac4f64257 fix(deps): update dependency ldapts to v8.0.1 (#3264)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-29 17:19:30 +00:00
homarr-renovate[bot] 1f9983f16d fix(deps): update tanstack-query monorepo to ^5.79.0 (#3263)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-29 12:23:07 +00:00
homarr-renovate[bot] 48ce2af4c1 fix(deps): update dependency zod to ^3.25.36 (#3262)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-29 08:26:55 +00:00
homarr-renovate[bot] 635c9a2687 fix(deps): update dependency @kubernetes/client-node to ^1.3.0 (#3261)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-29 01:15:13 +00:00
homarr-renovate[bot] c1324eab76 fix(deps): update dependency zod to ^3.25.34 (#3259)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-28 23:20:41 +00:00
homarr-renovate[bot] bfd7b70618 fix(deps): update trpc monorepo to ^11.1.4 (#3260)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-28 22:20:43 +00:00
homarr-renovate[bot] ad5f985645 chore(deps): update dependency @types/node to ^22.15.24 (#3257)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-28 21:20:37 +00:00
homarr-renovate[bot] 4a184d23f2 fix(deps): update dependency @ianvs/prettier-plugin-sort-imports to ^4.4.2 (#3258)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-28 20:23:40 +00:00
homarr-renovate[bot] 09b49a242d fix(deps): update dependency drizzle-orm to ^0.44.0 (#3256)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-28 15:22:07 +00:00
Meier Lukas 10f720d74c fix(search): removing server search engine causes loading error (#3252) 2025-05-28 07:57:37 +02:00
homarr-renovate[bot] 9c3a46c071 fix(deps): update dependency zod to ^3.25.32 (#3255)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-28 03:20:53 +00:00
homarr-renovate[bot] e2e4656001 fix(deps): update dependency zod to ^3.25.31 (#3254)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 22:20:28 +00:00
homarr-renovate[bot] 3253b2f138 fix(deps): update dependency octokit to ^5.0.3 (#3249)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 20:22:31 +00:00
homarr-renovate[bot] f9c7e7711a chore(deps): update dependency @types/node to ^22.15.23 (#3248)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 19:18:31 +00:00
homarr-renovate[bot] 8b553b80e6 fix(deps): update dependency typescript-eslint to ^8.33.0 (#3247)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 18:26:02 +00:00
homarr-renovate[bot] 75ebf075a5 chore(deps): update dependency @types/node to ^22.15.22 (#3246)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 18:25:39 +00:00
homarr-crowdin[bot] 1575716f41 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-27 17:02:51 +00:00
homarr-renovate[bot] 8966e2ad8a fix(deps): update dependency @t3-oss/env-nextjs to ^0.13.6 (#3244)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 12:36:02 +00:00
homarr-renovate[bot] 816544a996 fix(deps): update mantine monorepo to ^8.0.2 (#3245)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 11:19:17 +00:00
homarr-renovate[bot] 9c323850bc fix(deps): update dependency jotai to ^2.12.5 (#3243)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 09:21:29 +00:00
homarr-renovate[bot] c3936237c0 chore(deps): update dependency @types/react to v19.1.6 (#3242)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 08:26:07 +00:00
homarr-renovate[bot] 9b8d1cbb66 chore(deps): update dependency esbuild to ^0.25.5 (#3241)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 04:27:15 +00:00
homarr-renovate[bot] 3062948939 fix(deps): update dependency zod to ^3.25.30 (#3239)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 03:13:49 +00:00
homarr-renovate[bot] 78f98edc41 fix(deps): update trpc monorepo to ^11.1.3 (#3240)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-27 01:14:03 +00:00
homarr-renovate[bot] 81ca6ef7b2 fix(deps): update dependency zod to ^3.25.29 (#3237)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-26 23:20:16 +00:00
homarr-renovate[bot] 71186908b9 fix(deps): update tanstack-query monorepo to ^5.77.2 (#3232)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-26 12:34:40 +00:00
Meier Lukas cbbf0c8dcf fix: navigating back results in navigation back to first page opened (#3224) 2025-05-26 11:41:09 +02:00
Meier Lukas 1d27da2230 fix(calendar): remove scrollbar for 2x2 (#3227) 2025-05-26 11:40:42 +02:00
homarr-crowdin[bot] d4ef212042 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-26 00:53:16 +00:00
homarr-renovate[bot] 5ea7a20677 fix(deps): update tanstack-query monorepo to ^5.77.1 (#3229)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-25 22:19:07 +00:00
Meier Lukas 05e159f566 fix: csp prevents fonts to load (#3225) 2025-05-25 16:39:20 +02:00
homarr-renovate[bot] 6d9e42cf5d chore(deps): update dependency @semantic-release/github to ^11.0.3 (#3226)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-25 13:28:48 +00:00
Meier Lukas 437ecd1023 fix(calendar): load events for all visible days of calendar month (#3216) 2025-05-25 14:51:26 +02:00
Meier Lukas 85e98d2b60 fix(boards): app select modal images rendering issue (#3218) 2025-05-25 14:51:13 +02:00
homarr-crowdin[bot] 8e24d8362e chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-25 07:49:01 +00:00
Manuel c367f8ac23 docs: add pika pods (#3215) 2025-05-24 18:53:04 +02:00
Meier Lukas c67916f72e fix(downloads): upspeed font size wrong (#3214) 2025-05-24 18:52:55 +02:00
Meier Lukas 25bd3879db fix(media-request-stats): users squeezed together (#3213) 2025-05-24 18:52:45 +02:00
Meier Lukas 939df8f6d1 feat(cluster-health): add visibility options (#3210) 2025-05-24 17:49:49 +02:00
Meier Lukas 2dc871e531 feat(downloads): add option to limit amount of items (#3205) 2025-05-24 17:49:39 +02:00
Meier Lukas f7e5e823d5 fix(visual): check for creator board permission is wrong (#3212) 2025-05-24 17:49:08 +02:00
homarr-renovate[bot] 240bf16806 chore(deps): update dependency conventional-changelog-conventionalcommits to v9 (#3151)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-24 11:17:16 +02:00
homarr-renovate[bot] 640bf7b939 fix(deps): update dependency trpc-to-openapi to ^2.3.1 (#3046)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-24 09:14:40 +00:00
homarr-renovate[bot] ec42f265a0 fix(deps): update tanstack-query monorepo to ^5.77.0 (#3204)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-24 08:22:43 +00:00
homarr-crowdin[bot] ea5a763046 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-24 00:49:31 +00:00
homarr-renovate[bot] e0a9e4f97c fix(deps): update dependency zod to ^3.25.28 (#3201)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-23 23:20:33 +00:00
homarr-renovate[bot] 581746b2c2 chore(deps): update dependency semantic-release to ^24.2.5 (#3202)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-23 22:20:55 +00:00
ManuelandMeier Lukas be8b332f94 fix: reduce logging (#3200)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-05-23 22:36:23 +02:00
homarr-renovate[bot]andManuel 109f9e7979 fix(deps): update dependency node-cron to v4 (#3075)
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
2025-05-23 22:20:47 +02:00
Meier Lukas 2afa9b6727 perf: cache middleware onboarding and language checks (#3197) 2025-05-23 22:20:21 +02:00
09af0a76f6 feat: allow custom protocols for app href (#2575)
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-05-23 20:19:59 +00:00
Manuel 71516e02c0 ci: ignore auto gen and build files eslint (#3198) 2025-05-23 19:47:44 +00:00
Meier Lukas a5f7565468 chore(issue-template): add version 1.21.0 2025-05-23 21:19:35 +02:00
homarr-renovate[bot] 0b224e8835 fix(deps): update dependency zod to ^3.25.24 (#3195)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-23 19:18:29 +00:00
Releases Homarr f748a12b58 chore(release): 1.21.0 [skip ci] 2025-05-23 19:17:12 +00:00
homarr-releases[bot] 6b746b09b5 chore(release): automatic release v1.21.0 2025-05-23 19:15:16 +00:00
e1eda534da feat: docker widget (#2288)
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: homarr-crowdin[bot] <190541745+homarr-crowdin[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-05-23 18:35:04 +00:00
Meier Lukas 09f4e6785b fix(downloads): reordering columns in widget does not work (#3194) 2025-05-23 18:33:35 +00:00
Meier Lukas 09f3989483 fix(items): resize handle overlaps header (#3193) 2025-05-23 18:33:02 +00:00
Andre Silva 4cb004fce0 fix(releases-widget): add hardcoded css classNames(#3181) 2025-05-23 18:17:06 +02:00
homarr-renovate[bot] 0ef2310776 chore(deps): update node.js to v22.16.0 (#3180)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-23 18:11:43 +02:00
homarr-update-contributors[bot] b5bc8379ae chore: update contributors (#3192)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2025-05-23 12:55:50 +00:00
homarr-renovate[bot] 43b82419e9 fix(deps): update testcontainers-node monorepo to ^10.28.0 (#3191)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-23 10:23:03 +00:00
homarr-renovate[bot] 329d4727f1 chore(deps): update dependency @vitejs/plugin-react to ^4.5.0 (#3189)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-23 01:16:09 +00:00
homarr-renovate[bot] 9840644ca5 fix(deps): update dependency zod to ^3.25.23 (#3188)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-22 23:20:48 +00:00
homarr-renovate[bot] 37d637dcae fix(deps): update dependency zod to ^3.25.21 (#3187)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-22 21:20:03 +00:00
homarr-renovate[bot] 1584114e72 fix(deps): update tanstack-query monorepo to ^5.76.2 (#3186)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-22 16:24:12 +00:00
homarr-renovate[bot] 49498c870e fix(deps): update dependency zod to ^3.25.20 (#3183)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-21 23:20:30 +00:00
homarr-renovate[bot] 5466ff5047 chore(deps): update node.js to v22.16.0 (#3179)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-21 16:25:29 +00:00
homarr-renovate[bot] 1e7b2e617b fix(deps): update dependency swagger-ui-react to ^5.22.0 (#3178)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-21 13:34:52 +00:00
homarr-renovate[bot] 371803ac50 fix(deps): update dependency zod to ^3.25.17 (#3177)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-21 12:35:21 +00:00
homarr-renovate[bot] 746ada97be chore(deps): update dependency @types/react to v19.1.5 (#3176)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-21 11:19:20 +00:00
homarr-renovate[bot] 5080f51a8d fix(deps): update dependency zod to ^3.25.16 (#3175)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-21 10:22:29 +00:00
homarr-renovate[bot] 23404a27f1 fix(deps): update dependency zod to ^3.25.14 (#3174)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-21 09:23:12 +00:00
homarr-renovate[bot] dae6dedde5 fix(deps): update dependency zod to ^3.25.13 (#3173)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-21 04:25:16 +00:00
homarr-renovate[bot] caaee503c1 fix(deps): update dependency zod to ^3.25.12 (#3172)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-21 03:15:38 +00:00
homarr-crowdin[bot] a356d61808 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-21 00:52:08 +00:00
homarr-renovate[bot] 72bcbe8eff fix(deps): update dependency zod to ^3.25.8 (#3170)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-20 23:20:46 +00:00
homarr-renovate[bot] 145365fab7 chore(deps): update dependency @types/node to ^22.15.21 (#3169)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-20 23:20:28 +00:00
homarr-renovate[bot] 8c59abdde3 fix(deps): update dependency octokit to v5 (#3156)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-20 21:36:43 +02:00
homarr-renovate[bot] 47869a3af4 chore(deps): update dependency @types/dockerode to ^3.3.39 (#3166)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-20 19:18:32 +00:00
homarr-renovate[bot] 5403b9c95e chore(deps): update dependency @types/node to ^22.15.20 (#3167)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-20 18:27:17 +00:00
homarr-renovate[bot] 50445b560e fix(deps): update dependency octokit to ^4.1.4 (#3165)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-20 16:24:19 +00:00
homarr-renovate[bot] 18c465e81e fix(deps): update testcontainers-node monorepo to ^10.27.0 (#3159)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-20 09:23:43 +00:00
homarr-renovate[bot] eb5d5f8260 fix(deps): update dependency undici to v7.10.0 (#3158)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-20 08:27:45 +00:00
homarr-renovate[bot] 096d37be37 chore(deps): update dependency proxmox-api&gt;undici to v7.10.0 (#3157)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-20 08:26:49 +00:00
homarr-crowdin[bot] 279a4b6add chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-20 00:52:28 +00:00
homarr-renovate[bot] 22954b8d26 fix(deps): update dependency zod to ^3.25.7 (#3154)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-19 23:20:23 +00:00
homarr-renovate[bot] 92ebc05fbf fix(deps): update dependency zod to ^3.25.6 (#3153)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-19 22:20:45 +00:00
homarr-renovate[bot] 79755c95d3 fix(deps): update dependency zod to ^3.25.5 (#3152)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-19 21:20:22 +00:00
homarr-renovate[bot] 49b7080415 fix(deps): update dependency zod to ^3.25.4 (#3150)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-19 20:22:49 +00:00
homarr-renovate[bot] 5b9f75eb45 chore(deps): update vitest monorepo to ^3.1.4 (#3149)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-19 17:20:34 +00:00
homarr-renovate[bot] 6e2aa87102 fix(deps): update dependency zod to ^3.25.3 (#3148)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-19 16:25:04 +00:00
homarr-renovate[bot] 17fbb0fc41 chore(deps): update dependency prettier-plugin-packagejson to ^2.5.14 (#3147)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-19 12:36:05 +00:00
homarr-renovate[bot] 28be77b9c3 chore(deps): update dependency @types/node to ^22.15.19 (#3145)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-19 01:18:27 +00:00
homarr-crowdin[bot] adddae6430 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-18 08:06:41 +00:00
Meier Lukas ef540e980f fix: esbuild not part of dependencies (#3139) 2025-05-17 20:04:05 +02:00
homarr-renovate[bot] 15675d95bd chore(deps): update dependency semantic-release to ^24.2.4 (#3134)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-17 01:13:09 +00:00
homarr-crowdin[bot] 16e2f3f1e4 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-17 00:50:13 +00:00
homarr-renovate[bot] bf6b7912f9 chore(deps): update dependency eslint to ^9.27.0 (#3128)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-16 20:24:10 +00:00
homarr-renovate[bot] ee8870b66b fix(deps): update dependency @tabler/icons-react to ^3.33.0 (#3132)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-16 20:23:22 +00:00
Meier Lukas c6464e582e chore(issue-template): add version 1.20.0 2025-05-16 21:27:28 +02:00
homarr-renovate[bot] 886e95d517 fix(deps): update dependency zod-validation-error to ^3.4.1 (#3127)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-16 19:19:41 +00:00
Releases Homarr 25cf9ad7ca chore(release): 1.20.0 [skip ci] 2025-05-16 19:18:07 +00:00
homarr-releases[bot] dec3e9a653 chore(release): automatic release v1.20.0 2025-05-16 19:15:02 +00:00
Meier Lukas ef9a5e9895 feat(integration): improve integration test connection (#3005) 2025-05-16 20:59:12 +02:00
Meier LukasandManuel 3daf1c8341 feat(auth): add account linking for oidc providers (#3106)
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
2025-05-16 20:57:51 +02:00
Meier Lukas b86159ba7c fix(calendar): some timezones result in offset of one day (#3120) 2025-05-16 18:13:05 +00:00
Meier Lukas 42e7fe3273 fix(deps): missing dependencies in definitions package for docs link codegen (#3119) 2025-05-16 20:10:17 +02:00
homarr-update-contributors[bot] dd250660b8 chore: update contributors (#3125)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2025-05-16 18:02:43 +00:00
Meier Lukas 6e03e5a40c fix: updated contributors contain unnecessary data (#3124) 2025-05-16 19:57:20 +02:00
tech62andMeier Lukas 9b8320c10d feat: add css class to logo for custom css (#3121)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-05-16 15:12:46 +00:00
Andre Silva 94134303d7 feat(releases-widget): added auto icon search when editing a repository (#3087) 2025-05-16 16:33:23 +02:00
homarr-renovate[bot] b87e6bcaf2 chore(deps): update node.js to v22.15.1 (#3116)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-16 16:19:23 +02:00
homarr-renovate[bot] 5b16468cab fix(deps): update dependency sass to ^1.89.0 (#3118)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-16 01:16:45 +00:00
homarr-renovate[bot] b32278164e chore(deps): update dependency prettier-plugin-packagejson to ^2.5.13 (#3115)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-15 12:35:54 +00:00
homarr-renovate[bot] cb81efa42c fix(deps): update testcontainers-node monorepo to ^10.26.0 (#3114)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-15 10:23:42 +00:00
homarr-renovate[bot] dc34048ec2 chore(deps): update node.js to v22.15.1 (#3110)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-14 21:19:11 +00:00
homarr-renovate[bot] 25efe3d335 fix(deps): update mantine monorepo to ^8.0.1 (#3107)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-14 13:34:50 +00:00
homarr-renovate[bot] d96bc898fd fix(deps): update dependency @extractus/feed-extractor to v7.1.6 (#3105)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-14 09:21:16 +00:00
homarr-renovate[bot] 56427ef794 chore(deps): update dependency @types/node to ^22.15.18 (#3103)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-14 05:22:20 +00:00
homarr-renovate[bot] c1e5d77176 fix(deps): update tanstack-query monorepo to ^5.76.1 (#3101)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-13 22:20:30 +00:00
homarr-renovate[bot] 6703155c61 chore(deps): update pnpm to v10.11.0 (#3097)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-13 15:57:03 +00:00
homarr-renovate[bot] f776cd6934 chore(deps): update dependency @types/react-dom to v19.1.5 (#3096)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-13 11:19:02 +00:00
homarr-renovate[bot] 93b122c655 chore(deps): update dependency @types/react to v19.1.4 (#3095)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-12 20:23:30 +00:00
homarr-renovate[bot] 42b377d067 fix(deps): update dependency typescript-eslint to ^8.32.1 (#3093)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-12 18:27:38 +00:00
Meier Lukas 6c71724e2e ci: add update-contributors workflow (#3014) 2025-05-12 18:01:27 +02:00
Andre Silva 593d97e27a fix(releases-widget): reduced padding for ul in release description (#3085) 2025-05-12 16:00:01 +00:00
homarr-renovate[bot] ac49910e6e chore(deps): update dependency @types/react-dom to v19.1.4 (#3092)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-12 12:35:06 +00:00
Andre Silva 18fe8ecd39 fix(releases-widget): validation preventing removal of a previously added icon (#3086)
* fix(releases-widget): validation preventing removal of a previously added icon

* fix(releases-widget): bug with input controlled/uncontrolled state
2025-05-12 10:50:16 +00:00
homarr-renovate[bot] 388bc7392c fix(deps): update tanstack-query monorepo to ^5.76.0 (#3090)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-12 09:23:35 +00:00
homarr-crowdin[bot] 2f9f561a8e chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-12 00:54:31 +00:00
Andre Silva 72862efae2 fix(releases-widget): delete the empty repository when canceling Add Reposotory (#3082) 2025-05-11 20:11:55 +00:00
homarr-renovate[bot] 5686910a47 fix(deps): update dependency bcrypt to v6 (#3083)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-11 21:58:25 +02:00
Andre Silva 59cae02623 fix(releases-widget): removed version regex in options overview (#3081) 2025-05-11 15:30:31 +00:00
Meier Lukas e931b19661 chore(docs): update docs-sitemap 2025-05-11 15:38:24 +02:00
homarr-crowdin[bot] 8ff01eb17f chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-11 00:56:05 +00:00
homarr-renovate[bot] b0f7310cb7 fix(deps): update dependency sass to ^1.88.0 (#3078)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-10 23:20:18 +00:00
8c1b365733 feat(releases-widget): limit number of displayed releases and custom name for repositories (#2974)
Co-authored-by: Andre Silva <asilva01@acuitysso.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-05-10 22:06:14 +02:00
homarr-renovate[bot] b8bdf1836a chore(deps): update dependency proxmox-api&gt;undici to v7.9.0 (#3076)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-10 20:00:30 +00:00
homarr-renovate[bot] 01d53eee60 fix(deps): update dependency undici to v7.9.0 (#3077)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-10 19:18:44 +00:00
Meier Lukas 7f3574f8ba chore(issue-template): add version 1.19.1 2025-05-10 15:27:37 +02:00
homarr-renovate[bot] 3f9f94ea45 fix(deps): update dependency @auth/drizzle-adapter to ^1.9.1 (#3073)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-10 07:19:27 +00:00
homarr-renovate[bot] 98773e7dba fix(deps): update dependency next-auth to v5.0.0-beta.28 (#3074)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-10 06:25:49 +00:00
homarr-renovate[bot] 0d2311975c fix(deps): update dependency @auth/core to ^0.39.1 (#3072)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-10 06:24:50 +00:00
homarr-crowdin[bot] a3af95734e chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-10 00:49:16 +00:00
homarr-renovate[bot] 17c4090024 chore(deps): update dependency prettier-plugin-packagejson to ^2.5.12 (#3066)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-09 20:23:33 +00:00
Releases Homarr 93b9c4f6f3 chore(release): 1.19.1 [skip ci] 2025-05-09 19:15:08 +00:00
homarr-releases[bot] 1def17f4b5 chore(release): automatic release v1.19.1 2025-05-09 19:14:15 +00:00
b62d47d303 fix(releases-widget): improved provider parsing of missing properties (#2964)
Co-authored-by: Andre Silva <asilva01@acuitysso.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-05-09 17:30:46 +00:00
homarr-renovate[bot] b71c5c6fc7 fix(deps): update tanstack-query monorepo to ^5.75.7 (#3063)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-09 17:16:16 +00:00
Eugen StanandMeier Lukas 2bdb1c75f6 fix(logs): log error cause when object, sign in oidc debug messages (#3054)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-05-09 13:27:06 +02:00
347c6d1519 fix(releases-widget): error display, decouple database repository object from responses and batch widget queries (#2891)
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
Co-authored-by: Andre Silva <asilva01@acuitysso.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-05-09 13:24:37 +02:00
homarr-renovate[bot] d6180ebc73 fix(deps): update tanstack-query monorepo to ^5.75.6 (#3062)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-09 10:21:05 +00:00
homarr-renovate[bot] 395e03c810 fix(deps): update dependency eslint-config-prettier to ^10.1.5 (#3061)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-09 04:25:37 +00:00
homarr-renovate[bot] f26634745e chore(deps): update dependency @types/node to ^22.15.17 (#3057)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-08 14:21:10 +00:00
homarr-renovate[bot] 0ab2ae4cbb chore(deps): update dependency @types/node to ^22.15.16 (#3052)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-08 04:26:28 +00:00
homarr-renovate[bot] 0d5f430bd4 fix(deps): update dependency better-sqlite3 to ^11.10.0 (#3053)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-08 04:25:27 +00:00
homarr-renovate[bot] dd300c2e1f chore(deps): update dependency @types/node to ^22.15.15 (#3050)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-07 16:26:29 +00:00
homarr-renovate[bot] 8cb9ddbce3 fix(deps): update tanstack-query monorepo to ^5.75.5 (#3048)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-07 08:26:58 +00:00
homarr-renovate[bot] e94c05f3d1 fix(deps): update dependency eslint-config-prettier to ^10.1.3 (#3047)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-07 07:22:09 +00:00
homarr-renovate[bot] b6eb8ca0f6 fix(deps): update nextjs monorepo to v15.3.2 (#3045)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-06 23:21:35 +00:00
homarr-renovate[bot] 9b6110b9c9 fix(deps): update turbo monorepo to ^2.5.3 (#3043)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-06 21:21:12 +00:00
homarr-renovate[bot] 273d4edb23 fix(deps): update dependency @kubernetes/client-node to ^1.2.0 (#3042)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-06 19:17:55 +00:00
homarr-renovate[bot] 36bd2a51b5 chore(deps): update dependency @types/node to ^22.15.14 (#3041)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-06 18:27:40 +00:00
homarr-renovate[bot]andMeier Lukas 44853ea64b fix(deps): update mantine monorepo to v8 (major) (#3022)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-05-06 20:02:20 +02:00
homarr-renovate[bot] 680f7b0c72 fix(deps): update dependency react-error-boundary to v6 (#3016)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-06 19:39:15 +02:00
homarr-renovate[bot] 7c0baf5fba fix(deps): update dependency ldapts to v8 (#3028)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-06 19:38:53 +02:00
homarr-renovate[bot] 57264a5d6c chore(deps): update dependency @types/node to ^22.15.13 (#3040)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-06 17:21:05 +00:00
homarr-renovate[bot] 7ec1793040 fix(deps): update dependency jotai to ^2.12.4 (#3038)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-06 13:34:27 +00:00
homarr-renovate[bot] 2568a9cecd chore(deps): update dependency @types/node to ^22.15.12 (#3037)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-06 09:21:47 +00:00
homarr-renovate[bot] 861728a7c3 fix(deps): update tanstack-query monorepo to ^5.75.4 (#3029)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-06 08:27:10 +00:00
homarr-renovate[bot] 0284c5ec39 chore(deps): update dependency @types/node to ^22.15.11 (#3034)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-06 03:13:19 +00:00
homarr-renovate[bot] 3315053ba1 chore(deps): update dependency @types/react to v19.1.3 (#3035)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-06 03:12:31 +00:00
homarr-crowdin[bot] fce8bfe1f4 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-06 00:51:40 +00:00
homarr-renovate[bot] cb50eda8fb chore(deps): update dependency @types/node to ^22.15.9 (#3032)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-05 23:21:08 +00:00
homarr-renovate[bot] bee1dddc9f fix(deps): update dependency typescript-eslint to ^8.32.0 (#3027)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-05 21:22:55 +00:00
homarr-renovate[bot] 0e7eae4553 chore(deps): update dependency @types/node to ^22.15.7 (#3031)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-05 21:21:08 +00:00
homarr-renovate[bot] c36e1708be chore(deps): update dependency @types/node to ^22.15.5 (#3030)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-05 20:23:24 +00:00
homarr-renovate[bot] f1c001d728 chore(deps): update vitest monorepo to ^3.1.3 (#3024)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-05 14:23:48 +00:00
homarr-renovate[bot] 8a0557f927 fix(deps): update tiptap monorepo to v2.12.0 (#3025)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-05 14:22:25 +00:00
homarr-renovate[bot] 4aa6fbb42d chore(deps): update dependency prettier-plugin-packagejson to ^2.5.11 (#3023)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-05 13:33:38 +00:00
homarr-renovate[bot] e5e686d590 fix(deps): update dependency zod to ^3.24.4 (#3021)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-05 01:17:37 +00:00
homarr-renovate[bot] 758b1a0490 fix(deps): update dependency @extractus/feed-extractor to v7.1.5 (#3020)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-04 14:18:25 +00:00
homarr-renovate[bot] d512fc042a fix(deps): update tanstack-query monorepo to ^5.75.2 (#3018)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-04 08:39:09 +00:00
homarr-crowdin[bot] d401086d1b chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-04 00:57:16 +00:00
homarr-renovate[bot] 76ce6ff1fa fix(deps): update dependency drizzle-kit to ^0.31.1 (#3013)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-03 12:31:28 +00:00
homarr-renovate[bot] 9bb26ebcc3 fix(deps): update dependency ws to ^8.18.2 (#3012)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-03 06:25:50 +00:00
Meier Lukas a87bb54404 chore(issue-template): add version 1.19.0 2025-05-03 00:22:48 +02:00
homarr-renovate[bot] a9aa0e303b chore(deps): update dependency eslint to ^9.26.0 (#3008)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-02 22:21:25 +00:00
Releases Homarr dbae2d2fdd chore(release): 1.19.0 [skip ci] 2025-05-02 19:15:22 +00:00
homarr-releases[bot] b9b2398cd6 chore(release): automatic release v1.19.0 2025-05-02 19:14:41 +00:00
Meier Lukas 82c5361112 feat(widget): add prefetch for apps and bookmarks (#2895) 2025-05-02 19:23:15 +02:00
homarr-renovate[bot] 2b1c433cef fix(deps): update tanstack-query monorepo to ^5.75.1 (#3006)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-02 12:33:30 +00:00
homarr-renovate[bot] a70b0b7047 fix(deps): update mantine monorepo to ^7.17.7 (#3004)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-02 09:22:12 +00:00
homarr-crowdin[bot] e148698593 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-02 00:51:01 +00:00
homarr-renovate[bot] 0d1b40c381 fix(deps): update tanstack-query monorepo to ^5.75.0 (#2998)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-01 14:19:26 +00:00
homarr-renovate[bot] a934529146 fix(deps): update testcontainers-node monorepo to ^10.25.0 (#2997)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-05-01 11:19:58 +00:00
Meier Lukas 0c9c5b8955 fix(unifi): port is ignored (#2995) 2025-05-01 12:51:20 +02:00
Meier Lukas 4dfde2ba05 refactor: remove unused pretty-print-error dependency (#2992) 2025-05-01 10:08:52 +02:00
Meier Lukas 48aef9e862 refactor: replace deprecated unsable_concat with concat for trpc middlewares (#2991) 2025-05-01 10:08:41 +02:00
Meier Lukas 547a8c34c5 fix(cookie): parsing of header not working with percentage char (#2989) 2025-05-01 10:08:29 +02:00
Meier Lukas 9006d3d2fc fix(media-request): add missing completed status (#2990) 2025-05-01 10:08:19 +02:00
homarr-crowdin[bot] a2934247b2 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-05-01 00:57:45 +00:00
homarr-renovate[bot] 1a07fa9af5 fix(deps): update dependency @t3-oss/env-nextjs to ^0.13.4 (#2993)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-30 21:21:02 +00:00
Meier Lukas ec11c738f2 chore(docs): update docs-sitemap 2025-04-30 21:06:24 +02:00
homarr-renovate[bot] 254c248e12 chore(deps): update dependency @types/react-dom to v19.1.3 (#2988)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-30 11:18:12 +00:00
homarr-renovate[bot] 70686a0066 fix(deps): update tiptap monorepo to v2.11.9 (#2987)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-30 08:45:52 +00:00
homarr-renovate[bot] 3299cedc04 fix(deps): update tanstack-query monorepo to ^5.74.11 (#2984)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-29 18:26:21 +00:00
homarr-renovate[bot] 1012cad0e3 fix(deps): update tanstack-query monorepo to ^5.74.9 (#2983)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-29 11:19:25 +00:00
homarr-renovate[bot] b10f33bf0b fix(deps): update trpc monorepo to ^11.1.2 (#2982)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-29 07:21:16 +00:00
homarr-renovate[bot] a2dbac5ad7 fix(deps): update dependency tsx to v4.19.4 (#2981)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-29 06:27:59 +00:00
Andre Silva f39a21c296 fix(releases-widget): improve ui for smaller widgets (#2962) 2025-04-28 19:04:38 +00:00
homarr-renovate[bot] 284ea239f7 fix(deps): update dependency typescript-eslint to ^8.31.1 (#2979)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-28 18:28:13 +00:00
homarr-renovate[bot] 583a6b6f4c chore(deps): update dependency @types/node to ^22.15.3 (#2978)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-28 07:47:26 +00:00
homarr-renovate[bot] 039fad643e chore(deps): update pnpm to v10.10.0 (#2976)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-28 01:13:56 +00:00
homarr-crowdin[bot] 2f2c00ba3d chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-28 00:52:47 +00:00
Meier Lukas ebb3012a65 Merge branch 'main' into dev 2025-04-27 17:24:06 +02:00
homarr-renovate[bot] f4ce76ac13 fix(deps): update tanstack-query monorepo to ^5.74.7 (#2924)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-27 15:19:19 +00:00
Releases Homarr 830660b1ef chore(release): 1.18.1 [skip ci] 2025-04-27 15:17:33 +00:00
homarr-releases[bot] f7451d182a chore(release): automatic release v1.18.1 2025-04-27 15:16:33 +00:00
Meier Lukas bb92f5bb79 fix: cannot find module http-cookie-agent/http (#2972) 2025-04-27 17:12:03 +02:00
homarr-crowdin[bot] f7ff1e3911 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-27 13:58:54 +00:00
homarr-renovate[bot] 5f80bebef2 fix(deps): update dependency mysql2 to v3.14.1 (#2969)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-27 04:22:58 +00:00
homarr-crowdin[bot] 97fe1f6a3e chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-26 00:48:49 +00:00
homarr-renovate[bot] 35fee2f380 chore(deps): update dependency @semantic-release/github to ^11.0.2 (#2956)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-25 21:20:30 +00:00
Manuel e1e26b27de docs: add 1.18.0 to issue template (#2951) 2025-04-25 19:19:14 +00:00
Releases Homarr 55cdddb274 chore(release): 1.18.0 [skip ci] 2025-04-25 19:15:32 +00:00
homarr-releases[bot] dd85f5a9d5 chore(release): automatic release v1.18.0 2025-04-25 19:14:47 +00:00
Manuel 8550c69d51 feat: migrate to unifi package (#2894) 2025-04-25 18:58:55 +00:00
3dcee8cb86 feat: add releases widget (#2497)
Co-authored-by: Andre Silva <asilva01@acuitysso.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
2025-04-25 18:49:32 +00:00
homarr-renovate[bot] d97e74047d fix(deps): update turbo monorepo to ^2.5.2 (#2949)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-25 16:24:57 +00:00
homarr-renovate[bot] e36892fc8f chore(deps): update dependency @types/node to ^22.15.2 (#2946)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-25 09:22:12 +00:00
homarr-renovate[bot] 1d28bf4c93 fix(deps): update dependency drizzle-orm to ^0.43.1 (#2947)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-25 08:26:07 +00:00
homarr-renovate[bot] 8b076ce761 chore(deps): update dependency @types/node to ^22.15.1 (#2945)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-25 04:24:44 +00:00
homarr-renovate[bot] d78b2e1e85 fix(deps): update dependency tsdav to ^2.1.4 (#2944)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-25 03:09:10 +00:00
homarr-renovate[bot] f65c0125a9 chore(deps): update dependency @types/node to ^22.15.0 (#2942)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-24 21:21:27 +00:00
homarr-renovate[bot] 253308357a fix(deps): update dependency next-auth to v5.0.0-beta.27 (#2940)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-24 17:19:21 +00:00
homarr-renovate[bot] 295cc4a3c4 fix(deps): update dependency drizzle-orm to ^0.43.0 (#2939)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-24 14:20:39 +00:00
homarr-renovate[bot] 767fe946d7 fix(deps): update dependency next-intl to v4.1.0 (#2938)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-24 09:24:32 +00:00
homarr-renovate[bot] aba963c7cf fix(deps): update mantine monorepo to ^7.17.5 (#2937)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-24 09:23:37 +00:00
homarr-crowdin[bot] 4529cabb8a chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-24 04:29:49 +00:00
homarr-renovate[bot] 7766c3cbbc fix(deps): update turbo monorepo to ^2.5.1 (#2934)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-23 23:21:49 +00:00
homarr-renovate[bot] eb95ee285b fix(deps): update dependency @t3-oss/env-nextjs to ^0.13.0 (#2935)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-23 22:22:00 +00:00
homarr-renovate[bot] 1421c381cc chore(deps): update node.js to v22.15.0 (#2932)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-23 20:22:21 +02:00
homarr-renovate[bot] 573d5f89d6 fix(deps): update dependency glob to ^11.0.2 (#2931)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-23 16:24:21 +00:00
homarr-renovate[bot] e01b909402 chore(deps): update node.js to v22.15.0 (#2928)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-23 08:27:13 +00:00
homarr-renovate[bot] bd3535dcf8 fix(deps): update trpc monorepo to ^11.1.1 (#2927)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-23 01:13:29 +00:00
homarr-crowdin[bot] c4bd513134 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-23 00:50:32 +00:00
Meier Lukas c64d903f2b feat(widgets): add title to advanced options (#2909) 2025-04-22 18:33:15 +02:00
Meier Lukas f98750d0b3 feat(media-server): add option to only show playing sessions (#2899) 2025-04-22 18:30:46 +02:00
Meier Lukas c51424717d fix(certificates): improve validation and prevent crash (#2910) 2025-04-22 18:28:58 +02:00
homarr-renovate[bot] 3172e6e0c4 fix(deps): update dependency next-intl to v4.0.3 (#2922)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-22 16:25:51 +00:00
homarr-renovate[bot] 7aa379076d fix(deps): update dependency typescript-eslint to ^8.31.0 (#2919)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-22 16:25:09 +00:00
homarr-renovate[bot] 023ffcf7bf chore(deps): update dependency eslint to ^9.25.1 (#2918)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-22 01:13:02 +00:00
homarr-renovate[bot] fed28bd775 fix(deps): update dependency sass to ^1.87.0 (#2921)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-21 23:20:50 +00:00
homarr-renovate[bot] 09718b8258 chore(deps): update vitest monorepo to ^3.1.2 (#2915)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-21 09:22:24 +00:00
homarr-renovate[bot] 6040f063ca chore(deps): update pnpm to v10.9.0 (#2916)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-21 09:21:11 +00:00
homarr-renovate[bot] 3d508088d5 fix(deps): update dependency @ctrl/qbittorrent to ^9.6.0 (#2914)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-21 03:13:01 +00:00
homarr-crowdin[bot] 37484ce032 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-21 00:52:28 +00:00
Meier Lukas ac3c2a6377 chore(issue-template): add missing homarr version 1.17.0 2025-04-21 00:34:42 +02:00
homarr-renovate[bot]andMeier Lukas 6e6edc8a0d fix(deps): update dependency next-auth to v5.0.0-beta.26 (#2906)
* fix(deps): update dependency next-auth to v5.0.0-beta.26

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

* fix: lint issue

* fix: format issue

* fix: login with oidc no longer working

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-04-20 12:21:33 +00:00
homarr-renovate[bot] b0d7f9a48c fix(deps): update dependency @auth/core to ^0.39.0 (#2904)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-20 08:56:24 +00:00
homarr-renovate[bot] fdf700dba5 fix(deps): update dependency @auth/drizzle-adapter to ^1.9.0 (#2905)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-20 06:24:43 +00:00
homarr-renovate[bot] 2bf67c3de8 fix(deps): update dependency trpc-to-openapi to ^2.2.0 (#2903)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-19 23:19:45 +00:00
homarr-renovate[bot] 593e3ee5a6 fix(deps): update dependency dockerode to ^4.0.6 (#2901)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-19 13:03:16 +00:00
homarr-renovate[bot] ad5da4e8ac chore(deps): update dependency @vitejs/plugin-react to ^4.4.1 (#2902)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-19 12:26:25 +00:00
Releases Homarr 58ef0960b5 chore(release): 1.17.0 [skip ci] 2025-04-18 19:14:53 +00:00
homarr-releases[bot] 68d6967d24 chore(release): automatic release v1.17.0 2025-04-18 19:14:04 +00:00
homarr-renovate[bot] e5f0bb9792 chore(deps): update dependency eslint to ^9.25.0 (#2897)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-18 18:46:49 +00:00
Jacob HemmingandMeier Lukas 3d68f4d128 feat: add api-key support for tdarr (#2890)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-04-18 20:11:51 +02:00
Jacob Hemming cfca0ba583 fix: calculate available space correctly and filter unmounted drives (#2885) 2025-04-18 10:49:59 +00:00
Meier Lukas 9570abb2c3 chore(docs): update docs-sitemap 2025-04-18 10:52:02 +02:00
homarr-renovate[bot] 5fcb7a3b9c fix(deps): update dependency recharts to ^2.15.3 (#2893)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-18 05:18:26 +00:00
homarr-crowdin[bot] a55acb9eb9 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-18 00:49:15 +00:00
homarr-renovate[bot] d37cdbd080 fix(deps): update nextjs monorepo to v15.3.1 (#2889)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-17 15:56:13 +00:00
homarr-renovate[bot] 17174b6b96 fix(deps): update tanstack-query monorepo to ^5.74.4 (#2888)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-17 09:54:31 +00:00
homarr-crowdin[bot] 0466a2808a chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-17 00:49:43 +00:00
homarr-renovate[bot] 1bfe397d68 fix(deps): update dependency zod to ^3.24.3 (#2886)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-16 22:40:20 +00:00
Meier Lukas 60de6361bb chore(docs): update docs-sitemap 2025-04-16 22:58:28 +02:00
homarr-renovate[bot] 47d7abc0d7 fix(deps): update dependency drizzle-orm to ^0.42.0 (#2882)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-16 18:51:44 +00:00
rezstjeandManuel 2d12c1e641 fix: Tdarr widget empty statistics tab (#2883)
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
2025-04-16 18:34:06 +00:00
Thomas Camlong 00053bda7b feat: Prefer primary color (#2665) 2025-04-16 11:34:33 +02:00
homarr-renovate[bot] 97390c6030 fix(deps): update dependency drizzle-kit to ^0.31.0 (#2881)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-15 13:58:27 +00:00
homarr-renovate[bot] 865a52ae7d chore(deps): update dependency @vitejs/plugin-react to ^4.4.0 (#2879)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-15 09:40:02 +00:00
homarr-renovate[bot] fdca513929 fix(deps): update dependency typescript-eslint to ^8.30.1 (#2876)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-15 01:02:52 +00:00
homarr-crowdin[bot] ce5784b6c4 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-15 00:50:50 +00:00
homarr-renovate[bot] 1a4c136d41 chore(deps): update dependency @types/react to v19.1.2 (#2874)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-14 19:17:29 +00:00
homarr-renovate[bot] 089734b293 fix(deps): update dependency typescript-eslint to ^8.30.0 (#2873)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-14 17:37:45 +00:00
homarr-renovate[bot] 2e59bfcd60 chore(deps): update dependency @types/dockerode to ^3.3.38 (#2872)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-14 15:55:56 +00:00
homarr-renovate[bot] 02fb1b9dae fix(deps): update tanstack-query monorepo to ^5.74.3 (#2871)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-14 13:09:11 +00:00
homarr-renovate[bot] bf4a1cf1e8 fix(deps): update tanstack-query monorepo to ^5.74.2 (#2869)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-14 11:39:37 +00:00
homarr-renovate[bot] 4a0fb5d0bb chore(deps): update pnpm to v10.8.1 (#2870)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-14 11:31:01 +00:00
homarr-renovate[bot] c57b63b506 fix(deps): update dependency jotai to ^2.12.3 (#2868)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-14 01:03:30 +00:00
homarr-crowdin[bot] fa0e694ce0 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-14 00:52:46 +00:00
homarr-renovate[bot] ff1263b217 fix(deps): update dependency swagger-ui-react to ^5.21.0 (#2866)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-13 19:53:29 +00:00
homarr-renovate[bot] ac78e87042 fix(deps): update tanstack-query monorepo to ^5.74.0 (#2864)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-13 18:21:57 +00:00
homarr-renovate[bot] ec7adb03b6 chore(deps): update dependency jsdom to ^26.1.0 (#2863)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-13 10:41:29 +00:00
homarr-crowdin[bot] f5f39bc094 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-13 02:16:30 +00:00
homarr-renovate[bot] 4e6ab037c9 fix(deps): update tanstack-query monorepo to ^5.73.3 (#2860)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-12 16:18:00 +00:00
Andrea a29556ee8e fix(widget): format stock values and changes for better readability (#2836) 2025-04-12 15:53:40 +00:00
homarr-crowdin[bot] 71d8f7920d chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-12 00:48:49 +00:00
homarr-renovate[bot] 835e007334 chore(deps): update dependency @types/node to ^22.14.1 (#2857)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-11 21:43:33 +00:00
Meier Lukas d46b8f63c7 chore(issue-template): add missing homarr version 1.16.0 2025-04-11 21:56:00 +02:00
Releases Homarr a40ebb10c7 chore(release): 1.16.0 [skip ci] 2025-04-11 19:15:22 +00:00
homarr-releases[bot] 05dd6227fb chore(release): automatic release v1.16.0 2025-04-11 19:14:40 +00:00
Thomas CamlongandMeier Lukas 7a3c836a70 feat(boards): add quick app add menu item (#2681)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-04-11 18:55:00 +00:00
Meier Lukas 4baec7e3ff fix(boards): add missing upload button for background in settings (#2853) 2025-04-11 20:17:10 +02:00
homarr-renovate[bot] 93bd09c85e chore(deps): update dependency @types/react to v19.1.1 (#2852)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-11 15:54:41 +00:00
homarr-renovate[bot] 0ed6a79a5b fix(deps): update dependency ioredis to v5.6.1 (#2851)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-11 13:08:22 +00:00
homarr-renovate[bot] 0fe4ea9dd9 fix(deps): update trpc monorepo to ^11.1.0 (#2850)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-11 12:27:29 +00:00
homarr-crowdin[bot] d6a48d132a chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-11 00:50:01 +00:00
homarr-renovate[bot] c84dd2fd79 fix(deps): update dependency dotenv to ^16.5.0 (#2848)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-10 22:39:51 +00:00
Kudou Sterain 94263c445b feat: support aria2 integration (#2226) 2025-04-10 19:40:40 +00:00
homarr-renovate[bot] 4b0b892250 fix(deps): update dependency octokit to ^4.1.3 (#2847)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-10 15:20:33 +00:00
homarr-renovate[bot] 057ff2cfc1 fix(deps): update testcontainers-node monorepo to ^10.24.2 (#2846)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-10 13:45:18 +00:00
homarr-renovate[bot] 45d285a76b fix(deps): update dependency swagger-ui-react to ^5.20.8 (#2844)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-10 11:55:51 +00:00
homarr-renovate[bot] 924a7d076e fix(deps): update dependency eslint-config-prettier to ^10.1.2 (#2842)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-10 10:58:06 +00:00
homarr-renovate[bot] f13be4a708 fix(deps): update dependency undici to v7.8.0 (#2841)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-10 10:56:42 +00:00
homarr-renovate[bot] 7fb6beceef chore(deps): update dependency proxmox-api&gt;undici to v7.8.0 (#2840)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-10 10:18:59 +00:00
homarr-renovate[bot] a1fa4297ea fix(deps): update nextjs monorepo to v15.3.0 (#2838)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-09 20:38:56 +00:00
homarr-renovate[bot] f5e3138c81 fix(deps): update tanstack-query monorepo to ^5.72.2 (#2834)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-09 14:59:53 +00:00
Meier Lukas d21755426f chore(issue-template): add missing homarr version 1.15.0 2025-04-09 16:38:50 +02:00
Releases Homarr 5b332baa75 chore(release): 1.15.0 [skip ci] 2025-04-09 13:27:11 +00:00
homarr-releases[bot] d0b9cbf155 chore(release): automatic release v1.15.0 2025-04-09 13:26:13 +00:00
Meier Lukas 3948f7f9a4 fix(security): missing authorization check for changes to notebook widget (#2832) 2025-04-09 15:24:37 +02:00
homarr-renovate[bot] 092c95bbe0 fix(deps): update testcontainers-node monorepo to ^10.24.1 (#2831)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-09 10:49:01 +00:00
homarr-renovate[bot] 0fc7ca7738 fix(deps): update nextjs monorepo to v15.2.5 (#2828)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-09 07:53:31 +00:00
homarr-renovate[bot] da34b0a3cf chore(deps): update dependency @types/react-dom to v19.1.2 (#2830)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-09 07:45:00 +00:00
homarr-renovate[bot] 75100d1a49 fix(deps): update trpc monorepo to ^11.0.4 (#2827)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-08 22:45:13 +00:00
homarr-renovate[bot] bd5dcb991c fix(deps): update mantine monorepo to ^7.17.4 (#2824)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-08 14:40:46 +00:00
homarr-renovate[bot] b064304201 fix(deps): update dependency @kubernetes/client-node to ^1.1.2 (#2825)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-08 14:18:46 +00:00
homarr-renovate[bot] c01d9a4dc5 fix(deps): update tanstack-query monorepo to ^5.72.1 (#2823)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-08 13:08:29 +00:00
homarr-renovate[bot] a821bd71f4 fix(deps): update trpc monorepo to ^11.0.3 (#2822)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-08 07:34:05 +00:00
homarr-renovate[bot] 17d92727f2 fix(deps): update dependency typescript-eslint to ^8.29.1 (#2821)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-07 17:37:29 +00:00
homarr-renovate[bot] 8ef2661be4 chore(deps): update pnpm to v10.8.0 (#2820)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-07 17:16:45 +00:00
homarr-renovate[bot] df212cf511 fix(deps): update tanstack-query monorepo to ^5.72.0 (#2817)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-07 09:38:19 +00:00
homarr-renovate[bot] 85f8d23da3 fix(deps): update dependency swagger-ui-react to ^5.20.7 (#2816)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-07 07:46:05 +00:00
homarr-renovate[bot] 275224ee68 fix(deps): update dependency ldapts to v7.4.0 (#2815)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-07 03:22:37 +00:00
homarr-crowdin[bot] ffd1005ff1 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-07 00:50:37 +00:00
Meier Lukas bf046a715c fix(medias): upload size limited by nginx to 1mb (#2813) 2025-04-06 21:33:47 +02:00
Meier Lukas c960c6fc79 chore(issue-template): add missing homarr version 1.14.0 2025-04-06 20:24:05 +02:00
Meier Lukas 75ba3f2ae7 refactor: remove central validation export to improve typescript performance (#2810)
* refactor: remove central validation export to improve typescript performance

* fix: missing package exports change in validation package

* chore: address pull request feedback
2025-04-06 10:37:28 +00:00
c1cd563048 feat: unifi controller integration (#2236)
* feat: unifi controller integration

* fix: pr feedback

* fix: pr feedback

* fix: pr feedback

* fix: formatting

* fix: pr feedback

* fix: typecheck

---------

Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-04-06 10:17:51 +00:00
homarr-crowdin[bot] 7caad6fc47 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-06 00:52:20 +00:00
Meier Lukas e07b9e6a88 feat(boards): add upload buttons for background, favicon and logo (#2770)
* feat(boards): add upload buttons for background, favicon and logo

* feat(boards): use media search and show names for background

* fix: background preview not working
2025-04-05 13:54:16 +02:00
homarr-renovate[bot] 14bff5d530 chore(deps): update dependency typescript to ^5.8.3 (#2805)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-05 08:39:48 +00:00
Meier Lukas 3e88ecd64a chore(docs): update docs-sitemap 2025-04-05 10:17:53 +02:00
Meier Lukas c6102edff4 fix(performance): remove turbopack flag to no longer go out of memory after (#2803) 2025-04-04 22:30:00 +02:00
homarr-renovate[bot] 646e2c12e6 chore(deps): update dependency eslint to ^9.24.0 (#2802)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-04 19:31:51 +00:00
Releases Homarr 95e7136284 chore(release): 1.14.0 [skip ci] 2025-04-04 19:15:57 +00:00
homarr-releases[bot] 9de9aa59a4 chore(release): automatic release v1.14.0 2025-04-04 19:15:16 +00:00
homarr-renovate[bot] ee12461452 chore(deps): update dependency @types/better-sqlite3 to v7.6.13 (#2800)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-04 18:46:53 +00:00
homarr-renovate[bot] b03941ed80 fix(deps): update dependency trpc-to-openapi to ^2.1.5 (#2799)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-04 17:53:50 +00:00
homarr-renovate[bot] 968a82baf5 fix(deps): update dependency swagger-ui-react to ^5.20.6 (#2798)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-04 14:19:15 +00:00
homarr-renovate[bot] ea8bf43d7b fix(deps): update dependency @ctrl/qbittorrent to ^9.5.2 (#2795)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-04 13:34:29 +00:00
homarr-renovate[bot] 964368a06c fix(deps): update tanstack-query monorepo to ^5.71.10 (#2790)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-04 13:06:22 +00:00
homarr-renovate[bot] 98ecc683e5 fix(deps): update dependency sass to ^1.86.3 (#2796)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-04 01:01:37 +00:00
Meier Lukas 29ff879dfd fix: update check blocks page loading (#2782) 2025-04-03 23:22:50 +02:00
Meier Lukas 674253262b feat(cli): add cli command to recreate credentials admin (#2779)
* feat(cli): add cli command to recreate credentials admin

* fix: add missing username validation

* fix: deepsource issue
2025-04-03 23:22:31 +02:00
homarr-renovate[bot] f9aeb8ffe3 fix(deps): update dependency eslint-plugin-react to ^7.37.5 (#2794)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-03 20:21:26 +00:00
homarr-renovate[bot] 76b8b35cc1 fix(deps): update dependency @kubernetes/client-node to ^1.1.1 (#2793)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-03 20:18:43 +00:00
homarr-renovate[bot] 8886380f0a fix(deps): update trpc monorepo to ^11.0.2 (#2792)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-03 19:54:18 +00:00
homarr-renovate[bot] 0b41967ec3 fix(deps): update turbo monorepo to ^2.5.0 (#2791)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-03 17:19:01 +00:00
homarr-renovate[bot] 1b0504198b fix(deps): update testcontainers-node monorepo to ^10.24.0 (#2789)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-03 13:44:23 +00:00
homarr-renovate[bot] 3363eebddd fix(deps): update dependency swagger-ui-react to ^5.20.5 (#2787)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-03 11:16:57 +00:00
homarr-renovate[bot] 85672d9cf1 fix(deps): update dependency swagger-ui-react to ^5.20.4 (#2786)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-03 09:18:38 +00:00
homarr-renovate[bot] 3131082c77 fix(deps): update dependency sass to ^1.86.2 (#2784)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-03 06:59:52 +00:00
homarr-renovate[bot] f33b1cb5d0 fix(deps): update dependency recharts to ^2.15.2 (#2785)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-03 06:51:02 +00:00
homarr-renovate[bot] 4f32f11d8a chore(deps): update dependency @types/node to ^22.14.0 (#2781)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-02 18:58:38 +00:00
homarr-renovate[bot] 466d0e0053 fix(deps): update tanstack-query monorepo to ^5.71.5 (#2780)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-02 18:47:33 +00:00
Meier Lukas 7c4b71af3d chore(docs): update docs-sitemap 2025-04-02 20:19:12 +02:00
homarr-renovate[bot] 81d1231d8f fix(deps): update dependency swagger-ui-react to ^5.20.3 (#2777)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-02 11:32:28 +00:00
homarr-renovate[bot] 858fe6e19a fix(deps): update tanstack-query monorepo to ^5.71.3 (#2767)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-02 10:18:32 +00:00
homarr-renovate[bot] ff5c31342f fix(deps): update dependency undici to v7.7.0 (#2776)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-02 09:22:17 +00:00
homarr-renovate[bot] 807997ea76 chore(deps): update dependency proxmox-api&gt;undici to v7.7.0 (#2775)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-02 09:20:03 +00:00
homarr-renovate[bot] 9c12b18c6c chore(deps): update dependency @types/react-dom to v19.1.1 (#2774)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-02 09:18:21 +00:00
homarr-renovate[bot] 3951d052de chore(deps): update react monorepo to v19.1.0 (#2773)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-02 07:44:51 +00:00
homarr-renovate[bot] 5975b53212 chore(deps): update react monorepo (#2772)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-02 07:18:58 +00:00
homarr-renovate[bot] cc9522d981 chore(deps): update react monorepo (#2771)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-02 06:52:17 +00:00
Meier Lukas 7f77130978 fix(nextcloud): issue with self signed certificates (#2760) 2025-04-01 22:20:45 +02:00
homarr-renovate[bot] 243a97d637 chore(deps): update dependency @types/node to ^22.13.17 (#2769)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-01 17:18:29 +00:00
homarr-renovate[bot] 4feeb8b173 chore(deps): update pnpm to v10.7.1 (#2768)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-01 15:48:45 +00:00
homarr-renovate[bot] b1a8012bc7 chore(deps): update dependency @types/node to ^22.13.16 (#2766)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-01 09:56:53 +00:00
homarr-renovate[bot] 22b67188bc chore(deps): update dependency @types/node to ^22.13.15 (#2765)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-01 05:55:26 +00:00
homarr-renovate[bot] dd6e2c0f72 chore(deps): update dependency @types/ws to ^8.18.1 (#2764)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-04-01 03:40:34 +00:00
homarr-crowdin[bot] 9db0016252 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-04-01 00:56:05 +00:00
homarr-renovate[bot] 3e3505176a fix(deps): update dependency sass to ^1.86.1 (#2762)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-03-31 21:18:33 +00:00
homarr-renovate[bot] 3687c4b3d6 fix(deps): update dependency typescript-eslint to ^8.29.0 (#2761)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-03-31 17:37:16 +00:00
homarr-renovate[bot] 944010a7b1 chore(deps): update vitest monorepo to ^3.1.1 (#2758)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-03-31 10:43:18 +00:00
homarr-renovate[bot] db13fd03c0 fix(deps): update dependency dockerode to ^4.0.5 (#2757)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-03-31 09:20:46 +00:00
homarr-renovate[bot] 0b8a0098bd fix(deps): update tanstack-query monorepo to ^5.71.1 (#2756)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-03-31 08:51:16 +00:00
homarr-renovate[bot] 9a77b540bb chore(deps): update dependency @types/dockerode to ^3.3.37 (#2755)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-03-31 07:19:42 +00:00
homarr-crowdin[bot] c598579716 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-03-31 00:51:30 +00:00
homarr-renovate[bot] 59b1630532 fix(deps): update tiptap monorepo to v2.11.7 (#2750)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-03-30 15:42:50 +02:00
homarr-renovate[bot] 77044e8e0b fix(deps): update tanstack-query monorepo to ^5.71.0 (#2752)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-03-30 13:03:05 +00:00
homarr-renovate[bot] 0ec8f23b7c fix(deps): update tiptap monorepo to v2.11.7 (#2749)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-03-30 01:05:20 +00:00
homarr-renovate[bot] c69e331ded fix(deps): update dependency trpc-to-openapi to ^2.1.4 (#2748)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-03-30 01:04:35 +00:00
homarr-crowdin[bot] 4273cd9fcd chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-03-30 00:52:59 +00:00
Meier Lukas 98515312a2 revert: add restriction callback to restrict visibility and modification of widget kinds (#2746)
This reverts commit 84f73d33a0.
2025-03-29 12:43:10 +01:00
homarr-crowdin[bot] d1b14aca8b chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-03-29 00:47:55 +00:00
Manuel c6b4c9cbf2 docs: add 1.13.1 to issue template (#2738) 2025-03-28 22:04:23 +00:00
440 changed files with 32064 additions and 8857 deletions

No files matched your search

+1
View File
@@ -1,6 +1,7 @@
Dockerfile Dockerfile
.dockerignore .dockerignore
node_modules node_modules
**/node_modules
npm-debug.log npm-debug.log
README.md README.md
.next .next
+12
View File
@@ -31,6 +31,18 @@ body:
label: Version label: Version
description: What version of Homarr are you running? description: What version of Homarr are you running?
options: options:
- 1.23.0
- 1.22.0
- 1.21.0
- 1.20.0
- 1.19.1
- 1.19.0
- 1.18.0
- 1.17.0
- 1.16.0
- 1.15.0
- 1.14.0
- 1.13.1
- 1.13.0 - 1.13.0
- 1.12.0 - 1.12.0
- 1.11.0 - 1.11.0
+4 -4
View File
@@ -6,8 +6,8 @@
**Thank you for your contribution. Please ensure that your pull request meets the following pull request:** **Thank you for your contribution. Please ensure that your pull request meets the following pull request:**
- [ ] Builds without warnings or errors (``pnpm build``, autofix with ``pnpm format:fix``) - [ ] Builds without warnings or errors (`pnpm build`, autofix with `pnpm format:fix`)
- [ ] Pull request targets ``dev`` branch - [ ] Pull request targets `dev` branch
- [ ] Commits follow the [conventional commits guideline](https://www.conventionalcommits.org/en/v1.0.0/) - [ ] Commits follow the [conventional commits guideline](https://www.conventionalcommits.org/en/v1.0.0/)
- [ ] No shorthand variable names are used (eg. ``x``, ``y``, ``i`` or any abbrevation) - [ ] No shorthand variable names are used (eg. `x`, `y`, `i` or any abbrevation)
- [ ] Documentation is up to date. Create a pull request [here](https://github.com/homarr-labs/documentation/).
+1 -1
View File
@@ -6,7 +6,7 @@ on:
jobs: jobs:
approve-automatic-prs: approve-automatic-prs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.actor_id == 158783068 || github.actor_id == 190541745 # Id of renovate bot and crowdin bot see https://api.github.com/users/homarr-renovate%5Bbot%5D and https://api.github.com/users/homarr-crowdin%5Bbot%5D if: github.actor_id == 158783068 || github.actor_id == 190541745 || github.actor_id == 210161987 # Id of renovate bot and crowdin bot see https://api.github.com/users/homarr-renovate%5Bbot%5D and https://api.github.com/users/homarr-crowdin%5Bbot%5D and https://api.github.com/users/homarr-update-contributors%5Bbot%5D
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
+70
View File
@@ -0,0 +1,70 @@
name: Update Contributors
on:
schedule:
- cron: "0 12 * * FRI" # At 12:00 on Friday.
workflow_dispatch:
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
CROWDIN_TOKEN: "${{ secrets.CROWDIN_UPDATE_CONTRIBUTORS_TOKEN }}"
permissions:
contents: write
jobs:
update-contributors:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22]
steps:
- name: Obtain token
id: obtainToken
uses: tibdex/github-app-token@v2
with:
private_key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
app_id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
- name: Checkout repository
uses: actions/checkout@v4
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Run update script
run: node ./scripts/update-contributors.mjs
- name: Commit changes
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
run: |
git config --global user.email "210161987+homarr-update-contributors[bot]@users.noreply.github.com"
git config --global user.name "Homarr Update Contributors"
git add .
git commit -m "chore: update contributors"
- name: Create Pull Request
id: create-pull-request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ steps.obtainToken.outputs.token }}
branch: update-contributors
base: dev
title: "chore: update contributors"
delete-branch: true
body: |
This PR updates the contributors list in the static-data directory.
- name: Install GitHub CLI
run: sudo apt-get install -y gh
- name: Enable auto-merge
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
run: |
gh pr merge ${{steps.create-pull-request.outputs.pull-request-number}} --auto --squash
+1 -1
View File
@@ -1 +1 @@
22.14.0 22.16.0
+1
View File
@@ -26,6 +26,7 @@
"Sabnzbd", "Sabnzbd",
"SeDemal", "SeDemal",
"Sonarr", "Sonarr",
"sslverify",
"superjson", "superjson",
"tabler", "tabler",
"trpc", "trpc",
+378
View File
@@ -1,3 +1,381 @@
## [1.23.0](https://github.com/homarr-labs/homarr/compare/v1.22.0...v1.23.0) (2025-06-06)
### Features
* **releases-widget:** add `import from docker` functionality ([#3130](https://github.com/homarr-labs/homarr/issues/3130)) ([7071d76](https://github.com/homarr-labs/homarr/commit/7071d76c526f1b59a955abcc595894975d574218))
### Bug Fixes
* Allow nullable on cpuModelName and default to "Unknown CPU" if null in OMV Integration ([#3310](https://github.com/homarr-labs/homarr/issues/3310)) ([3e7bf62](https://github.com/homarr-labs/homarr/commit/3e7bf62500f4ce0a8662104e2a656ea98cf2558f))
* csp prevents external stylesheets ([#3337](https://github.com/homarr-labs/homarr/issues/3337)) ([c7a21b1](https://github.com/homarr-labs/homarr/commit/c7a21b1a5f4e7c9d994d81b75a3fd4fa7c013daf))
* **deps:** update dependency @tabler/icons-react to ^3.34.0 ([#3303](https://github.com/homarr-labs/homarr/issues/3303)) ([5b354ac](https://github.com/homarr-labs/homarr/commit/5b354aca2047e8316b331e327cf6031690dcfef9))
* **deps:** update dependency dockerode to ^4.0.7 ([#3331](https://github.com/homarr-labs/homarr/issues/3331)) ([07f81ce](https://github.com/homarr-labs/homarr/commit/07f81cee92627e3b6ee4b2bbf084657f9a83ac34))
* **deps:** update dependency drizzle-orm to ^0.44.1 ([#3280](https://github.com/homarr-labs/homarr/issues/3280)) ([9895c1b](https://github.com/homarr-labs/homarr/commit/9895c1b3ff028a7ea7273ab2625620c9132f5213))
* **deps:** update dependency drizzle-orm to ^0.44.2 ([#3315](https://github.com/homarr-labs/homarr/issues/3315)) ([9ac9042](https://github.com/homarr-labs/homarr/commit/9ac90425647bf1f10d3f299f043c5d40067347bc))
* **deps:** update dependency fast-xml-parser to ^5.2.4 ([#3336](https://github.com/homarr-labs/homarr/issues/3336)) ([3ad72e6](https://github.com/homarr-labs/homarr/commit/3ad72e63b1adecd18c53897a6023f09d6f5d011c))
* **deps:** update dependency node-cron to ^4.1.0 ([#3286](https://github.com/homarr-labs/homarr/issues/3286)) ([1f40868](https://github.com/homarr-labs/homarr/commit/1f408685e69397fa9e3a69bd4331a8240da3713b))
* **deps:** update dependency sass to ^1.89.1 ([#3285](https://github.com/homarr-labs/homarr/issues/3285)) ([b608ffe](https://github.com/homarr-labs/homarr/commit/b608ffe4900a5dd97ceb2669464c477404d090e7))
* **deps:** update dependency swagger-ui-react to ^5.24.0 ([#3325](https://github.com/homarr-labs/homarr/issues/3325)) ([cf7336b](https://github.com/homarr-labs/homarr/commit/cf7336ba87c617578ab52e2ece94fd84df3fc623))
* **deps:** update dependency typescript-eslint to ^8.33.1 ([#3302](https://github.com/homarr-labs/homarr/issues/3302)) ([124e8de](https://github.com/homarr-labs/homarr/commit/124e8de6c584b0efca91d92079f8b3c400bccab2))
* **deps:** update dependency video.js to ^8.23.3 ([#3320](https://github.com/homarr-labs/homarr/issues/3320)) ([81509fd](https://github.com/homarr-labs/homarr/commit/81509fd96dfe0561964eb58d3d21c02400db2e8c))
* **deps:** update dependency zod to ^3.25.43 ([#3290](https://github.com/homarr-labs/homarr/issues/3290)) ([161fc86](https://github.com/homarr-labs/homarr/commit/161fc867703c4cf634eaaeec1b73f5e3cbfa7fec))
* **deps:** update dependency zod to ^3.25.45 ([#3291](https://github.com/homarr-labs/homarr/issues/3291)) ([3337cfa](https://github.com/homarr-labs/homarr/commit/3337cfa3c7e34debcccdbb95ee0bc39f2af98a91))
* **deps:** update dependency zod to ^3.25.46 ([#3292](https://github.com/homarr-labs/homarr/issues/3292)) ([1b1671e](https://github.com/homarr-labs/homarr/commit/1b1671e0e6d808469ba7f8185c4590cf2f2f6f44))
* **deps:** update dependency zod to ^3.25.47 ([#3296](https://github.com/homarr-labs/homarr/issues/3296)) ([94f176d](https://github.com/homarr-labs/homarr/commit/94f176d544272e0378e65255aba58b05d56019b2))
* **deps:** update dependency zod to ^3.25.48 ([#3297](https://github.com/homarr-labs/homarr/issues/3297)) ([ccd8a85](https://github.com/homarr-labs/homarr/commit/ccd8a85e0fcd2cf40dbfb6058787d24a02680b93))
* **deps:** update dependency zod to ^3.25.49 ([#3304](https://github.com/homarr-labs/homarr/issues/3304)) ([0362487](https://github.com/homarr-labs/homarr/commit/0362487156457ae825cac501e7d53db58c31ab0b))
* **deps:** update dependency zod to ^3.25.50 ([#3312](https://github.com/homarr-labs/homarr/issues/3312)) ([a0a0449](https://github.com/homarr-labs/homarr/commit/a0a0449c704ff57725ab5e0b459eb209fc929446))
* **deps:** update dependency zod to ^3.25.51 ([#3316](https://github.com/homarr-labs/homarr/issues/3316)) ([a542ee0](https://github.com/homarr-labs/homarr/commit/a542ee079871c7609ed97c76aa2017305adbfdc9))
* **deps:** update dependency zod to ^3.25.55 ([#3332](https://github.com/homarr-labs/homarr/issues/3332)) ([ed3f848](https://github.com/homarr-labs/homarr/commit/ed3f848e8afb57cc1b9249551f0260eb52bbd4a6))
* **deps:** update tanstack-query monorepo to ^5.79.1 ([#3300](https://github.com/homarr-labs/homarr/issues/3300)) ([a3deed3](https://github.com/homarr-labs/homarr/commit/a3deed3b090aed0df0f45a9bd04c006640691e05))
* **deps:** update tanstack-query monorepo to ^5.79.2 ([#3301](https://github.com/homarr-labs/homarr/issues/3301)) ([a0585e3](https://github.com/homarr-labs/homarr/commit/a0585e3dd7d9cfbccf8ff5535ae2c753a8b92764))
* **deps:** update tanstack-query monorepo to ^5.80.0 ([#3307](https://github.com/homarr-labs/homarr/issues/3307)) ([df2a9a5](https://github.com/homarr-labs/homarr/commit/df2a9a5329e06847904af56a0d058c0da59f6380))
* **deps:** update tanstack-query monorepo to ^5.80.2 ([#3308](https://github.com/homarr-labs/homarr/issues/3308)) ([cae0cad](https://github.com/homarr-labs/homarr/commit/cae0cad92f68f716887e077194d4e30402466d57))
* **deps:** update tanstack-query monorepo to ^5.80.3 ([#3317](https://github.com/homarr-labs/homarr/issues/3317)) ([c051a42](https://github.com/homarr-labs/homarr/commit/c051a42791de4eae4baf20360efe5b0c8925c22b))
* **deps:** update tanstack-query monorepo to ^5.80.5 ([#3321](https://github.com/homarr-labs/homarr/issues/3321)) ([cb77a7d](https://github.com/homarr-labs/homarr/commit/cb77a7dc6d60560250765d129aac9bd236693e2b))
* **deps:** update tanstack-query monorepo to ^5.80.6 ([#3326](https://github.com/homarr-labs/homarr/issues/3326)) ([61553cf](https://github.com/homarr-labs/homarr/commit/61553cfc8e48a1179a148007fb970b1f4e6d509c))
* **deps:** update testcontainers-node monorepo to v11 (major) ([#3250](https://github.com/homarr-labs/homarr/issues/3250)) ([0d1e3a6](https://github.com/homarr-labs/homarr/commit/0d1e3a625a04abd5f667640ab40e01d28e017488))
* **deps:** update tiptap monorepo to v2.13.0 ([#3323](https://github.com/homarr-labs/homarr/issues/3323)) ([dac8234](https://github.com/homarr-labs/homarr/commit/dac82347e86dd7faacb24adbfc61ace9c917b8cd))
* **deps:** update tiptap monorepo to v2.14.0 ([#3334](https://github.com/homarr-labs/homarr/issues/3334)) ([6274a66](https://github.com/homarr-labs/homarr/commit/6274a66048163513e3faaf7011d7d6aa6ad9f161))
* **deps:** update trpc monorepo to ^11.2.0 ([#3293](https://github.com/homarr-labs/homarr/issues/3293)) ([f0ba693](https://github.com/homarr-labs/homarr/commit/f0ba693077a0c7f7897c81f02ad66b77fce52f1c))
* **deps:** update trpc monorepo to ^11.3.0 ([#3322](https://github.com/homarr-labs/homarr/issues/3322)) ([74aa561](https://github.com/homarr-labs/homarr/commit/74aa561f470f0f76421f356998caa91dc8bb65a1))
* **deps:** update trpc monorepo to ^11.3.1 ([#3329](https://github.com/homarr-labs/homarr/issues/3329)) ([8484daa](https://github.com/homarr-labs/homarr/commit/8484daaf7516ad72dd9e7de3117b5e0d50415dbb))
* **nextcloud:** correct calendar event date mapping and support showU… ([#3330](https://github.com/homarr-labs/homarr/issues/3330)) ([ffd2f80](https://github.com/homarr-labs/homarr/commit/ffd2f802bb777f13eac1ee45e1b5f0ca2b596672))
## [1.22.0](https://github.com/homarr-labs/homarr/compare/v1.21.0...v1.22.0) (2025-05-30)
### Features
* allow custom protocols for app href ([#2575](https://github.com/homarr-labs/homarr/issues/2575)) ([09af0a7](https://github.com/homarr-labs/homarr/commit/09af0a76f64c2fec512ba30726827208d5c3ad95))
* **cluster-health:** add visibility options ([#3210](https://github.com/homarr-labs/homarr/issues/3210)) ([939df8f](https://github.com/homarr-labs/homarr/commit/939df8f6d11bb36f71854425e4fdc4ca0724da31))
* **downloads:** add option to limit amount of items ([#3205](https://github.com/homarr-labs/homarr/issues/3205)) ([2dc871e](https://github.com/homarr-labs/homarr/commit/2dc871e531b58aee25db26ab7c5e94626517db36))
### Bug Fixes
* **boards:** app select modal images rendering issue ([#3218](https://github.com/homarr-labs/homarr/issues/3218)) ([85e98d2](https://github.com/homarr-labs/homarr/commit/85e98d2b603abe18b37fa74bf50836422b72c9e1))
* **calendar:** load events for all visible days of calendar month ([#3216](https://github.com/homarr-labs/homarr/issues/3216)) ([437ecd1](https://github.com/homarr-labs/homarr/commit/437ecd10239c70e5a8c4d34119076ab155a2159e))
* **calendar:** remove scrollbar for 2x2 ([#3227](https://github.com/homarr-labs/homarr/issues/3227)) ([1d27da2](https://github.com/homarr-labs/homarr/commit/1d27da22303b2133bb1a7c59102b7c292bc34833))
* **cluster-health:** storage items multiply ([#3206](https://github.com/homarr-labs/homarr/issues/3206)) ([1114934](https://github.com/homarr-labs/homarr/commit/11149348fcbe9002bba33763e0b03aed50d20f71))
* csp prevents fonts to load ([#3225](https://github.com/homarr-labs/homarr/issues/3225)) ([05e159f](https://github.com/homarr-labs/homarr/commit/05e159f5662f04dd298eb13816fe1768d7ef9312))
* **deps:** update dependency @ianvs/prettier-plugin-sort-imports to ^4.4.2 ([#3258](https://github.com/homarr-labs/homarr/issues/3258)) ([4a184d2](https://github.com/homarr-labs/homarr/commit/4a184d23f25a8a06b36cd8c6d9b2a65118399d9f))
* **deps:** update dependency @kubernetes/client-node to ^1.3.0 ([#3261](https://github.com/homarr-labs/homarr/issues/3261)) ([635c9a2](https://github.com/homarr-labs/homarr/commit/635c9a2687cf2b9a09a3093eb638ef772eddfb03))
* **deps:** update dependency @t3-oss/env-nextjs to ^0.13.6 ([#3244](https://github.com/homarr-labs/homarr/issues/3244)) ([8966e2a](https://github.com/homarr-labs/homarr/commit/8966e2ad8a2025dfc91934ccf825bd76c08dc333))
* **deps:** update dependency drizzle-orm to ^0.44.0 ([#3256](https://github.com/homarr-labs/homarr/issues/3256)) ([09b49a2](https://github.com/homarr-labs/homarr/commit/09b49a242dd5d3d34bab758f190ab948b6fc7566))
* **deps:** update dependency flag-icons to ^7.4.0 ([#3265](https://github.com/homarr-labs/homarr/issues/3265)) ([a0eda74](https://github.com/homarr-labs/homarr/commit/a0eda74fa472446c5b93c1b65829e65670620ab6))
* **deps:** update dependency flag-icons to ^7.5.0 ([#3268](https://github.com/homarr-labs/homarr/issues/3268)) ([2320d3c](https://github.com/homarr-labs/homarr/commit/2320d3c5674413e18e9bb42fd004ada016c5e605))
* **deps:** update dependency jotai to ^2.12.5 ([#3243](https://github.com/homarr-labs/homarr/issues/3243)) ([9c32385](https://github.com/homarr-labs/homarr/commit/9c323850bc49df5f6fc3437678faab0c7eab2e53))
* **deps:** update dependency ldapts to v8.0.1 ([#3264](https://github.com/homarr-labs/homarr/issues/3264)) ([bac4f64](https://github.com/homarr-labs/homarr/commit/bac4f642574caf5f170d20353c036396698e5963))
* **deps:** update dependency node-cron to v4 ([#3075](https://github.com/homarr-labs/homarr/issues/3075)) ([109f9e7](https://github.com/homarr-labs/homarr/commit/109f9e79793b36b46ab1987a56f7c206e21d32e4))
* **deps:** update dependency octokit to ^5.0.3 ([#3249](https://github.com/homarr-labs/homarr/issues/3249)) ([3253b2f](https://github.com/homarr-labs/homarr/commit/3253b2f138a9e911b50c952e37fb4afb1f71446d))
* **deps:** update dependency trpc-to-openapi to ^2.3.1 ([#3046](https://github.com/homarr-labs/homarr/issues/3046)) ([640bf7b](https://github.com/homarr-labs/homarr/commit/640bf7b939b201d9905f5119af0dc0da6120b98c))
* **deps:** update dependency typescript-eslint to ^8.33.0 ([#3247](https://github.com/homarr-labs/homarr/issues/3247)) ([8b553b8](https://github.com/homarr-labs/homarr/commit/8b553b80e61ab608d01d890d26a8ea5bae3f38b8))
* **deps:** update dependency zod to ^3.25.24 ([#3195](https://github.com/homarr-labs/homarr/issues/3195)) ([0b224e8](https://github.com/homarr-labs/homarr/commit/0b224e8835d4fc5a292a624c8af8d874216b703a))
* **deps:** update dependency zod to ^3.25.28 ([#3201](https://github.com/homarr-labs/homarr/issues/3201)) ([e0a9e4f](https://github.com/homarr-labs/homarr/commit/e0a9e4f97c9dddb95fbc0b2a65acfd802dac9c19))
* **deps:** update dependency zod to ^3.25.29 ([#3237](https://github.com/homarr-labs/homarr/issues/3237)) ([81ca6ef](https://github.com/homarr-labs/homarr/commit/81ca6ef7b2767fb0bc285ffa53998e2c586225ec))
* **deps:** update dependency zod to ^3.25.30 ([#3239](https://github.com/homarr-labs/homarr/issues/3239)) ([3062948](https://github.com/homarr-labs/homarr/commit/30629489396668125dec7d93fcb0e733099c35fe))
* **deps:** update dependency zod to ^3.25.31 ([#3254](https://github.com/homarr-labs/homarr/issues/3254)) ([e2e4656](https://github.com/homarr-labs/homarr/commit/e2e46560010a94603852ea90a0fc63fabd473198))
* **deps:** update dependency zod to ^3.25.32 ([#3255](https://github.com/homarr-labs/homarr/issues/3255)) ([9c3a46c](https://github.com/homarr-labs/homarr/commit/9c3a46c071e169459ec7b8c9ba1647b4b354cbea))
* **deps:** update dependency zod to ^3.25.34 ([#3259](https://github.com/homarr-labs/homarr/issues/3259)) ([c1324ea](https://github.com/homarr-labs/homarr/commit/c1324eab76795dabc0a41315191f9ab6a73ba501))
* **deps:** update dependency zod to ^3.25.36 ([#3262](https://github.com/homarr-labs/homarr/issues/3262)) ([48ce2af](https://github.com/homarr-labs/homarr/commit/48ce2af4c182985a7721b65535bcf1b41f1cdcba))
* **deps:** update dependency zod to ^3.25.39 ([#3270](https://github.com/homarr-labs/homarr/issues/3270)) ([862d07d](https://github.com/homarr-labs/homarr/commit/862d07db68e7624342cb81241ef9924e6407fda8))
* **deps:** update dependency zod to ^3.25.41 ([#3272](https://github.com/homarr-labs/homarr/issues/3272)) ([5bf20aa](https://github.com/homarr-labs/homarr/commit/5bf20aa3538503de1d4f4d04f9de3e3b92442d75))
* **deps:** update dependency zod to ^3.25.42 ([#3274](https://github.com/homarr-labs/homarr/issues/3274)) ([608280e](https://github.com/homarr-labs/homarr/commit/608280e670432098ce466cfb8fa2b315bf35ead2))
* **deps:** update mantine monorepo to ^8.0.2 ([#3245](https://github.com/homarr-labs/homarr/issues/3245)) ([816544a](https://github.com/homarr-labs/homarr/commit/816544a996656ccf7cbacf76aff83d013ba21ad1))
* **deps:** update nextjs monorepo to v15.3.3 ([#3269](https://github.com/homarr-labs/homarr/issues/3269)) ([d72a804](https://github.com/homarr-labs/homarr/commit/d72a804ad9feb24a2b067712128a6566930990a6))
* **deps:** update tanstack-query monorepo to ^5.77.0 ([#3204](https://github.com/homarr-labs/homarr/issues/3204)) ([ec42f26](https://github.com/homarr-labs/homarr/commit/ec42f265a04f0f96105d6b1318becd5cc94aa8fb))
* **deps:** update tanstack-query monorepo to ^5.77.1 ([#3229](https://github.com/homarr-labs/homarr/issues/3229)) ([5ea7a20](https://github.com/homarr-labs/homarr/commit/5ea7a206772924faf1f842c80033954c853b3a02))
* **deps:** update tanstack-query monorepo to ^5.77.2 ([#3232](https://github.com/homarr-labs/homarr/issues/3232)) ([7118690](https://github.com/homarr-labs/homarr/commit/71186908b96f717c6f8c528b0f983e1792a7a885))
* **deps:** update tanstack-query monorepo to ^5.79.0 ([#3263](https://github.com/homarr-labs/homarr/issues/3263)) ([1f9983f](https://github.com/homarr-labs/homarr/commit/1f9983f16db8677b8c244b63306ea4afd2195d94))
* **deps:** update trpc monorepo to ^11.1.3 ([#3240](https://github.com/homarr-labs/homarr/issues/3240)) ([78f98ed](https://github.com/homarr-labs/homarr/commit/78f98edc4145e27e2daa269121173905c642c139))
* **deps:** update trpc monorepo to ^11.1.4 ([#3260](https://github.com/homarr-labs/homarr/issues/3260)) ([bfd7b70](https://github.com/homarr-labs/homarr/commit/bfd7b706182340eaa1db70a3c6cdcc935aee8df4))
* **deps:** update turbo monorepo to ^2.5.4 ([#3279](https://github.com/homarr-labs/homarr/issues/3279)) ([c8202ab](https://github.com/homarr-labs/homarr/commit/c8202ab3c676bf796c6e7aff2ebfef224a0124c8))
* **downloads:** upspeed font size wrong ([#3214](https://github.com/homarr-labs/homarr/issues/3214)) ([c67916f](https://github.com/homarr-labs/homarr/commit/c67916f72eded64133fd458184a599f32d8fc63c))
* **media-request-stats:** users squeezed together ([#3213](https://github.com/homarr-labs/homarr/issues/3213)) ([25bd387](https://github.com/homarr-labs/homarr/commit/25bd3879db9260bbd24a1338cc5f26652ed78d72))
* navigating back results in navigation back to first page opened ([#3224](https://github.com/homarr-labs/homarr/issues/3224)) ([cbbf0c8](https://github.com/homarr-labs/homarr/commit/cbbf0c8dcf685e31e70bb62f04b1065dab90e18d))
* reduce logging ([#3200](https://github.com/homarr-labs/homarr/issues/3200)) ([be8b332](https://github.com/homarr-labs/homarr/commit/be8b332f942beb3af181ee6f39558882ed640a0e))
* **search:** removing server search engine causes loading error ([#3252](https://github.com/homarr-labs/homarr/issues/3252)) ([10f720d](https://github.com/homarr-labs/homarr/commit/10f720d74c6cc70cbf217e8daf8cc7878ac6d712))
* **visual:** check for creator board permission is wrong ([#3212](https://github.com/homarr-labs/homarr/issues/3212)) ([f7e5e82](https://github.com/homarr-labs/homarr/commit/f7e5e823d5dee36ea11f243a0da7d84a1f363419))
### Performance Improvements
* cache middleware onboarding and language checks ([#3197](https://github.com/homarr-labs/homarr/issues/3197)) ([2afa9b6](https://github.com/homarr-labs/homarr/commit/2afa9b67274edd79b52598dc0d22437048a67e2c))
## [1.21.0](https://github.com/homarr-labs/homarr/compare/v1.20.0...v1.21.0) (2025-05-23)
### Features
* docker widget ([#2288](https://github.com/homarr-labs/homarr/issues/2288)) ([e1eda53](https://github.com/homarr-labs/homarr/commit/e1eda534da6d97655546f3140f9e36eca8817db3))
### Bug Fixes
* **deps:** update dependency @tabler/icons-react to ^3.33.0 ([#3132](https://github.com/homarr-labs/homarr/issues/3132)) ([ee8870b](https://github.com/homarr-labs/homarr/commit/ee8870b66bbcb8eeeef359ece9391a496198be0d))
* **deps:** update dependency octokit to ^4.1.4 ([#3165](https://github.com/homarr-labs/homarr/issues/3165)) ([50445b5](https://github.com/homarr-labs/homarr/commit/50445b560edc3af2631f8d8d9aa81f3387d848f7))
* **deps:** update dependency octokit to v5 ([#3156](https://github.com/homarr-labs/homarr/issues/3156)) ([8c59abd](https://github.com/homarr-labs/homarr/commit/8c59abdde3d8034f22a2b926b3291b55a0d492ea))
* **deps:** update dependency swagger-ui-react to ^5.22.0 ([#3178](https://github.com/homarr-labs/homarr/issues/3178)) ([1e7b2e6](https://github.com/homarr-labs/homarr/commit/1e7b2e617b1437f0f98bc791e763bc49b6589b9e))
* **deps:** update dependency undici to v7.10.0 ([#3158](https://github.com/homarr-labs/homarr/issues/3158)) ([eb5d5f8](https://github.com/homarr-labs/homarr/commit/eb5d5f8260cc9795806ecdb56fbb3f8d19858ed7))
* **deps:** update dependency zod to ^3.25.12 ([#3172](https://github.com/homarr-labs/homarr/issues/3172)) ([caaee50](https://github.com/homarr-labs/homarr/commit/caaee503c128ff2a0b2a7de7a7c353484e4741a5))
* **deps:** update dependency zod to ^3.25.13 ([#3173](https://github.com/homarr-labs/homarr/issues/3173)) ([dae6ded](https://github.com/homarr-labs/homarr/commit/dae6dedde544bf28fcdf87c71a9d585078527b82))
* **deps:** update dependency zod to ^3.25.14 ([#3174](https://github.com/homarr-labs/homarr/issues/3174)) ([23404a2](https://github.com/homarr-labs/homarr/commit/23404a27f1a9dfec4e9126750f8d97e7c5eda572))
* **deps:** update dependency zod to ^3.25.16 ([#3175](https://github.com/homarr-labs/homarr/issues/3175)) ([5080f51](https://github.com/homarr-labs/homarr/commit/5080f51a8dbe1b82571f356f7ae29ed7be69e18e))
* **deps:** update dependency zod to ^3.25.17 ([#3177](https://github.com/homarr-labs/homarr/issues/3177)) ([371803a](https://github.com/homarr-labs/homarr/commit/371803ac509efd246367c812a6c0143e7aeda03f))
* **deps:** update dependency zod to ^3.25.20 ([#3183](https://github.com/homarr-labs/homarr/issues/3183)) ([49498c8](https://github.com/homarr-labs/homarr/commit/49498c870e494b417e6c63d8547a4c0ffd93768b))
* **deps:** update dependency zod to ^3.25.21 ([#3187](https://github.com/homarr-labs/homarr/issues/3187)) ([37d637d](https://github.com/homarr-labs/homarr/commit/37d637dcae6cbf62efda25a8dc01eedba66f2c74))
* **deps:** update dependency zod to ^3.25.23 ([#3188](https://github.com/homarr-labs/homarr/issues/3188)) ([9840644](https://github.com/homarr-labs/homarr/commit/9840644ca50d3157223e7da0cf895f2e750afc58))
* **deps:** update dependency zod to ^3.25.3 ([#3148](https://github.com/homarr-labs/homarr/issues/3148)) ([6e2aa87](https://github.com/homarr-labs/homarr/commit/6e2aa87102b72319236483b9a1d3e5ddd6fe2c82))
* **deps:** update dependency zod to ^3.25.4 ([#3150](https://github.com/homarr-labs/homarr/issues/3150)) ([49b7080](https://github.com/homarr-labs/homarr/commit/49b7080415aaeee576507d48e8cda18acc9943a8))
* **deps:** update dependency zod to ^3.25.5 ([#3152](https://github.com/homarr-labs/homarr/issues/3152)) ([79755c9](https://github.com/homarr-labs/homarr/commit/79755c95d3cf58680e434f0113c130db9f649d96))
* **deps:** update dependency zod to ^3.25.6 ([#3153](https://github.com/homarr-labs/homarr/issues/3153)) ([92ebc05](https://github.com/homarr-labs/homarr/commit/92ebc05fbfded6d20d242ad477cb0dc30243e8cc))
* **deps:** update dependency zod to ^3.25.7 ([#3154](https://github.com/homarr-labs/homarr/issues/3154)) ([22954b8](https://github.com/homarr-labs/homarr/commit/22954b8d2637cdea9a6502cab2d3d40359fa098f))
* **deps:** update dependency zod to ^3.25.8 ([#3170](https://github.com/homarr-labs/homarr/issues/3170)) ([72bcbe8](https://github.com/homarr-labs/homarr/commit/72bcbe8effe47fdfb6a94de8defc0d3c12e7c754))
* **deps:** update dependency zod-validation-error to ^3.4.1 ([#3127](https://github.com/homarr-labs/homarr/issues/3127)) ([886e95d](https://github.com/homarr-labs/homarr/commit/886e95d51714ce20e03d32b4c2cfb673e7e4c218))
* **deps:** update tanstack-query monorepo to ^5.76.2 ([#3186](https://github.com/homarr-labs/homarr/issues/3186)) ([1584114](https://github.com/homarr-labs/homarr/commit/1584114e72246a018b9f49a859e49f7fb985009d))
* **deps:** update testcontainers-node monorepo to ^10.27.0 ([#3159](https://github.com/homarr-labs/homarr/issues/3159)) ([18c465e](https://github.com/homarr-labs/homarr/commit/18c465e81e92cf58b7e32bcb70f1ab404a03a135))
* **deps:** update testcontainers-node monorepo to ^10.28.0 ([#3191](https://github.com/homarr-labs/homarr/issues/3191)) ([43b8241](https://github.com/homarr-labs/homarr/commit/43b82419e9396f0b80bafa0a707d575c3732dc5b))
* **downloads:** reordering columns in widget does not work ([#3194](https://github.com/homarr-labs/homarr/issues/3194)) ([09f4e67](https://github.com/homarr-labs/homarr/commit/09f4e6785b2ce38da3b52003e9c9300cdd0feb85))
* esbuild not part of dependencies ([#3139](https://github.com/homarr-labs/homarr/issues/3139)) ([ef540e9](https://github.com/homarr-labs/homarr/commit/ef540e980f90f730020ef081dd105e33b0671232))
* **items:** resize handle overlaps header ([#3193](https://github.com/homarr-labs/homarr/issues/3193)) ([09f3989](https://github.com/homarr-labs/homarr/commit/09f398948348fb402600506571a967ba405fbafb))
* **releases-widget:** add hardcoded css classNames([#3181](https://github.com/homarr-labs/homarr/issues/3181)) ([4cb004f](https://github.com/homarr-labs/homarr/commit/4cb004fce0750da339f603b944de55135faa4bfa))
## [1.20.0](https://github.com/homarr-labs/homarr/compare/v1.19.1...v1.20.0) (2025-05-16)
### Features
* add css class to logo for custom css ([#3121](https://github.com/homarr-labs/homarr/issues/3121)) ([9b8320c](https://github.com/homarr-labs/homarr/commit/9b8320c10d51cc449b3fb5c1532408bff5655788))
* **auth:** add account linking for oidc providers ([#3106](https://github.com/homarr-labs/homarr/issues/3106)) ([3daf1c8](https://github.com/homarr-labs/homarr/commit/3daf1c8341ab40a580777246e742ff5019a483d2))
* **integration:** improve integration test connection ([#3005](https://github.com/homarr-labs/homarr/issues/3005)) ([ef9a5e9](https://github.com/homarr-labs/homarr/commit/ef9a5e9895d7079008046df66f6a6d538af106c9))
* **releases-widget:** added auto icon search when editing a repository ([#3087](https://github.com/homarr-labs/homarr/issues/3087)) ([9413430](https://github.com/homarr-labs/homarr/commit/94134303d7dc6f2d52e4955dcd9d1afbd8d203fb))
* **releases-widget:** limit number of displayed releases and custom name for repositories ([#2974](https://github.com/homarr-labs/homarr/issues/2974)) ([8c1b365](https://github.com/homarr-labs/homarr/commit/8c1b365733c2bccd0838cbf07aa59115bcc0c0cd))
### Bug Fixes
* **calendar:** some timezones result in offset of one day ([#3120](https://github.com/homarr-labs/homarr/issues/3120)) ([b86159b](https://github.com/homarr-labs/homarr/commit/b86159ba7c1cdaea973466bd7430fc45fd0d8317))
* **deps:** missing dependencies in definitions package for docs link codegen ([#3119](https://github.com/homarr-labs/homarr/issues/3119)) ([42e7fe3](https://github.com/homarr-labs/homarr/commit/42e7fe3273b0f0e8f67e38ac3fd758528cc47913))
* **deps:** update dependency @auth/core to ^0.39.1 ([#3072](https://github.com/homarr-labs/homarr/issues/3072)) ([0d23119](https://github.com/homarr-labs/homarr/commit/0d2311975c9062f538e7cf7166cc354bc6b21468))
* **deps:** update dependency @auth/drizzle-adapter to ^1.9.1 ([#3073](https://github.com/homarr-labs/homarr/issues/3073)) ([3f9f94e](https://github.com/homarr-labs/homarr/commit/3f9f94ea45e001d0f2c966ee21d1e148103a854e))
* **deps:** update dependency @extractus/feed-extractor to v7.1.6 ([#3105](https://github.com/homarr-labs/homarr/issues/3105)) ([d96bc89](https://github.com/homarr-labs/homarr/commit/d96bc898fdc014940207c1d7bead371ac4df33a9))
* **deps:** update dependency bcrypt to v6 ([#3083](https://github.com/homarr-labs/homarr/issues/3083)) ([5686910](https://github.com/homarr-labs/homarr/commit/5686910a4735801c40caed08a4c8b931fd04418f))
* **deps:** update dependency next-auth to v5.0.0-beta.28 ([#3074](https://github.com/homarr-labs/homarr/issues/3074)) ([98773e7](https://github.com/homarr-labs/homarr/commit/98773e7dba3f7f816a50c134cedf81cddb55e5f1))
* **deps:** update dependency sass to ^1.88.0 ([#3078](https://github.com/homarr-labs/homarr/issues/3078)) ([b0f7310](https://github.com/homarr-labs/homarr/commit/b0f7310cb79e014e58a72dcf812d3d4cc876d034))
* **deps:** update dependency sass to ^1.89.0 ([#3118](https://github.com/homarr-labs/homarr/issues/3118)) ([5b16468](https://github.com/homarr-labs/homarr/commit/5b16468cab53429d4f8cf36771ab6efafea86743))
* **deps:** update dependency typescript-eslint to ^8.32.1 ([#3093](https://github.com/homarr-labs/homarr/issues/3093)) ([42b377d](https://github.com/homarr-labs/homarr/commit/42b377d0672973cb3d7b32c0d1e2c0663d68c2a8))
* **deps:** update dependency undici to v7.9.0 ([#3077](https://github.com/homarr-labs/homarr/issues/3077)) ([01d53ee](https://github.com/homarr-labs/homarr/commit/01d53eee60f7269158565cfa46f7aebdfa8d41cf))
* **deps:** update mantine monorepo to ^8.0.1 ([#3107](https://github.com/homarr-labs/homarr/issues/3107)) ([25efe3d](https://github.com/homarr-labs/homarr/commit/25efe3d33504f8e4f1eb3f29edfdeccf4b15a8e4))
* **deps:** update tanstack-query monorepo to ^5.76.0 ([#3090](https://github.com/homarr-labs/homarr/issues/3090)) ([388bc73](https://github.com/homarr-labs/homarr/commit/388bc7392c318e05f85134cf969e10d9627c55f9))
* **deps:** update tanstack-query monorepo to ^5.76.1 ([#3101](https://github.com/homarr-labs/homarr/issues/3101)) ([c1e5d77](https://github.com/homarr-labs/homarr/commit/c1e5d77176126c6248f052d2baa3cf09837fba62))
* **deps:** update testcontainers-node monorepo to ^10.26.0 ([#3114](https://github.com/homarr-labs/homarr/issues/3114)) ([cb81efa](https://github.com/homarr-labs/homarr/commit/cb81efa42c99f4bd8024b21e0818294db8d22fec))
* **releases-widget:** delete the empty repository when canceling `Add Reposotory` ([#3082](https://github.com/homarr-labs/homarr/issues/3082)) ([72862ef](https://github.com/homarr-labs/homarr/commit/72862efae274cfc25fe656aef27f6acb2e1b0ca6))
* **releases-widget:** reduced padding for `ul` in release description ([#3085](https://github.com/homarr-labs/homarr/issues/3085)) ([593d97e](https://github.com/homarr-labs/homarr/commit/593d97e27aa85ab023935d12c8cbf534d38c288f))
* **releases-widget:** removed version regex in options overview ([#3081](https://github.com/homarr-labs/homarr/issues/3081)) ([59cae02](https://github.com/homarr-labs/homarr/commit/59cae02623b09e60b8c300c18349ea282c872822))
* **releases-widget:** validation preventing removal of a previously added icon ([#3086](https://github.com/homarr-labs/homarr/issues/3086)) ([18fe8ec](https://github.com/homarr-labs/homarr/commit/18fe8ecd39f6551135beb0333cbad5b9ff4d6664))
* updated contributors contain unnecessary data ([#3124](https://github.com/homarr-labs/homarr/issues/3124)) ([6e03e5a](https://github.com/homarr-labs/homarr/commit/6e03e5a40c9aa0ee5e0e3de4277ffbe44ad45b66))
## [1.19.1](https://github.com/homarr-labs/homarr/compare/v1.19.0...v1.19.1) (2025-05-09)
### Bug Fixes
* **deps:** update dependency @extractus/feed-extractor to v7.1.5 ([#3020](https://github.com/homarr-labs/homarr/issues/3020)) ([758b1a0](https://github.com/homarr-labs/homarr/commit/758b1a049019d903f152f783eab3b6fc4be4de14))
* **deps:** update dependency @kubernetes/client-node to ^1.2.0 ([#3042](https://github.com/homarr-labs/homarr/issues/3042)) ([273d4ed](https://github.com/homarr-labs/homarr/commit/273d4edb233f5d5faa7105c23208d95268119786))
* **deps:** update dependency better-sqlite3 to ^11.10.0 ([#3053](https://github.com/homarr-labs/homarr/issues/3053)) ([0d5f430](https://github.com/homarr-labs/homarr/commit/0d5f430bd45e6d29d604d4f93ff57cfc7a64e309))
* **deps:** update dependency drizzle-kit to ^0.31.1 ([#3013](https://github.com/homarr-labs/homarr/issues/3013)) ([76ce6ff](https://github.com/homarr-labs/homarr/commit/76ce6ff1fab34223e7ff5abf46193f1ae9b011d1))
* **deps:** update dependency eslint-config-prettier to ^10.1.3 ([#3047](https://github.com/homarr-labs/homarr/issues/3047)) ([e94c05f](https://github.com/homarr-labs/homarr/commit/e94c05f3d1eea1841b978359d3c41d8a46a08a39))
* **deps:** update dependency eslint-config-prettier to ^10.1.5 ([#3061](https://github.com/homarr-labs/homarr/issues/3061)) ([395e03c](https://github.com/homarr-labs/homarr/commit/395e03c8101af873f35388ae1b833dc68d552792))
* **deps:** update dependency jotai to ^2.12.4 ([#3038](https://github.com/homarr-labs/homarr/issues/3038)) ([7ec1793](https://github.com/homarr-labs/homarr/commit/7ec179304040d6782e516358aeaae2eaa143dc1a))
* **deps:** update dependency ldapts to v8 ([#3028](https://github.com/homarr-labs/homarr/issues/3028)) ([7c0baf5](https://github.com/homarr-labs/homarr/commit/7c0baf5fba19735c2b3fac0728359a9aa8c617e6))
* **deps:** update dependency react-error-boundary to v6 ([#3016](https://github.com/homarr-labs/homarr/issues/3016)) ([680f7b0](https://github.com/homarr-labs/homarr/commit/680f7b0c7277ffcbb6a033fd7a2f14146e8a2fca))
* **deps:** update dependency typescript-eslint to ^8.32.0 ([#3027](https://github.com/homarr-labs/homarr/issues/3027)) ([bee1ddd](https://github.com/homarr-labs/homarr/commit/bee1dddc9ff1b213ef9fd9cc1696bf22cc816b19))
* **deps:** update dependency ws to ^8.18.2 ([#3012](https://github.com/homarr-labs/homarr/issues/3012)) ([9bb26eb](https://github.com/homarr-labs/homarr/commit/9bb26ebcc3a3f400317c85bd1fb13621d52049c2))
* **deps:** update dependency zod to ^3.24.4 ([#3021](https://github.com/homarr-labs/homarr/issues/3021)) ([e5e686d](https://github.com/homarr-labs/homarr/commit/e5e686d59086320a565c3367a389d1117fd74d88))
* **deps:** update mantine monorepo to v8 (major) ([#3022](https://github.com/homarr-labs/homarr/issues/3022)) ([44853ea](https://github.com/homarr-labs/homarr/commit/44853ea64bed3a093a26cb07904d8beedf220e4e))
* **deps:** update nextjs monorepo to v15.3.2 ([#3045](https://github.com/homarr-labs/homarr/issues/3045)) ([b6eb8ca](https://github.com/homarr-labs/homarr/commit/b6eb8ca0f6d0e6b2fac40423a6c0157dff292622))
* **deps:** update tanstack-query monorepo to ^5.75.2 ([#3018](https://github.com/homarr-labs/homarr/issues/3018)) ([d512fc0](https://github.com/homarr-labs/homarr/commit/d512fc042a0cb06238f3c86f92b12ad152208a9c))
* **deps:** update tanstack-query monorepo to ^5.75.4 ([#3029](https://github.com/homarr-labs/homarr/issues/3029)) ([861728a](https://github.com/homarr-labs/homarr/commit/861728a7c3156bbe618b9fb186d9f168ba9b339b))
* **deps:** update tanstack-query monorepo to ^5.75.5 ([#3048](https://github.com/homarr-labs/homarr/issues/3048)) ([8cb9ddb](https://github.com/homarr-labs/homarr/commit/8cb9ddbce3f00e6e3df07c796c39fda7dd18b7b1))
* **deps:** update tanstack-query monorepo to ^5.75.6 ([#3062](https://github.com/homarr-labs/homarr/issues/3062)) ([d6180eb](https://github.com/homarr-labs/homarr/commit/d6180ebc7387249265ad9c296c52aab6d393d88a))
* **deps:** update tanstack-query monorepo to ^5.75.7 ([#3063](https://github.com/homarr-labs/homarr/issues/3063)) ([b71c5c6](https://github.com/homarr-labs/homarr/commit/b71c5c6fc73cab4ebc1bfe9d70722f166ebcdb47))
* **deps:** update tiptap monorepo to v2.12.0 ([#3025](https://github.com/homarr-labs/homarr/issues/3025)) ([8a0557f](https://github.com/homarr-labs/homarr/commit/8a0557f927f6423e37a372c4c4ffea224598ff45))
* **deps:** update turbo monorepo to ^2.5.3 ([#3043](https://github.com/homarr-labs/homarr/issues/3043)) ([9b6110b](https://github.com/homarr-labs/homarr/commit/9b6110b9c9ec08bde335cc95d7548e2dddfab45b))
* **logs:** log error cause when object, sign in oidc debug messages ([#3054](https://github.com/homarr-labs/homarr/issues/3054)) ([2bdb1c7](https://github.com/homarr-labs/homarr/commit/2bdb1c75f680e7715bbe68c6f50e24ea861302ca))
* **releases-widget:** error display, decouple database repository object from responses and batch widget queries ([#2891](https://github.com/homarr-labs/homarr/issues/2891)) ([347c6d1](https://github.com/homarr-labs/homarr/commit/347c6d15191a64899ff844cfe689973f44b9c556))
* **releases-widget:** improved provider parsing of missing properties ([#2964](https://github.com/homarr-labs/homarr/issues/2964)) ([b62d47d](https://github.com/homarr-labs/homarr/commit/b62d47d303ca1271b932b6347a489df6f16fc680))
## [1.19.0](https://github.com/homarr-labs/homarr/compare/v1.18.1...v1.19.0) (2025-05-02)
### Features
* **widget:** add prefetch for apps and bookmarks ([#2895](https://github.com/homarr-labs/homarr/issues/2895)) ([82c5361](https://github.com/homarr-labs/homarr/commit/82c53611122dd07e5639a902afa0a6a4fd8ed4e5))
### Bug Fixes
* **cookie:** parsing of header not working with percentage char ([#2989](https://github.com/homarr-labs/homarr/issues/2989)) ([547a8c3](https://github.com/homarr-labs/homarr/commit/547a8c34c58c88e6f0074df327f49aadf9ab07f4))
* **deps:** update dependency @t3-oss/env-nextjs to ^0.13.4 ([#2993](https://github.com/homarr-labs/homarr/issues/2993)) ([1a07fa9](https://github.com/homarr-labs/homarr/commit/1a07fa9af59e93f9ce5d70b72855bb070f5b5787))
* **deps:** update dependency tsx to v4.19.4 ([#2981](https://github.com/homarr-labs/homarr/issues/2981)) ([a2dbac5](https://github.com/homarr-labs/homarr/commit/a2dbac5ad73aa50af7f4b40076bb5fcfa61adf80))
* **deps:** update dependency typescript-eslint to ^8.31.1 ([#2979](https://github.com/homarr-labs/homarr/issues/2979)) ([284ea23](https://github.com/homarr-labs/homarr/commit/284ea239f71f49c74e3ea60fa5de2fd968f1675b))
* **deps:** update mantine monorepo to ^7.17.7 ([#3004](https://github.com/homarr-labs/homarr/issues/3004)) ([a70b0b7](https://github.com/homarr-labs/homarr/commit/a70b0b70479df8fff8a2d256995844bad1fb55a0))
* **deps:** update tanstack-query monorepo to ^5.74.11 ([#2984](https://github.com/homarr-labs/homarr/issues/2984)) ([3299ced](https://github.com/homarr-labs/homarr/commit/3299cedc04cd87c7f014f69a900e7c6e6c33d954))
* **deps:** update tanstack-query monorepo to ^5.74.7 ([#2924](https://github.com/homarr-labs/homarr/issues/2924)) ([f4ce76a](https://github.com/homarr-labs/homarr/commit/f4ce76ac13136c2f559d3aff019dcc2add7c98dd))
* **deps:** update tanstack-query monorepo to ^5.74.9 ([#2983](https://github.com/homarr-labs/homarr/issues/2983)) ([1012cad](https://github.com/homarr-labs/homarr/commit/1012cad0e32f43f46bbb4e89ade3f0eb7fa54313))
* **deps:** update tanstack-query monorepo to ^5.75.0 ([#2998](https://github.com/homarr-labs/homarr/issues/2998)) ([0d1b40c](https://github.com/homarr-labs/homarr/commit/0d1b40c381fc068c770480f81c6c7dcefce0ec86))
* **deps:** update tanstack-query monorepo to ^5.75.1 ([#3006](https://github.com/homarr-labs/homarr/issues/3006)) ([2b1c433](https://github.com/homarr-labs/homarr/commit/2b1c433cefe667d930ce5178cc1218ffb7fa43c0))
* **deps:** update testcontainers-node monorepo to ^10.25.0 ([#2997](https://github.com/homarr-labs/homarr/issues/2997)) ([a934529](https://github.com/homarr-labs/homarr/commit/a934529146a0f53a047d2e3e834145c62c8ee891))
* **deps:** update tiptap monorepo to v2.11.9 ([#2987](https://github.com/homarr-labs/homarr/issues/2987)) ([70686a0](https://github.com/homarr-labs/homarr/commit/70686a00662a7f690efbb45920e5c45ad4a866ef))
* **deps:** update trpc monorepo to ^11.1.2 ([#2982](https://github.com/homarr-labs/homarr/issues/2982)) ([b10f33b](https://github.com/homarr-labs/homarr/commit/b10f33bf0b976217b2076ce84d8d1d9219b36e4c))
* **media-request:** add missing completed status ([#2990](https://github.com/homarr-labs/homarr/issues/2990)) ([9006d3d](https://github.com/homarr-labs/homarr/commit/9006d3d2fc21be5ba2eae616e467113356c91ebe))
* **releases-widget:** improve ui for smaller widgets ([#2962](https://github.com/homarr-labs/homarr/issues/2962)) ([f39a21c](https://github.com/homarr-labs/homarr/commit/f39a21c2962530298274ee705f3d85da023ea3e0))
* **unifi:** port is ignored ([#2995](https://github.com/homarr-labs/homarr/issues/2995)) ([0c9c5b8](https://github.com/homarr-labs/homarr/commit/0c9c5b8955c883191e5b6b137660c1a3cbd16ce4))
## [1.18.1](https://github.com/homarr-labs/homarr/compare/v1.18.0...v1.18.1) (2025-04-27)
### Bug Fixes
* cannot find module http-cookie-agent/http ([#2972](https://github.com/homarr-labs/homarr/issues/2972)) ([bb92f5b](https://github.com/homarr-labs/homarr/commit/bb92f5bb79e8f236dd149e87d14c1a35955140c7))
* **deps:** update dependency mysql2 to v3.14.1 ([#2969](https://github.com/homarr-labs/homarr/issues/2969)) ([5f80beb](https://github.com/homarr-labs/homarr/commit/5f80bebef2bd1598ad02dd1e169a1a6c27a4c143))
## [1.18.0](https://github.com/homarr-labs/homarr/compare/v1.17.0...v1.18.0) (2025-04-25)
### Features
* add releases widget ([#2497](https://github.com/homarr-labs/homarr/issues/2497)) ([3dcee8c](https://github.com/homarr-labs/homarr/commit/3dcee8cb869dd11d54631636253ad17e2af97dd7))
* **media-server:** add option to only show playing sessions ([#2899](https://github.com/homarr-labs/homarr/issues/2899)) ([f98750d](https://github.com/homarr-labs/homarr/commit/f98750d0b3e96b234d6f57b82bf7aa8729a091f2))
* migrate to unifi package ([#2894](https://github.com/homarr-labs/homarr/issues/2894)) ([8550c69](https://github.com/homarr-labs/homarr/commit/8550c69d51497676b654e204a85cdc84e8a38e30))
* **widgets:** add title to advanced options ([#2909](https://github.com/homarr-labs/homarr/issues/2909)) ([c64d903](https://github.com/homarr-labs/homarr/commit/c64d903f2b6375970325fb5031afd468dbf0ae11))
### Bug Fixes
* **certificates:** improve validation and prevent crash ([#2910](https://github.com/homarr-labs/homarr/issues/2910)) ([c514247](https://github.com/homarr-labs/homarr/commit/c51424717ddce5a2f4110658a1fbfab375185f66))
* **deps:** update dependency @auth/core to ^0.39.0 ([#2904](https://github.com/homarr-labs/homarr/issues/2904)) ([b0d7f9a](https://github.com/homarr-labs/homarr/commit/b0d7f9a48c05d380ff84ed7f48c9a9fd8dc33602))
* **deps:** update dependency @auth/drizzle-adapter to ^1.9.0 ([#2905](https://github.com/homarr-labs/homarr/issues/2905)) ([fdf700d](https://github.com/homarr-labs/homarr/commit/fdf700dba51d0bc4e72cdd63a63acb99c2196329))
* **deps:** update dependency @ctrl/qbittorrent to ^9.6.0 ([#2914](https://github.com/homarr-labs/homarr/issues/2914)) ([3d50808](https://github.com/homarr-labs/homarr/commit/3d508088d5a7d7fda53f49c196c2c6dd84d598d1))
* **deps:** update dependency @t3-oss/env-nextjs to ^0.13.0 ([#2935](https://github.com/homarr-labs/homarr/issues/2935)) ([eb95ee2](https://github.com/homarr-labs/homarr/commit/eb95ee285b727a8bda7ac9df3f75c25ced475358))
* **deps:** update dependency dockerode to ^4.0.6 ([#2901](https://github.com/homarr-labs/homarr/issues/2901)) ([593e3ee](https://github.com/homarr-labs/homarr/commit/593e3ee5a6f9cb8d50a51262c85c3c067fce0691))
* **deps:** update dependency drizzle-orm to ^0.43.0 ([#2939](https://github.com/homarr-labs/homarr/issues/2939)) ([295cc4a](https://github.com/homarr-labs/homarr/commit/295cc4a3c41ba9ad20e93df9d6ea5627d56d310d))
* **deps:** update dependency drizzle-orm to ^0.43.1 ([#2947](https://github.com/homarr-labs/homarr/issues/2947)) ([1d28bf4](https://github.com/homarr-labs/homarr/commit/1d28bf4c93c43c1956c5b386ca8a02ca6ae59eb5))
* **deps:** update dependency glob to ^11.0.2 ([#2931](https://github.com/homarr-labs/homarr/issues/2931)) ([573d5f8](https://github.com/homarr-labs/homarr/commit/573d5f89d6576020c3a4393372a56f8f75628e58))
* **deps:** update dependency next-auth to v5.0.0-beta.26 ([#2906](https://github.com/homarr-labs/homarr/issues/2906)) ([6e6edc8](https://github.com/homarr-labs/homarr/commit/6e6edc8a0d809614588967b51751257929a6ccd3))
* **deps:** update dependency next-auth to v5.0.0-beta.27 ([#2940](https://github.com/homarr-labs/homarr/issues/2940)) ([2533083](https://github.com/homarr-labs/homarr/commit/253308357a55ab2fac7ac4144215beefdc517b8d))
* **deps:** update dependency next-intl to v4.0.3 ([#2922](https://github.com/homarr-labs/homarr/issues/2922)) ([3172e6e](https://github.com/homarr-labs/homarr/commit/3172e6e0c4e55f62a7ee32ca84fcaa940e774565))
* **deps:** update dependency next-intl to v4.1.0 ([#2938](https://github.com/homarr-labs/homarr/issues/2938)) ([767fe94](https://github.com/homarr-labs/homarr/commit/767fe946d78268eddce6e213665176b8c6397e92))
* **deps:** update dependency sass to ^1.87.0 ([#2921](https://github.com/homarr-labs/homarr/issues/2921)) ([fed28bd](https://github.com/homarr-labs/homarr/commit/fed28bd77546be83b9965bd1116996b621f6b639))
* **deps:** update dependency trpc-to-openapi to ^2.2.0 ([#2903](https://github.com/homarr-labs/homarr/issues/2903)) ([2bf67c3](https://github.com/homarr-labs/homarr/commit/2bf67c3de8609eb504e322d830f507973a55d6fc))
* **deps:** update dependency tsdav to ^2.1.4 ([#2944](https://github.com/homarr-labs/homarr/issues/2944)) ([d78b2e1](https://github.com/homarr-labs/homarr/commit/d78b2e1e85a6c72ce1366e50ee4a1e83176b7913))
* **deps:** update dependency typescript-eslint to ^8.31.0 ([#2919](https://github.com/homarr-labs/homarr/issues/2919)) ([7aa3790](https://github.com/homarr-labs/homarr/commit/7aa379076da24c90238dd5fe2714dbceb3b84c2a))
* **deps:** update mantine monorepo to ^7.17.5 ([#2937](https://github.com/homarr-labs/homarr/issues/2937)) ([aba963c](https://github.com/homarr-labs/homarr/commit/aba963c7cf0d554d51e5fc233e546e4917ecdf67))
* **deps:** update trpc monorepo to ^11.1.1 ([#2927](https://github.com/homarr-labs/homarr/issues/2927)) ([bd3535d](https://github.com/homarr-labs/homarr/commit/bd3535dcf84569b87cf647bc5e0b957667b7e199))
* **deps:** update turbo monorepo to ^2.5.1 ([#2934](https://github.com/homarr-labs/homarr/issues/2934)) ([7766c3c](https://github.com/homarr-labs/homarr/commit/7766c3cbbc1187f6c38157fe15182c8b7d329acf))
* **deps:** update turbo monorepo to ^2.5.2 ([#2949](https://github.com/homarr-labs/homarr/issues/2949)) ([d97e740](https://github.com/homarr-labs/homarr/commit/d97e74047dc107abb05ca65d8a91a41c6071fbd4))
## [1.17.0](https://github.com/homarr-labs/homarr/compare/v1.16.0...v1.17.0) (2025-04-18)
### Features
* add api-key support for tdarr ([#2890](https://github.com/homarr-labs/homarr/issues/2890)) ([3d68f4d](https://github.com/homarr-labs/homarr/commit/3d68f4d1283b6cb27f9532d1adf52cef78a0fb03))
* Prefer primary color ([#2665](https://github.com/homarr-labs/homarr/issues/2665)) ([00053bd](https://github.com/homarr-labs/homarr/commit/00053bda7bbcaaf359074c3b35573ee0afc1409d))
### Bug Fixes
* calculate available space correctly and filter unmounted drives ([#2885](https://github.com/homarr-labs/homarr/issues/2885)) ([cfca0ba](https://github.com/homarr-labs/homarr/commit/cfca0ba58323ec09f8f64da2fee251ef14ba914a))
* **deps:** update dependency drizzle-kit to ^0.31.0 ([#2881](https://github.com/homarr-labs/homarr/issues/2881)) ([97390c6](https://github.com/homarr-labs/homarr/commit/97390c60300f3d16dabf0710fa435c6b3ae86a6f))
* **deps:** update dependency drizzle-orm to ^0.42.0 ([#2882](https://github.com/homarr-labs/homarr/issues/2882)) ([47d7abc](https://github.com/homarr-labs/homarr/commit/47d7abc0d739b91328834e0eaea033637632f22c))
* **deps:** update dependency jotai to ^2.12.3 ([#2868](https://github.com/homarr-labs/homarr/issues/2868)) ([c57b63b](https://github.com/homarr-labs/homarr/commit/c57b63b506733459ea0c09776917042fa878829b))
* **deps:** update dependency recharts to ^2.15.3 ([#2893](https://github.com/homarr-labs/homarr/issues/2893)) ([5fcb7a3](https://github.com/homarr-labs/homarr/commit/5fcb7a3b9c279c1b402e120a4a6f08265e8d5912))
* **deps:** update dependency swagger-ui-react to ^5.21.0 ([#2866](https://github.com/homarr-labs/homarr/issues/2866)) ([ff1263b](https://github.com/homarr-labs/homarr/commit/ff1263b217e96cbb2bc6b8e98ff117c44ca9f00d))
* **deps:** update dependency typescript-eslint to ^8.30.0 ([#2873](https://github.com/homarr-labs/homarr/issues/2873)) ([089734b](https://github.com/homarr-labs/homarr/commit/089734b293a21d6eeaa7c5df5ee9930853ed2fcf))
* **deps:** update dependency typescript-eslint to ^8.30.1 ([#2876](https://github.com/homarr-labs/homarr/issues/2876)) ([fdca513](https://github.com/homarr-labs/homarr/commit/fdca513929e9e594af2d59ad17c915cbc7ac2e08))
* **deps:** update dependency zod to ^3.24.3 ([#2886](https://github.com/homarr-labs/homarr/issues/2886)) ([1bfe397](https://github.com/homarr-labs/homarr/commit/1bfe397d68a88037e35701aab8a7b4c9027ec1e3))
* **deps:** update nextjs monorepo to v15.3.1 ([#2889](https://github.com/homarr-labs/homarr/issues/2889)) ([d37cdbd](https://github.com/homarr-labs/homarr/commit/d37cdbd080c2b3a4df536ab7dad4fc4a4b3c915f))
* **deps:** update tanstack-query monorepo to ^5.73.3 ([#2860](https://github.com/homarr-labs/homarr/issues/2860)) ([4e6ab03](https://github.com/homarr-labs/homarr/commit/4e6ab037c9245103b05afe977e9c288082a2b539))
* **deps:** update tanstack-query monorepo to ^5.74.0 ([#2864](https://github.com/homarr-labs/homarr/issues/2864)) ([ac78e87](https://github.com/homarr-labs/homarr/commit/ac78e8704287f45d6a8ca26cba9b87146e1c6161))
* **deps:** update tanstack-query monorepo to ^5.74.2 ([#2869](https://github.com/homarr-labs/homarr/issues/2869)) ([bf4a1cf](https://github.com/homarr-labs/homarr/commit/bf4a1cf1e8c99ada97adf6ee118ddaafc00115e9))
* **deps:** update tanstack-query monorepo to ^5.74.3 ([#2871](https://github.com/homarr-labs/homarr/issues/2871)) ([02fb1b9](https://github.com/homarr-labs/homarr/commit/02fb1b9dae501e475353e8e2510e4f8938c4e124))
* **deps:** update tanstack-query monorepo to ^5.74.4 ([#2888](https://github.com/homarr-labs/homarr/issues/2888)) ([17174b6](https://github.com/homarr-labs/homarr/commit/17174b6b96ae16b480e2bc5eb9f8ab7b914bc843))
* Tdarr widget empty statistics tab ([#2883](https://github.com/homarr-labs/homarr/issues/2883)) ([2d12c1e](https://github.com/homarr-labs/homarr/commit/2d12c1e641398e20a70f8bfb24a48de02498496a))
* **widget:** format stock values and changes for better readability ([#2836](https://github.com/homarr-labs/homarr/issues/2836)) ([a29556e](https://github.com/homarr-labs/homarr/commit/a29556ee8ec50aa384a9a46f6f5a5fa5dfa4cdac))
## [1.16.0](https://github.com/homarr-labs/homarr/compare/v1.15.0...v1.16.0) (2025-04-11)
### Features
* **boards:** add quick app add menu item ([#2681](https://github.com/homarr-labs/homarr/issues/2681)) ([7a3c836](https://github.com/homarr-labs/homarr/commit/7a3c836a70a9b9656c6fe74fe6a4bf8e7166a030))
* support aria2 integration ([#2226](https://github.com/homarr-labs/homarr/issues/2226)) ([94263c4](https://github.com/homarr-labs/homarr/commit/94263c445b8a0d0cdbea70ebe45ef932b426204d))
### Bug Fixes
* **boards:** add missing upload button for background in settings ([#2853](https://github.com/homarr-labs/homarr/issues/2853)) ([4baec7e](https://github.com/homarr-labs/homarr/commit/4baec7e3ff33ac0973daf0ac1f0171e349787bdb))
* **deps:** update dependency dotenv to ^16.5.0 ([#2848](https://github.com/homarr-labs/homarr/issues/2848)) ([c84dd2f](https://github.com/homarr-labs/homarr/commit/c84dd2fd797ca674e4f553ddc71926e9b533daa4))
* **deps:** update dependency eslint-config-prettier to ^10.1.2 ([#2842](https://github.com/homarr-labs/homarr/issues/2842)) ([924a7d0](https://github.com/homarr-labs/homarr/commit/924a7d076ef8bdd67afcce2e9f850bf334b43c3a))
* **deps:** update dependency ioredis to v5.6.1 ([#2851](https://github.com/homarr-labs/homarr/issues/2851)) ([0ed6a79](https://github.com/homarr-labs/homarr/commit/0ed6a79a5bba25fa7a90bec00d3815b3fffae427))
* **deps:** update dependency octokit to ^4.1.3 ([#2847](https://github.com/homarr-labs/homarr/issues/2847)) ([4b0b892](https://github.com/homarr-labs/homarr/commit/4b0b89225000137f02f3b89376c46d4d01387128))
* **deps:** update dependency swagger-ui-react to ^5.20.8 ([#2844](https://github.com/homarr-labs/homarr/issues/2844)) ([45d285a](https://github.com/homarr-labs/homarr/commit/45d285a76ba46d4d4c594f1f2e822d784a281b72))
* **deps:** update dependency undici to v7.8.0 ([#2841](https://github.com/homarr-labs/homarr/issues/2841)) ([f13be4a](https://github.com/homarr-labs/homarr/commit/f13be4a70870aca99a7f49f5068dadcd7fe01c22))
* **deps:** update nextjs monorepo to v15.3.0 ([#2838](https://github.com/homarr-labs/homarr/issues/2838)) ([a1fa429](https://github.com/homarr-labs/homarr/commit/a1fa4297ead54a9f8c842b2d23001f725359e000))
* **deps:** update tanstack-query monorepo to ^5.72.2 ([#2834](https://github.com/homarr-labs/homarr/issues/2834)) ([f5e3138](https://github.com/homarr-labs/homarr/commit/f5e3138c8118295d8230c3655af1c3f11ee5d4ff))
* **deps:** update testcontainers-node monorepo to ^10.24.2 ([#2846](https://github.com/homarr-labs/homarr/issues/2846)) ([057ff2c](https://github.com/homarr-labs/homarr/commit/057ff2cfc1cf062a3c48deb28699df8e1ba89f75))
* **deps:** update trpc monorepo to ^11.1.0 ([#2850](https://github.com/homarr-labs/homarr/issues/2850)) ([0fe4ea9](https://github.com/homarr-labs/homarr/commit/0fe4ea9dd9cbfb2def1bede9db8eaf649f79f1b0))
## [1.15.0](https://github.com/homarr-labs/homarr/compare/v1.14.0...v1.15.0) (2025-04-09)
### Features
* **boards:** add upload buttons for background, favicon and logo ([#2770](https://github.com/homarr-labs/homarr/issues/2770)) ([e07b9e6](https://github.com/homarr-labs/homarr/commit/e07b9e6a88ff0aad0aa5dae2f32f648ad5f0c0a8))
* unifi controller integration ([#2236](https://github.com/homarr-labs/homarr/issues/2236)) ([c1cd563](https://github.com/homarr-labs/homarr/commit/c1cd56304872b5f7b2b49624c6f5018f750d126b))
### Bug Fixes
* **deps:** update dependency @kubernetes/client-node to ^1.1.2 ([#2825](https://github.com/homarr-labs/homarr/issues/2825)) ([b064304](https://github.com/homarr-labs/homarr/commit/b064304201ed2ce52162edd5bc9ab5131da7ac97))
* **deps:** update dependency ldapts to v7.4.0 ([#2815](https://github.com/homarr-labs/homarr/issues/2815)) ([275224e](https://github.com/homarr-labs/homarr/commit/275224ee68bb49f329a6765095b46ec249027aca))
* **deps:** update dependency swagger-ui-react to ^5.20.7 ([#2816](https://github.com/homarr-labs/homarr/issues/2816)) ([85f8d23](https://github.com/homarr-labs/homarr/commit/85f8d23da3f3ec17945d79ef8a5212c5ceeefe75))
* **deps:** update dependency typescript-eslint to ^8.29.1 ([#2821](https://github.com/homarr-labs/homarr/issues/2821)) ([17d9272](https://github.com/homarr-labs/homarr/commit/17d92727f24cf0e2491dc93fd7c72759a24bbd54))
* **deps:** update mantine monorepo to ^7.17.4 ([#2824](https://github.com/homarr-labs/homarr/issues/2824)) ([bd5dcb9](https://github.com/homarr-labs/homarr/commit/bd5dcb991c8bd476cefbae1b8b2f407dd8db1acb))
* **deps:** update nextjs monorepo to v15.2.5 ([#2828](https://github.com/homarr-labs/homarr/issues/2828)) ([0fc7ca7](https://github.com/homarr-labs/homarr/commit/0fc7ca7738aee2d86940f43e3593368d4102cc73))
* **deps:** update tanstack-query monorepo to ^5.72.0 ([#2817](https://github.com/homarr-labs/homarr/issues/2817)) ([df212cf](https://github.com/homarr-labs/homarr/commit/df212cf5114a970ad4aed84811cfc7a2e4be67b3))
* **deps:** update tanstack-query monorepo to ^5.72.1 ([#2823](https://github.com/homarr-labs/homarr/issues/2823)) ([c01d9a4](https://github.com/homarr-labs/homarr/commit/c01d9a4dc5aba540a2b333a94507d027e5fb8d99))
* **deps:** update testcontainers-node monorepo to ^10.24.1 ([#2831](https://github.com/homarr-labs/homarr/issues/2831)) ([092c95b](https://github.com/homarr-labs/homarr/commit/092c95bbe0be73ad3dedb13c7f84ba7c36ca14db))
* **deps:** update trpc monorepo to ^11.0.3 ([#2822](https://github.com/homarr-labs/homarr/issues/2822)) ([a821bd7](https://github.com/homarr-labs/homarr/commit/a821bd71f47e03a276a96af705bec90a00229e0f))
* **deps:** update trpc monorepo to ^11.0.4 ([#2827](https://github.com/homarr-labs/homarr/issues/2827)) ([75100d1](https://github.com/homarr-labs/homarr/commit/75100d1a4926b838682206189eac046d844dba13))
* **medias:** upload size limited by nginx to 1mb ([#2813](https://github.com/homarr-labs/homarr/issues/2813)) ([bf046a7](https://github.com/homarr-labs/homarr/commit/bf046a715cd6897792b2f1582d076c055063bf57))
* **performance:** remove turbopack flag to no longer go out of memory after ([#2803](https://github.com/homarr-labs/homarr/issues/2803)) ([c6102ed](https://github.com/homarr-labs/homarr/commit/c6102edff467082803769c488ce642500edcdd25))
* **security:** missing authorization check for changes to notebook widget ([#2832](https://github.com/homarr-labs/homarr/issues/2832)) ([3948f7f](https://github.com/homarr-labs/homarr/commit/3948f7f9a47cd4562c9b4fd8252f116b8a95c982))
## [1.14.0](https://github.com/homarr-labs/homarr/compare/v1.13.1...v1.14.0) (2025-04-04)
### Features
* **cli:** add cli command to recreate credentials admin ([#2779](https://github.com/homarr-labs/homarr/issues/2779)) ([6742532](https://github.com/homarr-labs/homarr/commit/674253262bd4853ebde85d229d666a38733a7026))
### Bug Fixes
* **deps:** update dependency @ctrl/qbittorrent to ^9.5.2 ([#2795](https://github.com/homarr-labs/homarr/issues/2795)) ([ea8bf43](https://github.com/homarr-labs/homarr/commit/ea8bf43d7b56f526fbf2d4a3955c2c179e4e74d8))
* **deps:** update dependency @kubernetes/client-node to ^1.1.1 ([#2793](https://github.com/homarr-labs/homarr/issues/2793)) ([76b8b35](https://github.com/homarr-labs/homarr/commit/76b8b35cc1f586797210bdcd3d848d64b5f36d63))
* **deps:** update dependency dockerode to ^4.0.5 ([#2757](https://github.com/homarr-labs/homarr/issues/2757)) ([db13fd0](https://github.com/homarr-labs/homarr/commit/db13fd03c018cb2f79f0b094b93b69d69fde97ff))
* **deps:** update dependency eslint-plugin-react to ^7.37.5 ([#2794](https://github.com/homarr-labs/homarr/issues/2794)) ([f9aeb8f](https://github.com/homarr-labs/homarr/commit/f9aeb8ffe3ff4cd65cd76c560a687912262a735f))
* **deps:** update dependency recharts to ^2.15.2 ([#2785](https://github.com/homarr-labs/homarr/issues/2785)) ([f33b1cb](https://github.com/homarr-labs/homarr/commit/f33b1cb5d0c317df5348e87e0f5f43fdc6044615))
* **deps:** update dependency sass to ^1.86.1 ([#2762](https://github.com/homarr-labs/homarr/issues/2762)) ([3e35051](https://github.com/homarr-labs/homarr/commit/3e3505176a7df01bae5173d7f62380fa17859e8f))
* **deps:** update dependency sass to ^1.86.2 ([#2784](https://github.com/homarr-labs/homarr/issues/2784)) ([3131082](https://github.com/homarr-labs/homarr/commit/3131082c778191be5568a93bbf5f10219f3c2be9))
* **deps:** update dependency sass to ^1.86.3 ([#2796](https://github.com/homarr-labs/homarr/issues/2796)) ([98ecc68](https://github.com/homarr-labs/homarr/commit/98ecc683e563475644530682f08d335ac3671a5e))
* **deps:** update dependency swagger-ui-react to ^5.20.3 ([#2777](https://github.com/homarr-labs/homarr/issues/2777)) ([81d1231](https://github.com/homarr-labs/homarr/commit/81d1231d8fc026dd9f1ba58e28937aba9d94d88e))
* **deps:** update dependency swagger-ui-react to ^5.20.4 ([#2786](https://github.com/homarr-labs/homarr/issues/2786)) ([85672d9](https://github.com/homarr-labs/homarr/commit/85672d9cf15bf48c9cc2fb4e746f65814f46361b))
* **deps:** update dependency swagger-ui-react to ^5.20.5 ([#2787](https://github.com/homarr-labs/homarr/issues/2787)) ([3363eeb](https://github.com/homarr-labs/homarr/commit/3363eebdddf10a028d1cdebcb4d581105ba1d63c))
* **deps:** update dependency swagger-ui-react to ^5.20.6 ([#2798](https://github.com/homarr-labs/homarr/issues/2798)) ([968a82b](https://github.com/homarr-labs/homarr/commit/968a82baf5d6adc64c680508493b77b892fdd5fc))
* **deps:** update dependency trpc-to-openapi to ^2.1.4 ([#2748](https://github.com/homarr-labs/homarr/issues/2748)) ([c69e331](https://github.com/homarr-labs/homarr/commit/c69e331ded3cfde837dcf28c8b1ba4a9b7a3c8aa))
* **deps:** update dependency trpc-to-openapi to ^2.1.5 ([#2799](https://github.com/homarr-labs/homarr/issues/2799)) ([b03941e](https://github.com/homarr-labs/homarr/commit/b03941ed80057154f0b4b04c764652ada1fdf18c))
* **deps:** update dependency typescript-eslint to ^8.29.0 ([#2761](https://github.com/homarr-labs/homarr/issues/2761)) ([3687c4b](https://github.com/homarr-labs/homarr/commit/3687c4b3d655d9cebb85fb3c3ad80f494e9a8f0a))
* **deps:** update dependency undici to v7.7.0 ([#2776](https://github.com/homarr-labs/homarr/issues/2776)) ([ff5c313](https://github.com/homarr-labs/homarr/commit/ff5c31342fe9eb668533d7d9f0d65ad3f01eba0a))
* **deps:** update tanstack-query monorepo to ^5.71.0 ([#2752](https://github.com/homarr-labs/homarr/issues/2752)) ([77044e8](https://github.com/homarr-labs/homarr/commit/77044e8e0b8e7267b8547eb1d0f86026948b292a))
* **deps:** update tanstack-query monorepo to ^5.71.1 ([#2756](https://github.com/homarr-labs/homarr/issues/2756)) ([0b8a009](https://github.com/homarr-labs/homarr/commit/0b8a0098bd02a71107af9702e39000227028ec20))
* **deps:** update tanstack-query monorepo to ^5.71.10 ([#2790](https://github.com/homarr-labs/homarr/issues/2790)) ([964368a](https://github.com/homarr-labs/homarr/commit/964368a06c8385df63a252e2d0759a2524348ac6))
* **deps:** update tanstack-query monorepo to ^5.71.3 ([#2767](https://github.com/homarr-labs/homarr/issues/2767)) ([858fe6e](https://github.com/homarr-labs/homarr/commit/858fe6e19a682bb0fa0cfa0d11c2423447fdad2f))
* **deps:** update tanstack-query monorepo to ^5.71.5 ([#2780](https://github.com/homarr-labs/homarr/issues/2780)) ([466d0e0](https://github.com/homarr-labs/homarr/commit/466d0e00530903ab4101cd2e94c00ab8a7c1c8ce))
* **deps:** update testcontainers-node monorepo to ^10.24.0 ([#2789](https://github.com/homarr-labs/homarr/issues/2789)) ([1b05041](https://github.com/homarr-labs/homarr/commit/1b0504198b41032319ead2604721abaaf08709d8))
* **deps:** update tiptap monorepo to v2.11.7 ([#2749](https://github.com/homarr-labs/homarr/issues/2749)) ([0ec8f23](https://github.com/homarr-labs/homarr/commit/0ec8f23b7c655889d8f6fb9fcfd66a9736eb5a10))
* **deps:** update tiptap monorepo to v2.11.7 ([#2750](https://github.com/homarr-labs/homarr/issues/2750)) ([59b1630](https://github.com/homarr-labs/homarr/commit/59b16305326ac811ad91e89c0786b9e6b1b7e1df))
* **deps:** update trpc monorepo to ^11.0.2 ([#2792](https://github.com/homarr-labs/homarr/issues/2792)) ([8886380](https://github.com/homarr-labs/homarr/commit/8886380f0a9ea7d35cd54182a378b2f47b71c283))
* **deps:** update turbo monorepo to ^2.5.0 ([#2791](https://github.com/homarr-labs/homarr/issues/2791)) ([0b41967](https://github.com/homarr-labs/homarr/commit/0b41967ec366d925dbb172d38c3677d88f51f0a9))
* **nextcloud:** issue with self signed certificates ([#2760](https://github.com/homarr-labs/homarr/issues/2760)) ([7f77130](https://github.com/homarr-labs/homarr/commit/7f77130978eb7f25e571b7315250c91656a85d78))
* update check blocks page loading ([#2782](https://github.com/homarr-labs/homarr/issues/2782)) ([29ff879](https://github.com/homarr-labs/homarr/commit/29ff879dfd1036d44b7c4735a0b269a83173b4d1))
### Reverts
* add restriction callback to restrict visibility and modification of widget kinds ([#2746](https://github.com/homarr-labs/homarr/issues/2746)) ([9851531](https://github.com/homarr-labs/homarr/commit/98515312a2439d1ed9fc5b412843ca625120b2ac))
## [1.13.1](https://github.com/homarr-labs/homarr/compare/v1.13.0...v1.13.1) (2025-03-28) ## [1.13.1](https://github.com/homarr-labs/homarr/compare/v1.13.0...v1.13.1) (2025-03-28)
### Bug Fixes ### Bug Fixes
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:22.14.0-alpine AS base FROM node:22.16.0-alpine AS base
FROM base AS builder FROM base AS builder
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
+27
View File
@@ -49,6 +49,33 @@ const nextConfig: NextConfig = {
images: { images: {
domains: ["cdn.jsdelivr.net"], domains: ["cdn.jsdelivr.net"],
}, },
// eslint-disable-next-line @typescript-eslint/require-await,no-restricted-syntax
async headers() {
return [
{
source: "/(.*)", // Apply CSP to all routes
headers: [
{
key: "Content-Security-Policy",
value: `
default-src 'self';
script-src * 'unsafe-inline' 'unsafe-eval';
base-uri 'self';
connect-src *;
style-src * 'unsafe-inline';
frame-ancestors *;
frame-src *;
form-action 'self';
img-src * data:;
font-src * data:;
`
.replace(/\s{2,}/g, " ")
.trim(),
},
],
},
];
},
}; };
// Skip transform is used because of webpack loader, without it for example 'Tooltip.Floating' will not work and show an error // Skip transform is used because of webpack loader, without it for example 'Tooltip.Floating' will not work and show an error
+29 -29
View File
@@ -6,7 +6,7 @@
"scripts": { "scripts": {
"build": "pnpm with-env next build", "build": "pnpm with-env next build",
"clean": "git clean -xdf .next .turbo node_modules", "clean": "git clean -xdf .next .turbo node_modules",
"dev": "pnpm with-env next dev --turbopack", "dev": "pnpm with-env next dev",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint", "lint": "eslint",
"start": "pnpm with-env next start", "start": "pnpm with-env next start",
@@ -48,59 +48,59 @@
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0", "@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "^7.17.3", "@mantine/colors-generator": "^8.0.2",
"@mantine/core": "^7.17.3", "@mantine/core": "^8.0.2",
"@mantine/dropzone": "^7.17.3", "@mantine/dropzone": "^8.0.2",
"@mantine/hooks": "^7.17.3", "@mantine/hooks": "^8.0.2",
"@mantine/modals": "^7.17.3", "@mantine/modals": "^8.0.2",
"@mantine/tiptap": "^7.17.3", "@mantine/tiptap": "^8.0.2",
"@million/lint": "1.0.14", "@million/lint": "1.0.14",
"@tabler/icons-react": "^3.31.0", "@tabler/icons-react": "^3.34.0",
"@tanstack/react-query": "^5.70.0", "@tanstack/react-query": "^5.80.6",
"@tanstack/react-query-devtools": "^5.70.0", "@tanstack/react-query-devtools": "^5.80.6",
"@tanstack/react-query-next-experimental": "^5.70.0", "@tanstack/react-query-next-experimental": "^5.80.6",
"@trpc/client": "^11.0.1", "@trpc/client": "^11.3.1",
"@trpc/next": "^11.0.1", "@trpc/next": "^11.3.1",
"@trpc/react-query": "^11.0.1", "@trpc/react-query": "^11.3.1",
"@trpc/server": "^11.0.1", "@trpc/server": "^11.3.1",
"@xterm/addon-canvas": "^0.7.0", "@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.10.0", "@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"chroma-js": "^3.1.2", "chroma-js": "^3.1.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dotenv": "^16.4.7", "dotenv": "^16.5.0",
"flag-icons": "^7.3.2", "flag-icons": "^7.5.0",
"glob": "^11.0.1", "glob": "^11.0.2",
"jotai": "^2.12.2", "jotai": "^2.12.5",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "15.2.4", "next": "15.3.3",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-error-boundary": "^5.0.0", "react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1", "react-simple-code-editor": "^0.14.1",
"sass": "^1.86.0", "sass": "^1.89.1",
"superjson": "2.2.2", "superjson": "2.2.2",
"swagger-ui-react": "^5.20.2", "swagger-ui-react": "^5.24.0",
"use-deep-compare-effect": "^1.8.1", "use-deep-compare-effect": "^1.8.1",
"zod": "^3.24.2" "zod": "^3.25.56"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1", "@types/chroma-js": "3.1.1",
"@types/node": "^22.13.14", "@types/node": "^22.15.30",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@types/react": "19.0.12", "@types/react": "19.1.6",
"@types/react-dom": "19.0.4", "@types/react-dom": "19.1.6",
"@types/swagger-ui-react": "^5.18.0", "@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"eslint": "^9.23.0", "eslint": "^9.28.0",
"node-loader": "^2.1.0", "node-loader": "^2.1.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.2" "typescript": "^5.8.3"
} }
} }
@@ -9,7 +9,7 @@ import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui"; import { CustomPasswordInput } from "@homarr/ui";
import { validation } from "@homarr/validation"; import { userRegistrationSchema } from "@homarr/validation/user";
interface RegistrationFormProps { interface RegistrationFormProps {
invite: { invite: {
@@ -22,7 +22,7 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
const t = useScopedI18n("user"); const t = useScopedI18n("user");
const router = useRouter(); const router = useRouter();
const { mutate, isPending } = clientApi.user.register.useMutation(); const { mutate, isPending } = clientApi.user.register.useMutation();
const form = useZodForm(validation.user.registration, { const form = useZodForm(userRegistrationSchema, {
initialValues: { initialValues: {
username: "", username: "",
password: "", password: "",
@@ -30,7 +30,7 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
}, },
}); });
const handleSubmit = (values: z.infer<typeof validation.user.registration>) => { const handleSubmit = (values: z.infer<typeof userRegistrationSchema>) => {
mutate( mutate(
{ {
...values, ...values,
@@ -2,7 +2,7 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { Anchor, Button, Card, Code, Collapse, Divider, PasswordInput, Stack, Text, TextInput } from "@mantine/core"; import { Anchor, Button, Card, Code, Collapse, Divider, PasswordInput, Stack, Text, TextInput } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import { z } from "zod"; import { z } from "zod";
@@ -13,7 +13,9 @@ import type { useForm } from "@homarr/form";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { userSignInSchema } from "@homarr/validation/user";
type Provider = "credentials" | "ldap" | "oidc";
interface LoginFormProps { interface LoginFormProps {
providers: string[]; providers: string[];
@@ -22,10 +24,12 @@ interface LoginFormProps {
callbackUrl: string; callbackUrl: string;
} }
const extendedValidation = validation.user.signIn.extend({ provider: z.enum(["credentials", "ldap"]) }); const extendedValidation = userSignInSchema.extend({ provider: z.enum(["credentials", "ldap"]) });
export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, callbackUrl }: LoginFormProps) => { export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, callbackUrl }: LoginFormProps) => {
const t = useScopedI18n("user"); const t = useScopedI18n("user");
const searchParams = useSearchParams();
const isError = searchParams.has("error");
const router = useRouter(); const router = useRouter();
const [isPending, setIsPending] = useState(false); const [isPending, setIsPending] = useState(false);
const form = useZodForm(extendedValidation, { const form = useZodForm(extendedValidation, {
@@ -39,22 +43,34 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
const credentialInputsVisible = providers.includes("credentials") || providers.includes("ldap"); const credentialInputsVisible = providers.includes("credentials") || providers.includes("ldap");
const onSuccess = useCallback( const onSuccess = useCallback(
async (response: Awaited<ReturnType<typeof signIn>>) => { async (provider: Provider, response: Awaited<ReturnType<typeof signIn>>) => {
if (response && (!response.ok || response.error)) { if (!response.ok || response.error) {
// eslint-disable-next-line @typescript-eslint/only-throw-error // eslint-disable-next-line @typescript-eslint/only-throw-error
throw response.error; throw response.error;
} }
if (provider === "oidc") {
if (!response.url) {
showErrorNotification({
title: t("action.login.notification.error.title"),
message: t("action.login.notification.error.message"),
autoClose: 10000,
});
return;
}
router.push(response.url);
return;
}
showSuccessNotification({ showSuccessNotification({
title: t("action.login.notification.success.title"), title: t("action.login.notification.success.title"),
message: t("action.login.notification.success.message"), message: t("action.login.notification.success.message"),
}); });
// Redirect to the callback URL if the response is defined and comes from a credentials provider (ldap or credentials). oidc is redirected automatically. // Redirect to the callback URL if the response is defined and comes from a credentials provider (ldap or credentials). oidc is redirected automatically.
if (response) { await revalidatePathActionAsync("/");
await revalidatePathActionAsync("/"); router.push(callbackUrl);
router.push(callbackUrl);
}
}, },
[t, router, callbackUrl], [t, router, callbackUrl],
); );
@@ -70,14 +86,14 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
}, [t]); }, [t]);
const signInAsync = useCallback( const signInAsync = useCallback(
async (provider: string, options?: Parameters<typeof signIn>[1]) => { async (provider: Provider, options?: Parameters<typeof signIn>[1]) => {
setIsPending(true); setIsPending(true);
await signIn(provider, { await signIn(provider, {
...options, ...options,
redirect: false, redirect: false,
callbackUrl: new URL(callbackUrl, window.location.href).href, callbackUrl: new URL(callbackUrl, window.location.href).href,
}) })
.then(onSuccess) .then((response) => onSuccess(provider, response))
.catch(onError); .catch(onError);
}, },
[setIsPending, onSuccess, onError, callbackUrl], [setIsPending, onSuccess, onError, callbackUrl],
@@ -86,11 +102,12 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
const isLoginInProgress = useRef(false); const isLoginInProgress = useRef(false);
useEffect(() => { useEffect(() => {
if (isError) return;
if (isOidcAutoLoginEnabled && !isPending && !isLoginInProgress.current) { if (isOidcAutoLoginEnabled && !isPending && !isLoginInProgress.current) {
isLoginInProgress.current = true; isLoginInProgress.current = true;
void signInAsync("oidc"); void signInAsync("oidc");
} }
}, [signInAsync, isOidcAutoLoginEnabled, isPending]); }, [signInAsync, isOidcAutoLoginEnabled, isPending, isError]);
return ( return (
<Stack gap="xl"> <Stack gap="xl">
@@ -4,15 +4,21 @@ import { TRPCError } from "@trpc/server";
// Placed here because gridstack styles are used for board content // Placed here because gridstack styles are used for board content
import "~/styles/gridstack.scss"; import "~/styles/gridstack.scss";
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { getQueryClient } from "@homarr/api/server";
import { IntegrationProvider } from "@homarr/auth/client"; import { IntegrationProvider } from "@homarr/auth/client";
import { auth } from "@homarr/auth/next"; import { auth } from "@homarr/auth/next";
import { getIntegrationsWithPermissionsAsync } from "@homarr/auth/server"; import { getIntegrationsWithPermissionsAsync } from "@homarr/auth/server";
import { isNullOrWhitespace } from "@homarr/common"; import { isNullOrWhitespace } from "@homarr/common";
import type { WidgetKind } from "@homarr/definitions";
import { logger } from "@homarr/log";
import { getI18n } from "@homarr/translation/server"; import { getI18n } from "@homarr/translation/server";
import { prefetchForKindAsync } from "@homarr/widgets/prefetch";
import { createMetaTitle } from "~/metadata"; import { createMetaTitle } from "~/metadata";
import { createBoardLayout } from "../_layout-creator"; import { createBoardLayout } from "../_layout-creator";
import type { Board } from "../_types"; import type { Board, Item } from "../_types";
import { DynamicClientBoard } from "./_dynamic-client"; import { DynamicClientBoard } from "./_dynamic-client";
import { BoardContentHeaderActions } from "./_header-actions"; import { BoardContentHeaderActions } from "./_header-actions";
@@ -31,14 +37,36 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
getInitialBoardAsync: getInitialBoard, getInitialBoardAsync: getInitialBoard,
}), }),
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
page: async () => { page: async ({ params }: { params: Promise<TParams> }) => {
const session = await auth(); const session = await auth();
const integrations = await getIntegrationsWithPermissionsAsync(session); const integrations = await getIntegrationsWithPermissionsAsync(session);
const board = await getInitialBoard(await params);
const queryClient = getQueryClient();
// Prefetch item data
const itemsMap = board.items.reduce((acc, item) => {
const existing = acc.get(item.kind);
if (existing) {
existing.push(item);
} else {
acc.set(item.kind, [item]);
}
return acc;
}, new Map<WidgetKind, Item[]>());
for (const [kind, items] of itemsMap) {
await prefetchForKindAsync(kind, queryClient, items).catch((error) => {
logger.error(new Error("Failed to prefetch widget", { cause: error }));
});
}
return ( return (
<IntegrationProvider integrations={integrations}> <HydrationBoundary state={dehydrate(queryClient)}>
<DynamicClientBoard /> <IntegrationProvider integrations={integrations}>
</IntegrationProvider> <DynamicClientBoard />
</IntegrationProvider>
</HydrationBoundary>
); );
}, },
generateMetadataAsync: async ({ params }: { params: Promise<TParams> }): Promise<Metadata> => { generateMetadataAsync: async ({ params }: { params: Promise<TParams> }): Promise<Metadata> => {
@@ -1,6 +1,5 @@
"use client"; "use client";
import type { MouseEvent } from "react";
import { useCallback, useEffect } from "react"; import { useCallback, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -25,9 +24,11 @@ import { useEditMode } from "@homarr/boards/edit-mode";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";
import { env } from "@homarr/common/env"; import { env } from "@homarr/common/env";
import { useConfirmModal, useModalAction } from "@homarr/modals"; import { useConfirmModal, useModalAction } from "@homarr/modals";
import { AppSelectModal } from "@homarr/modals-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { useItemActions } from "~/components/board/items/item-actions";
import { ItemSelectModal } from "~/components/board/items/item-select-modal"; import { ItemSelectModal } from "~/components/board/items/item-select-modal";
import { useBoardPermissions } from "~/components/board/permissions/client"; import { useBoardPermissions } from "~/components/board/permissions/client";
import { useCategoryActions } from "~/components/board/sections/category/category-actions"; import { useCategoryActions } from "~/components/board/sections/category/category-actions";
@@ -62,8 +63,10 @@ export const BoardContentHeaderActions = () => {
const AddMenu = () => { const AddMenu = () => {
const { openModal: openCategoryEditModal } = useModalAction(CategoryEditModal); const { openModal: openCategoryEditModal } = useModalAction(CategoryEditModal);
const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal); const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal);
const { openModal: openAppSelectModal } = useModalAction(AppSelectModal);
const { addCategoryToEnd } = useCategoryActions(); const { addCategoryToEnd } = useCategoryActions();
const { addDynamicSection } = useDynamicSectionActions(); const { addDynamicSection } = useDynamicSectionActions();
const { createItem } = useItemActions();
const t = useI18n(); const t = useI18n();
const handleAddCategory = useCallback( const handleAddCategory = useCallback(
@@ -90,6 +93,17 @@ const AddMenu = () => {
openItemSelectModal(); openItemSelectModal();
}, [openItemSelectModal]); }, [openItemSelectModal]);
const handleSelectApp = useCallback(() => {
openAppSelectModal({
onSelect: (appId) => {
createItem({
kind: "app",
options: { appId },
});
},
});
}, [openAppSelectModal, createItem]);
return ( return (
<Menu position="bottom-end" withArrow> <Menu position="bottom-end" withArrow>
<Menu.Target> <Menu.Target>
@@ -101,10 +115,14 @@ const AddMenu = () => {
</HeaderButton> </HeaderButton>
</Menu.Target> </Menu.Target>
<Menu.Dropdown style={{ transform: "translate(-3px, 0)" }}> <Menu.Dropdown style={{ transform: "translate(-3px, 0)" }}>
<Menu.Item leftSection={<IconBox size={20} />} onClick={handleSelectItem}> <Menu.Item leftSection={<IconResize size={20} />} onClick={handleSelectItem}>
{t("item.action.create")} {t("item.action.create")}
</Menu.Item> </Menu.Item>
<Menu.Item leftSection={<IconBox size={20} />} onClick={handleSelectApp}>
{t("app.action.add")}
</Menu.Item>
<Menu.Divider /> <Menu.Divider />
<Menu.Item leftSection={<IconBoxAlignTop size={20} />} onClick={handleAddCategory}> <Menu.Item leftSection={<IconBoxAlignTop size={20} />} onClick={handleAddCategory}>
@@ -185,17 +203,22 @@ const SelectBoardsMenu = () => {
); );
}; };
const anchorSelector = "a[href]:not([target='_blank'])";
const usePreventLeaveWithDirty = (isDirty: boolean) => { const usePreventLeaveWithDirty = (isDirty: boolean) => {
const t = useI18n(); const t = useI18n();
const { openConfirmModal } = useConfirmModal(); const { openConfirmModal } = useConfirmModal();
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
const handleClick = (event: MouseEvent<HTMLElement>) => { if (!isDirty) return;
const handleClick = (event: Event) => {
const target = (event.target as HTMLElement).closest("a"); const target = (event.target as HTMLElement).closest("a");
if (!target) return; if (!target) {
if (!isDirty) return; console.warn("No anchor element found for click event", event);
return;
}
event.preventDefault(); event.preventDefault();
@@ -212,33 +235,29 @@ const usePreventLeaveWithDirty = (isDirty: boolean) => {
}; };
const handlePopState = (event: Event) => { const handlePopState = (event: Event) => {
if (isDirty) { window.history.pushState(null, document.title, window.location.href);
window.history.pushState(null, document.title, window.location.href); event.preventDefault();
event.preventDefault();
} else {
window.history.back();
}
}; };
const handleBeforeUnload = (event: BeforeUnloadEvent) => { const handleBeforeUnload = (event: BeforeUnloadEvent) => {
if (!isDirty) return;
if (env.NODE_ENV === "development") return; // Allow to reload in development if (env.NODE_ENV === "development") return; // Allow to reload in development
event.preventDefault(); event.preventDefault();
event.returnValue = true; event.returnValue = true;
}; };
document.querySelectorAll("a").forEach((link) => { const anchors = document.querySelectorAll(anchorSelector);
link.addEventListener("click", handleClick as never); anchors.forEach((link) => {
link.addEventListener("click", handleClick);
}); });
window.addEventListener("popstate", handlePopState); window.addEventListener("popstate", handlePopState);
window.addEventListener("beforeunload", handleBeforeUnload); window.addEventListener("beforeunload", handleBeforeUnload);
return () => { return () => {
document.querySelectorAll("a").forEach((link) => { anchors.forEach((link) => {
link.removeEventListener("click", handleClick as never); link.removeEventListener("click", handleClick);
window.removeEventListener("popstate", handlePopState);
}); });
window.removeEventListener("popstate", handlePopState);
window.removeEventListener("beforeunload", handleBeforeUnload); window.removeEventListener("beforeunload", handleBeforeUnload);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -2,7 +2,7 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import type { MantineColorsTuple } from "@mantine/core"; import type { MantineColorsTuple } from "@mantine/core";
import { colorsTuple, createTheme, darken, lighten, MantineProvider } from "@mantine/core"; import { colorsTuple, createTheme, darken, lighten, MantineProvider, rem } from "@mantine/core";
import { useRequiredBoard } from "@homarr/boards/context"; import { useRequiredBoard } from "@homarr/boards/context";
import type { ColorScheme } from "@homarr/definitions"; import type { ColorScheme } from "@homarr/definitions";
@@ -24,6 +24,11 @@ export const BoardMantineProvider = ({
}, },
primaryColor: "primaryColor", primaryColor: "primaryColor",
autoContrast: true, autoContrast: true,
fontSizes: {
"2xl": rem(24),
"3xl": rem(28),
"4xl": rem(36),
},
}); });
return ( return (
@@ -20,7 +20,7 @@ import { useDisclosure } from "@mantine/hooks";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
import type { Board } from "../../_types"; import type { Board } from "../../_types";
import { generateColors } from "../../(content)/_theme"; import { generateColors } from "../../(content)/_theme";
@@ -35,7 +35,7 @@ const hexRegex = /^#[0-9a-fA-F]{6}$/;
const progressPercentageLabel = (value: number) => `${value}%`; const progressPercentageLabel = (value: number) => `${value}%`;
export const ColorSettingsContent = ({ board }: Props) => { export const ColorSettingsContent = ({ board }: Props) => {
const form = useZodForm(validation.board.savePartialSettings, { const form = useZodForm(boardSavePartialSettingsSchema, {
initialValues: { initialValues: {
primaryColor: board.primaryColor, primaryColor: board.primaryColor,
secondaryColor: board.secondaryColor, secondaryColor: board.secondaryColor,
@@ -123,7 +123,7 @@ export const ColorSettingsContent = ({ board }: Props) => {
</Grid.Col> </Grid.Col>
</Grid> </Grid>
<Group justify="end"> <Group justify="end">
<Button type="submit" loading={isPending} color="teal"> <Button type="submit" loading={isPending}>
{t("common.action.saveChanges")} {t("common.action.saveChanges")}
</Button> </Button>
</Group> </Group>
@@ -1,14 +1,20 @@
"use client"; "use client";
import { Button, Grid, Group, Stack, TextInput } from "@mantine/core"; import { startTransition } from "react";
import { ActionIcon, Autocomplete, Button, Center, Grid, Group, Popover, Stack, Text } from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import { IconPhotoOff, IconUpload } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { backgroundImageAttachments, backgroundImageRepeats, backgroundImageSizes } from "@homarr/definitions"; import { backgroundImageAttachments, backgroundImageRepeats, backgroundImageSizes } from "@homarr/definitions";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { UploadMedia } from "@homarr/forms-collection";
import type { TranslationObject } from "@homarr/translation"; import type { TranslationObject } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import type { SelectItemWithDescriptionBadge } from "@homarr/ui"; import type { SelectItemWithDescriptionBadge } from "@homarr/ui";
import { SelectWithDescriptionBadge } from "@homarr/ui"; import { SelectWithDescriptionBadge } from "@homarr/ui";
import { validation } from "@homarr/validation"; import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
import type { Board } from "../../_types"; import type { Board } from "../../_types";
import { useSavePartialSettingsMutation } from "./_shared"; import { useSavePartialSettingsMutation } from "./_shared";
@@ -18,8 +24,9 @@ interface Props {
} }
export const BackgroundSettingsContent = ({ board }: Props) => { export const BackgroundSettingsContent = ({ board }: Props) => {
const t = useI18n(); const t = useI18n();
const { data: session } = useSession();
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board); const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
const form = useZodForm(validation.board.savePartialSettings, { const form = useZodForm(boardSavePartialSettingsSchema, {
initialValues: { initialValues: {
backgroundImageUrl: board.backgroundImageUrl ?? "", backgroundImageUrl: board.backgroundImageUrl ?? "",
backgroundImageAttachment: board.backgroundImageAttachment, backgroundImageAttachment: board.backgroundImageAttachment,
@@ -28,6 +35,16 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
}, },
}); });
const [debouncedSearch] = useDebouncedValue(form.values.backgroundImageUrl, 200);
const medias = clientApi.media.getPaginated.useQuery({
page: 1,
pageSize: 10,
includeFromAllUsers: true,
search: debouncedSearch ?? "",
});
const images = medias.data?.items.filter((media) => media.contentType.startsWith("image/")) ?? [];
const imageMap = new Map(images.map((image) => [`/api/user-medias/${image.id}`, image]));
const backgroundImageAttachmentData = useBackgroundOptionData( const backgroundImageAttachmentData = useBackgroundOptionData(
"backgroundImageAttachment", "backgroundImageAttachment",
backgroundImageAttachments, backgroundImageAttachments,
@@ -47,10 +64,76 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
<Stack> <Stack>
<Grid> <Grid>
<Grid.Col span={12}> <Grid.Col span={12}>
<TextInput <Group wrap="nowrap" gap="xs" w="100%" align="start">
label={t("board.field.backgroundImageUrl.label")} <Autocomplete
{...form.getInputProps("backgroundImageUrl")} flex={1}
/> leftSection={
form.values.backgroundImageUrl &&
form.values.backgroundImageUrl.trim().length >= 2 && (
<Popover width={300} withArrow>
<Popover.Target>
<Center h="100%">
<ImagePreview src={form.values.backgroundImageUrl} w={20} h={20} />
</Center>
</Popover.Target>
<Popover.Dropdown>
<ImagePreview src={form.values.backgroundImageUrl} w="100%" />
</Popover.Dropdown>
</Popover>
)
}
// We filter it on the server
filter={({ options }) => options}
label={t("board.field.backgroundImageUrl.label")}
placeholder={`${t("board.field.backgroundImageUrl.placeholder")}...`}
renderOption={({ option }) => {
const current = imageMap.get(option.value);
if (!current) return null;
return (
<Group gap="sm">
<ImagePreview src={option.value} w={20} h={20} />
<Stack gap={0}>
<Text size="sm">{current.name}</Text>
<Text size="xs" c="dimmed">
{option.value}
</Text>
</Stack>
</Group>
);
}}
data={[
{
group: t("board.field.backgroundImageUrl.group.your"),
items: images
.filter((media) => media.creatorId === session?.user.id)
.map((media) => `/api/user-medias/${media.id}`),
},
{
group: t("board.field.backgroundImageUrl.group.other"),
items: images
.filter((media) => media.creatorId !== session?.user.id)
.map((media) => `/api/user-medias/${media.id}`),
},
]}
{...form.getInputProps("backgroundImageUrl")}
/>
{session?.user.permissions.includes("media-upload") && (
<UploadMedia
onSuccess={({ url }) =>
startTransition(() => {
form.setFieldValue("backgroundImageUrl", url);
})
}
>
{({ onClick, loading }) => (
<ActionIcon onClick={onClick} loading={loading} mt={24} size={36} variant="default">
<IconUpload size={16} stroke={1.5} />
</ActionIcon>
)}
</UploadMedia>
)}
</Group>
</Grid.Col> </Grid.Col>
<Grid.Col span={12}> <Grid.Col span={12}>
<SelectWithDescriptionBadge <SelectWithDescriptionBadge
@@ -76,7 +159,7 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
</Grid> </Grid>
<Group justify="end"> <Group justify="end">
<Button type="submit" loading={isPending} color="teal"> <Button type="submit" loading={isPending}>
{t("common.action.saveChanges")} {t("common.action.saveChanges")}
</Button> </Button>
</Group> </Group>
@@ -85,6 +168,21 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
); );
}; };
interface ImagePreviewProps {
src: string;
w: string | number;
h?: string | number;
}
const ImagePreview = ({ src, w, h }: ImagePreviewProps) => {
if (!["/", "http://", "https://"].some((prefix) => src.startsWith(prefix))) {
return <IconPhotoOff size={w} />;
}
// eslint-disable-next-line @next/next/no-img-element
return <img src={src} alt="preview image" style={{ width: w, height: h, objectFit: "contain" }} />;
};
type BackgroundImageKey = "backgroundImageAttachment" | "backgroundImageSize" | "backgroundImageRepeat"; type BackgroundImageKey = "backgroundImageAttachment" | "backgroundImageSize" | "backgroundImageRepeat";
type inferOptions<TKey extends BackgroundImageKey> = TranslationObject["board"]["field"][TKey]["option"]; type inferOptions<TKey extends BackgroundImageKey> = TranslationObject["board"]["field"][TKey]["option"];
@@ -38,7 +38,7 @@ export const BehaviorSettingsContent = ({ board }: Props) => {
/> />
<Group justify="end"> <Group justify="end">
<Button type="submit" loading={isPending} color="teal"> <Button type="submit" loading={isPending}>
{t("common.action.saveChanges")} {t("common.action.saveChanges")}
</Button> </Button>
</Group> </Group>
@@ -46,7 +46,7 @@ export const CustomCssSettingsContent = ({ board }: Props) => {
</Alert> </Alert>
<Group justify="end"> <Group justify="end">
<Button type="submit" loading={isPending} color="teal"> <Button type="submit" loading={isPending}>
{t("common.action.saveChanges")} {t("common.action.saveChanges")}
</Button> </Button>
</Group> </Group>
@@ -1,14 +1,14 @@
"use client"; "use client";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { Button, Grid, Group, Loader, Stack, TextInput, Tooltip } from "@mantine/core"; import { Button, Grid, Group, Loader, Stack, TextInput } from "@mantine/core";
import { useDebouncedValue, useDocumentTitle, useFavicon } from "@mantine/hooks"; import { useDebouncedValue, useDocumentTitle, useFavicon } from "@mantine/hooks";
import { IconAlertTriangle } from "@tabler/icons-react";
import { useUpdateBoard } from "@homarr/boards/updater"; import { useUpdateBoard } from "@homarr/boards/updater";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { IconPicker } from "@homarr/forms-collection";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
import { createMetaTitle } from "~/metadata"; import { createMetaTitle } from "~/metadata";
import type { Board } from "../../_types"; import type { Board } from "../../_types";
@@ -28,7 +28,7 @@ export const GeneralSettingsContent = ({ board }: Props) => {
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board); const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
const form = useZodForm( const form = useZodForm(
validation.board.savePartialSettings boardSavePartialSettingsSchema
.pick({ .pick({
pageTitle: true, pageTitle: true,
logoImageUrl: true, logoImageUrl: true,
@@ -52,9 +52,9 @@ export const GeneralSettingsContent = ({ board }: Props) => {
}, },
); );
useLogoPreview(form.values.logoImageUrl);
useFaviconPreview(form.values.faviconImageUrl);
const metaTitleStatus = useMetaTitlePreview(form.values.metaTitle); const metaTitleStatus = useMetaTitlePreview(form.values.metaTitle);
const faviconStatus = useFaviconPreview(form.values.faviconImageUrl);
const logoStatus = useLogoPreview(form.values.logoImageUrl);
// Cleanup for not applied changes of the page title and logo image URL // Cleanup for not applied changes of the page title and logo image URL
useEffect(() => { useEffect(() => {
@@ -94,29 +94,29 @@ export const GeneralSettingsContent = ({ board }: Props) => {
<TextInput <TextInput
label={t("board.field.metaTitle.label")} label={t("board.field.metaTitle.label")}
placeholder={createMetaTitle(t("board.content.metaTitle", { boardName: board.name }))} placeholder={createMetaTitle(t("board.content.metaTitle", { boardName: board.name }))}
rightSection={<PendingOrInvalidIndicator {...metaTitleStatus} />} rightSection={metaTitleStatus.isPending && <Loader size="xs" />}
{...form.getInputProps("metaTitle")} {...form.getInputProps("metaTitle")}
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={{ xs: 12, md: 6 }}> <Grid.Col span={{ xs: 12, md: 6 }}>
<TextInput <IconPicker
{...form.getInputProps("logoImageUrl")}
label={t("board.field.logoImageUrl.label")} label={t("board.field.logoImageUrl.label")}
placeholder="/logo/logo.png" placeholder="/logo/logo.png"
rightSection={<PendingOrInvalidIndicator {...logoStatus} />} withAsterisk={false}
{...form.getInputProps("logoImageUrl")}
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={{ xs: 12, md: 6 }}> <Grid.Col span={{ xs: 12, md: 6 }}>
<TextInput <IconPicker
{...form.getInputProps("faviconImageUrl")}
label={t("board.field.faviconImageUrl.label")} label={t("board.field.faviconImageUrl.label")}
placeholder="/logo/logo.png" placeholder="/logo/logo.png"
rightSection={<PendingOrInvalidIndicator {...faviconStatus} />} withAsterisk={false}
{...form.getInputProps("faviconImageUrl")}
/> />
</Grid.Col> </Grid.Col>
</Grid> </Grid>
<Group justify="end"> <Group justify="end">
<Button type="submit" loading={isPending} color="teal"> <Button type="submit" loading={isPending}>
{t("common.action.saveChanges")} {t("common.action.saveChanges")}
</Button> </Button>
</Group> </Group>
@@ -125,40 +125,16 @@ export const GeneralSettingsContent = ({ board }: Props) => {
); );
}; };
const PendingOrInvalidIndicator = ({ isPending, isInvalid }: { isPending: boolean; isInvalid?: boolean }) => {
const t = useI18n();
if (isInvalid) {
return (
<Tooltip multiline w={220} label={t("board.setting.section.general.unrecognizedLink")}>
<IconAlertTriangle size="1rem" color="red" />
</Tooltip>
);
}
if (isPending) {
return <Loader size="xs" />;
}
return null;
};
const useLogoPreview = (url: string | null) => { const useLogoPreview = (url: string | null) => {
const { updateBoard } = useUpdateBoard(); const { updateBoard } = useUpdateBoard();
const [logoDebounced] = useDebouncedValue(url ?? "", 500); const [logoDebounced] = useDebouncedValue(url ?? "", 500);
useEffect(() => { useEffect(() => {
if (!logoDebounced.includes(".") && logoDebounced.length >= 1) return;
updateBoard((previous) => ({ updateBoard((previous) => ({
...previous, ...previous,
logoImageUrl: logoDebounced.length >= 1 ? logoDebounced : null, logoImageUrl: logoDebounced.length >= 1 ? logoDebounced : null,
})); }));
}, [logoDebounced, updateBoard]); }, [logoDebounced, updateBoard]);
return {
isPending: (url ?? "") !== logoDebounced,
isInvalid: logoDebounced.length >= 1 && !logoDebounced.includes("."),
};
}; };
const useMetaTitlePreview = (title: string | null) => { const useMetaTitlePreview = (title: string | null) => {
@@ -170,16 +146,7 @@ const useMetaTitlePreview = (title: string | null) => {
}; };
}; };
const validFaviconExtensions = ["ico", "png", "svg", "gif"];
const isValidUrl = (url: string) =>
url.includes("/") && validFaviconExtensions.some((extension) => url.endsWith(`.${extension}`));
const useFaviconPreview = (url: string | null) => { const useFaviconPreview = (url: string | null) => {
const [faviconDebounced] = useDebouncedValue(url ?? "", 500); const [faviconDebounced] = useDebouncedValue(url ?? "", 500);
useFavicon(isValidUrl(faviconDebounced) ? faviconDebounced : ""); useFavicon(faviconDebounced);
return {
isPending: (url ?? "") !== faviconDebounced,
isInvalid: faviconDebounced.length >= 1 && !isValidUrl(faviconDebounced),
};
}; };
@@ -6,7 +6,7 @@ import { clientApi } from "@homarr/api/client";
import { createId } from "@homarr/db/client"; import { createId } from "@homarr/db/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { boardSaveLayoutsSchema } from "@homarr/validation/board";
import type { Board } from "../../_types"; import type { Board } from "../../_types";
@@ -22,7 +22,7 @@ export const LayoutSettingsContent = ({ board }: Props) => {
void utils.board.getHomeBoard.invalidate(); void utils.board.getHomeBoard.invalidate();
}, },
}); });
const form = useZodForm(validation.board.saveLayouts.omit({ id: true }).required(), { const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), {
initialValues: { initialValues: {
layouts: board.layouts, layouts: board.layouts,
}, },
@@ -105,7 +105,7 @@ export const LayoutSettingsContent = ({ board }: Props) => {
</Stack> </Stack>
<Group justify="end"> <Group justify="end">
<Button type="submit" loading={isPending} color="teal"> <Button type="submit" loading={isPending}>
{t("common.action.saveChanges")} {t("common.action.saveChanges")}
</Button> </Button>
</Group> </Group>
@@ -8,18 +8,18 @@ import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { groupCreateSchema } from "@homarr/validation/group";
export const InitGroup = () => { export const InitGroup = () => {
const t = useI18n(); const t = useI18n();
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation(); const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation();
const form = useZodForm(validation.group.create, { const form = useZodForm(groupCreateSchema, {
initialValues: { initialValues: {
name: "", name: "",
}, },
}); });
const handleSubmitAsync = async (values: z.infer<typeof validation.group.create>) => { const handleSubmitAsync = async (values: z.infer<typeof groupCreateSchema>) => {
await mutateAsync(values, { await mutateAsync(values, {
async onSuccess() { async onSuccess() {
await revalidatePathActionAsync("/init"); await revalidatePathActionAsync("/init");
@@ -12,13 +12,13 @@ import type { CheckboxProps } from "@homarr/form/types";
import { defaultServerSettings } from "@homarr/server-settings"; import { defaultServerSettings } from "@homarr/server-settings";
import type { TranslationObject } from "@homarr/translation"; import type { TranslationObject } from "@homarr/translation";
import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { settingsInitSchema } from "@homarr/validation/settings";
export const InitSettings = () => { export const InitSettings = () => {
const tSection = useScopedI18n("management.page.settings.section"); const tSection = useScopedI18n("management.page.settings.section");
const t = useI18n(); const t = useI18n();
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation(); const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation();
const form = useZodForm(validation.settings.init, { initialValues: defaultServerSettings }); const form = useZodForm(settingsInitSchema, { initialValues: defaultServerSettings });
form.watch("analytics.enableGeneral", ({ value }) => { form.watch("analytics.enableGeneral", ({ value }) => {
if (!value) { if (!value) {
@@ -30,7 +30,7 @@ export const InitSettings = () => {
} }
}); });
const handleSubmitAsync = async (values: z.infer<typeof validation.settings.init>) => { const handleSubmitAsync = async (values: z.infer<typeof settingsInitSchema>) => {
await mutateAsync(values, { await mutateAsync(values, {
async onSuccess() { async onSuccess() {
await revalidatePathActionAsync("/init"); await revalidatePathActionAsync("/init");
@@ -10,13 +10,13 @@ import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui"; import { CustomPasswordInput } from "@homarr/ui";
import { validation } from "@homarr/validation"; import { userInitSchema } from "@homarr/validation/user";
export const InitUserForm = () => { export const InitUserForm = () => {
const t = useScopedI18n("user"); const t = useScopedI18n("user");
const tUser = useScopedI18n("init.step.user"); const tUser = useScopedI18n("init.step.user");
const { mutateAsync, isPending } = clientApi.user.initUser.useMutation(); const { mutateAsync, isPending } = clientApi.user.initUser.useMutation();
const form = useZodForm(validation.user.init, { const form = useZodForm(userInitSchema, {
initialValues: { initialValues: {
username: "", username: "",
password: "", password: "",
@@ -74,4 +74,4 @@ export const InitUserForm = () => {
); );
}; };
type FormType = z.infer<typeof validation.user.init>; type FormType = z.infer<typeof userInitSchema>;
+10 -1
View File
@@ -7,6 +7,7 @@ import "@homarr/ui/styles.css";
import "~/styles/scroll-area.scss"; import "~/styles/scroll-area.scss";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import type { DayOfWeek } from "@mantine/dates";
import { NextIntlClientProvider } from "next-intl"; import { NextIntlClientProvider } from "next-intl";
import { api } from "@homarr/api/server"; import { api } from "@homarr/api/server";
@@ -87,7 +88,15 @@ export default async function Layout(props: {
}, },
(innerProps) => ( (innerProps) => (
<SettingsProvider <SettingsProvider
user={user} user={
user
? {
...user,
// Convert type, because output schema is not smart enough to infer $type from drizzle
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
}
: null
}
serverSettings={{ serverSettings={{
board: { board: {
homeBoardId: serverSettings.board.homeBoardId, homeBoardId: serverSettings.board.homeBoardId,
@@ -10,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { AppForm } from "@homarr/forms-collection"; import { AppForm } from "@homarr/forms-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { validation } from "@homarr/validation"; import type { appManageSchema } from "@homarr/validation/app";
interface AppEditFormProps { interface AppEditFormProps {
app: RouterOutputs["app"]["byId"]; app: RouterOutputs["app"]["byId"];
@@ -40,7 +40,7 @@ export const AppEditForm = ({ app }: AppEditFormProps) => {
}); });
const handleSubmit = useCallback( const handleSubmit = useCallback(
(values: z.infer<typeof validation.app.manage>) => { (values: z.infer<typeof appManageSchema>) => {
mutate({ mutate({
id: app.id, id: app.id,
...values, ...values,
@@ -0,0 +1,96 @@
import { useMemo } from "react";
import { Accordion, Anchor, Card, Stack, Text } from "@mantine/core";
import { IconSubtask } from "@tabler/icons-react";
import { getMantineColor } from "@homarr/common";
import { useI18n } from "@homarr/translation/client";
import { CertificateErrorDetails } from "./test-connection-certificate";
import type { AnyMappedTestConnectionError, MappedError } from "./types";
interface IntegrationTestConnectionErrorProps {
error: AnyMappedTestConnectionError;
url: string;
}
export const IntegrationTestConnectionError = ({ error, url }: IntegrationTestConnectionErrorProps) => {
const t = useI18n();
const causeArray = useMemo(() => toCauseArray(error.cause), [error.cause]);
return (
<Card withBorder style={{ borderColor: getMantineColor("red", 8) }}>
<Stack>
<Stack gap="sm">
<Text size="lg" fw={500} c="red.8">
{t(`integration.testConnection.error.${error.type}.title`)}
</Text>
{error.type !== "request" && error.type !== "certificate" && error.type !== "statusCode" ? (
<Text size="md">{t(`integration.testConnection.error.${error.type}.description`)}</Text>
) : null}
{error.type === "request" ? (
<Text size="md">
{t(
`integration.testConnection.error.request.description.${error.data.type}.${error.data.reason}` as never,
)}
</Text>
) : null}
{error.type === "statusCode" ? (
error.data.reason === "other" ? (
<Text size="md">
{t.rich("integration.testConnection.error.statusCode.otherDescription", {
statusCode: error.data.statusCode.toString(),
url: () => <Anchor href={error.data.url}>{error.data.url}</Anchor>,
})}
</Text>
) : (
<Text size="md">
{t.rich("integration.testConnection.error.statusCode.description", {
reason: t(`integration.testConnection.error.statusCode.reason.${error.data.reason}`),
statusCode: error.data.statusCode.toString(),
url: () => <Anchor href={error.data.url}>{error.data.url}</Anchor>,
})}
</Text>
)
) : null}
{error.type === "certificate" ? <CertificateErrorDetails error={error} url={url} /> : null}
</Stack>
{error.cause ? (
<Accordion variant="contained">
<Accordion.Item value="cause">
<Accordion.Control icon={<IconSubtask size={16} stroke={1.5} />}>
{t("integration.testConnection.error.common.cause.title")}
</Accordion.Control>
<Accordion.Panel>
<pre style={{ whiteSpace: "pre-wrap" }}>
{error.name}: {error.message}
{"\n"}
{causeArray
.map(
(cause) =>
`caused by ${cause.name}${cause.message ? `: ${cause.message}` : ""} ${cause.metadata.map((item) => `${item.key}=${item.value}`).join(" ")}`,
)
.join("\n")}
</pre>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
) : null}
</Stack>
</Card>
);
};
const toCauseArray = (cause: MappedError | undefined) => {
const causeArray: MappedError[] = [];
let currentCause: MappedError | undefined = cause;
while (currentCause) {
causeArray.push(currentCause);
currentCause = currentCause.cause;
}
return causeArray.map(({ cause: _innerCause, ...cause }) => cause);
};
@@ -0,0 +1,327 @@
import { useState } from "react";
import { ActionIcon, Alert, Anchor, Button, Card, CopyButton, Group, SimpleGrid, Stack, Text } from "@mantine/core";
import { IconAlertTriangle, IconCheck, IconCopy, IconExclamationCircle, IconRepeat } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { getMantineColor } from "@homarr/common";
import { createId } from "@homarr/db/client";
import { createDocumentationLink } from "@homarr/definitions";
import { createModal, useConfirmModal, useModalAction } from "@homarr/modals";
import { AddCertificateModal } from "@homarr/modals-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useCurrentLocale, useI18n, useScopedI18n } from "@homarr/translation/client";
import type { MappedCertificate, MappedTestConnectionCertificateError } from "./types";
interface CertificateErrorDetailsProps {
error: MappedTestConnectionCertificateError;
url: string;
}
export const CertificateErrorDetails = ({ error, url }: CertificateErrorDetailsProps) => {
const tError = useScopedI18n("integration.testConnection.error");
const { data: session } = useSession();
const isAdmin = session?.user.permissions.includes("admin") ?? false;
const [showRetryButton, setShowRetryButton] = useState(false);
const { openModal: openUploadModal } = useModalAction(AddCertificateModal);
const { openConfirmModal } = useConfirmModal();
const { mutateAsync: trustHostnameAsync } = clientApi.certificates.trustHostnameMismatch.useMutation();
const { mutateAsync: addCertificateAsync } = clientApi.certificates.addCertificate.useMutation();
const handleTrustHostname = () => {
const { hostname } = new URL(url);
openConfirmModal({
title: tError("certificate.hostnameMismatch.confirm.title"),
children: tError("certificate.hostnameMismatch.confirm.message"),
// eslint-disable-next-line no-restricted-syntax
async onConfirm() {
await trustHostnameAsync(
{
hostname,
certificate: error.data.certificate.pem,
},
{
onSuccess() {
showSuccessNotification({
title: tError("certificate.hostnameMismatch.notification.success.title"),
message: tError("certificate.hostnameMismatch.notification.success.message"),
});
setShowRetryButton(true);
},
onError() {
showErrorNotification({
title: tError("certificate.hostnameMismatch.notification.error.title"),
message: tError("certificate.hostnameMismatch.notification.error.message"),
});
},
},
);
},
});
};
const handleTrustSelfSigned = () => {
const { hostname } = new URL(url);
openConfirmModal({
title: tError("certificate.selfSigned.confirm.title"),
children: tError("certificate.selfSigned.confirm.message"),
// eslint-disable-next-line no-restricted-syntax
async onConfirm() {
const formData = new FormData();
formData.append(
"file",
new File([error.data.certificate.pem], `${hostname}-${createId()}.crt`, {
type: "application/x-x509-ca-cert",
}),
);
await addCertificateAsync(formData, {
onSuccess() {
showSuccessNotification({
title: tError("certificate.selfSigned.notification.success.title"),
message: tError("certificate.selfSigned.notification.success.message"),
});
setShowRetryButton(true);
},
onError() {
showErrorNotification({
title: tError("certificate.selfSigned.notification.error.title"),
message: tError("certificate.selfSigned.notification.error.message"),
});
},
});
},
});
};
const description = <Text size="md">{tError(`certificate.description.${error.data.reason}`)}</Text>;
if (!isAdmin) {
return (
<>
{description}
<NotEnoughPermissionsAlert />
</>
);
}
return (
<>
{description}
<CertificateDetailsCard certificate={error.data.certificate} />
{error.data.reason === "hostnameMismatch" && <HostnameMismatchAlert />}
{!error.data.certificate.isSelfSigned && error.data.reason === "untrusted" && <CertificateExtractAlert />}
{showRetryButton && (
<Button
variant="default"
fullWidth
leftSection={<IconRepeat size={16} color={getMantineColor("blue", 6)} stroke={1.5} />}
type="submit"
>
{tError("certificate.action.retry.label")}
</Button>
)}
{(error.data.reason === "untrusted" && error.data.certificate.isSelfSigned) ||
error.data.reason === "hostnameMismatch" ? (
<Button
variant="default"
fullWidth
onClick={error.data.reason === "hostnameMismatch" ? handleTrustHostname : handleTrustSelfSigned}
>
{tError("certificate.action.trust.label")}
</Button>
) : null}
{error.data.reason === "untrusted" && !error.data.certificate.isSelfSigned ? (
<Button
variant="default"
fullWidth
onClick={() =>
openUploadModal({
onSuccess() {
setShowRetryButton(true);
},
})
}
>
{tError("certificate.action.upload.label")}
</Button>
) : null}
</>
);
};
const NotEnoughPermissionsAlert = () => {
const t = useI18n();
return (
<Alert
icon={<IconAlertTriangle size={16} />}
title={t("integration.testConnection.error.certificate.alert.permission.title")}
color="yellow"
>
{t("integration.testConnection.error.certificate.alert.permission.message")}
</Alert>
);
};
const HostnameMismatchAlert = () => {
const t = useI18n();
return (
<Alert
icon={<IconAlertTriangle size={16} />}
title={t("integration.testConnection.error.certificate.alert.hostnameMismatch.title")}
color="yellow"
>
{t("integration.testConnection.error.certificate.alert.hostnameMismatch.message")}
</Alert>
);
};
const CertificateExtractAlert = () => {
const t = useI18n();
return (
<Alert
icon={<IconExclamationCircle size={16} />}
title={t("integration.testConnection.error.certificate.alert.extract.title")}
color="red"
>
{t.rich("integration.testConnection.error.certificate.alert.extract.message", {
docsLink: () => (
<Anchor
href={createDocumentationLink("/docs/management/certificates", "#obtaining-certificates")}
target="_blank"
>
{t("common.here")}
</Anchor>
),
})}
</Alert>
);
};
interface CertificateDetailsProps {
certificate: MappedCertificate;
}
export const CertificateDetailsCard = ({ certificate }: CertificateDetailsProps) => {
const { openModal } = useModalAction(PemContentModal);
const locale = useCurrentLocale();
const tDetails = useScopedI18n("integration.testConnection.error.certificate.details");
const tCertificateField = useScopedI18n("certificate.field");
return (
<Card withBorder>
<Text fw={500}>{tDetails("title")}</Text>
<Group justify="space-between">
<Text size="sm" c="dimmed">
{tDetails("description")}
</Text>
<Anchor
size="sm"
ta="start"
component="button"
type="button"
onClick={() => openModal({ content: certificate.pem })}
>
{tDetails("content.action")}
</Anchor>
</Group>
<SimpleGrid cols={{ base: 1, md: 2 }} mt="md">
<Stack gap={0}>
<Text size="xs" c="dimmed">
{tCertificateField("subject.label")}
</Text>
<Text size="sm">{certificate.subject}</Text>
</Stack>
<Stack gap={0}>
<Text size="xs" c="dimmed">
{tCertificateField("issuer.label")}
</Text>
<Text size="sm">{certificate.issuer}</Text>
</Stack>
<Stack gap={0}>
<Text size="xs" c="dimmed">
{tCertificateField("validFrom.label")}
</Text>
<Text size="sm">
{new Intl.DateTimeFormat(locale, {
dateStyle: "full",
timeStyle: "long",
}).format(certificate.validFrom)}
</Text>
</Stack>
<Stack gap={0}>
<Text size="xs" c="dimmed">
{tCertificateField("validTo.label")}
</Text>
<Text size="sm">
{new Intl.DateTimeFormat(locale, {
dateStyle: "full",
timeStyle: "long",
}).format(certificate.validTo)}
</Text>
</Stack>
<Stack gap={0}>
<Text size="xs" c="dimmed">
{tCertificateField("serialNumber.label")}
</Text>
<Text size="sm">{certificate.serialNumber}</Text>
</Stack>
</SimpleGrid>
<SimpleGrid cols={1} mt="md">
<Stack gap={0}>
<Text size="xs" c="dimmed">
{tCertificateField("fingerprint.label")}
</Text>
<Text size="sm">{certificate.fingerprint}</Text>
</Stack>
</SimpleGrid>
</Card>
);
};
const PemContentModal = createModal<{ content: string }>(({ actions, innerProps }) => {
const t = useI18n();
return (
<Stack>
<Card w="100%" pos="relative" bg="dark.6" fz="xs" p="sm">
<pre
style={{
whiteSpace: "pre-wrap",
wordWrap: "break-word",
}}
>
{innerProps.content}
</pre>
<CopyButton value={innerProps.content}>
{({ copy, copied }) => (
<ActionIcon onClick={copy} pos="absolute" top={8} right={8} variant="default">
{copied ? (
<IconCheck size={16} stroke={1.5} color={getMantineColor("green", 6)} />
) : (
<IconCopy size={16} stroke={1.5} />
)}
</ActionIcon>
)}
</CopyButton>
</Card>
<Button variant="light" color="gray" onClick={actions.closeModal}>
{t("common.action.close")}
</Button>
</Stack>
);
}).withOptions({
defaultTitle(t) {
return t("integration.testConnection.error.certificate.details.content.title");
},
size: "lg",
});
@@ -0,0 +1,6 @@
import type { RouterOutputs } from "@homarr/api";
export type AnyMappedTestConnectionError = Exclude<RouterOutputs["integration"]["create"], undefined>["error"];
export type MappedTestConnectionCertificateError = Extract<AnyMappedTestConnectionError, { type: "certificate" }>;
export type MappedCertificate = MappedTestConnectionCertificateError["data"]["certificate"];
export type MappedError = Exclude<AnyMappedTestConnectionError["cause"], undefined>;
@@ -1,5 +1,6 @@
"use client"; "use client";
import { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Button, Fieldset, Group, Stack, TextInput } from "@mantine/core"; import { Button, Fieldset, Group, Stack, TextInput } from "@mantine/core";
@@ -10,14 +11,15 @@ import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";
import { getAllSecretKindOptions, getDefaultSecretKinds } from "@homarr/definitions"; import { getAllSecretKindOptions, getDefaultSecretKinds } from "@homarr/definitions";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
import { useConfirmModal } from "@homarr/modals"; import { useConfirmModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { integrationUpdateSchema } from "@homarr/validation/integration";
import { SecretCard } from "../../_components/secrets/integration-secret-card"; import { SecretCard } from "../../_components/secrets/integration-secret-card";
import { IntegrationSecretInput } from "../../_components/secrets/integration-secret-inputs"; import { IntegrationSecretInput } from "../../_components/secrets/integration-secret-inputs";
import { IntegrationTestConnectionError } from "../../_components/test-connection/integration-test-connection-error";
import type { AnyMappedTestConnectionError } from "../../_components/test-connection/types";
interface EditIntegrationForm { interface EditIntegrationForm {
integration: RouterOutputs["integration"]["byId"]; integration: RouterOutputs["integration"]["byId"];
@@ -32,7 +34,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
) ?? getDefaultSecretKinds(integration.kind); ) ?? getDefaultSecretKinds(integration.kind);
const router = useRouter(); const router = useRouter();
const form = useZodForm(validation.integration.update.omit({ id: true }), { const form = useZodForm(integrationUpdateSchema.omit({ id: true }), {
initialValues: { initialValues: {
name: integration.name, name: integration.name,
url: integration.url, url: integration.url,
@@ -43,6 +45,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
}, },
}); });
const { mutateAsync, isPending } = clientApi.integration.update.useMutation(); const { mutateAsync, isPending } = clientApi.integration.update.useMutation();
const [error, setError] = useState<null | AnyMappedTestConnectionError>(null);
const secretsMap = new Map(integration.secrets.map((secret) => [secret.kind, secret])); const secretsMap = new Map(integration.secrets.map((secret) => [secret.kind, secret]));
@@ -57,26 +60,24 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
})), })),
}, },
{ {
onSuccess: () => { onSuccess: (data) => {
// We do it this way as we are unable to send a typesafe error through onError
if (data?.error) {
setError(data.error);
showErrorNotification({
title: t("integration.page.edit.notification.error.title"),
message: t("integration.page.edit.notification.error.message"),
});
return;
}
showSuccessNotification({ showSuccessNotification({
title: t("integration.page.edit.notification.success.title"), title: t("integration.page.edit.notification.success.title"),
message: t("integration.page.edit.notification.success.message"), message: t("integration.page.edit.notification.success.message"),
}); });
void revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations")); void revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
}, },
onError: (error) => { onError: () => {
const testConnectionError = convertIntegrationTestConnectionError(error.data?.error);
if (testConnectionError) {
showErrorNotification({
title: t(`integration.testConnection.notification.${testConnectionError.key}.title`),
message:
testConnectionError.message ??
t(`integration.testConnection.notification.${testConnectionError.key}.message`),
});
return;
}
showErrorNotification({ showErrorNotification({
title: t("integration.page.edit.notification.error.title"), title: t("integration.page.edit.notification.error.title"),
message: t("integration.page.edit.notification.error.message"), message: t("integration.page.edit.notification.error.message"),
@@ -128,6 +129,8 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
</Stack> </Stack>
</Fieldset> </Fieldset>
{error !== null && <IntegrationTestConnectionError error={error} url={form.values.url} />}
<Group justify="end" align="center"> <Group justify="end" align="center">
<Button variant="default" component={Link} href="/manage/integrations"> <Button variant="default" component={Link} href="/manage/integrations">
{t("common.action.backToOverview")} {t("common.action.backToOverview")}
@@ -141,4 +144,4 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
); );
}; };
type FormType = Omit<z.infer<typeof validation.integration.update>, "id">; type FormType = Omit<z.infer<typeof integrationUpdateSchema>, "id">;
@@ -24,23 +24,25 @@ import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions
import { getAllSecretKindOptions, getIconUrl, getIntegrationName, integrationDefs } from "@homarr/definitions"; import { getAllSecretKindOptions, getIconUrl, getIntegrationName, integrationDefs } from "@homarr/definitions";
import type { UseFormReturnType } from "@homarr/form"; import type { UseFormReturnType } from "@homarr/form";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { appHrefSchema } from "@homarr/validation/app";
import { integrationCreateSchema } from "@homarr/validation/integration";
import { IntegrationSecretInput } from "../_components/secrets/integration-secret-inputs"; import { IntegrationSecretInput } from "../_components/secrets/integration-secret-inputs";
import { IntegrationTestConnectionError } from "../_components/test-connection/integration-test-connection-error";
import type { AnyMappedTestConnectionError } from "../_components/test-connection/types";
interface NewIntegrationFormProps { interface NewIntegrationFormProps {
searchParams: Partial<z.infer<typeof validation.integration.create>> & { searchParams: Partial<z.infer<typeof integrationCreateSchema>> & {
kind: IntegrationKind; kind: IntegrationKind;
}; };
} }
const formSchema = validation.integration.create.omit({ kind: true }).and( const formSchema = integrationCreateSchema.omit({ kind: true }).and(
z.object({ z.object({
createApp: z.boolean(), createApp: z.boolean(),
appHref: validation.app.manage.shape.href, appHref: appHrefSchema,
}), }),
); );
@@ -72,6 +74,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
clientApi.integration.create.useMutation(); clientApi.integration.create.useMutation();
const { mutateAsync: createAppAsync, isPending: isPendingApp } = clientApi.app.create.useMutation(); const { mutateAsync: createAppAsync, isPending: isPendingApp } = clientApi.app.create.useMutation();
const isPending = isPendingIntegration || isPendingApp; const isPending = isPendingIntegration || isPendingApp;
const [error, setError] = useState<null | AnyMappedTestConnectionError>(null);
const handleSubmitAsync = async (values: FormType) => { const handleSubmitAsync = async (values: FormType) => {
await createIntegrationAsync( await createIntegrationAsync(
@@ -80,7 +83,17 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
...values, ...values,
}, },
{ {
async onSuccess() { async onSuccess(data) {
// We do it this way as we are unable to send a typesafe error through onError
if (data?.error) {
setError(data.error);
showErrorNotification({
title: t("integration.page.create.notification.error.title"),
message: t("integration.page.create.notification.error.message"),
});
return;
}
showSuccessNotification({ showSuccessNotification({
title: t("integration.page.create.notification.success.title"), title: t("integration.page.create.notification.success.title"),
message: t("integration.page.create.notification.success.message"), message: t("integration.page.create.notification.success.message"),
@@ -113,19 +126,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
}, },
); );
}, },
onError: (error) => { onError: () => {
const testConnectionError = convertIntegrationTestConnectionError(error.data?.error);
if (testConnectionError) {
showErrorNotification({
title: t(`integration.testConnection.notification.${testConnectionError.key}.title`),
message:
testConnectionError.message ??
t(`integration.testConnection.notification.${testConnectionError.key}.message`),
});
return;
}
showErrorNotification({ showErrorNotification({
title: t("integration.page.create.notification.error.title"), title: t("integration.page.create.notification.error.title"),
message: t("integration.page.create.notification.error.message"), message: t("integration.page.create.notification.error.message"),
@@ -163,6 +164,8 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
</Stack> </Stack>
</Fieldset> </Fieldset>
{error !== null && <IntegrationTestConnectionError error={error} url={form.values.url} />}
{supportsSearchEngine && ( {supportsSearchEngine && (
<Checkbox <Checkbox
label={t("integration.field.attemptSearchEngineCreation.label")} label={t("integration.field.attemptSearchEngineCreation.label")}
@@ -7,14 +7,14 @@ import type { IntegrationKind } from "@homarr/definitions";
import { getIntegrationName, integrationKinds } from "@homarr/definitions"; import { getIntegrationName, integrationKinds } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server"; import { getScopedI18n } from "@homarr/translation/server";
import { IntegrationAvatar } from "@homarr/ui"; import { IntegrationAvatar } from "@homarr/ui";
import type { validation } from "@homarr/validation"; import type { integrationCreateSchema } from "@homarr/validation/integration";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NewIntegrationForm } from "./_integration-new-form"; import { NewIntegrationForm } from "./_integration-new-form";
interface NewIntegrationPageProps { interface NewIntegrationPageProps {
searchParams: Promise< searchParams: Promise<
Partial<z.infer<typeof validation.integration.create>> & { Partial<z.infer<typeof integrationCreateSchema>> & {
kind: IntegrationKind; kind: IntegrationKind;
} }
>; >;
@@ -12,9 +12,9 @@ import { useZodForm } from "@homarr/form";
import { IconPicker } from "@homarr/forms-collection"; import { IconPicker } from "@homarr/forms-collection";
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 { validation } from "@homarr/validation"; import { searchEngineManageSchema } from "@homarr/validation/search-engine";
type FormType = z.infer<typeof validation.searchEngine.manage>; type FormType = z.infer<typeof searchEngineManageSchema>;
interface SearchEngineFormProps { interface SearchEngineFormProps {
submitButtonTranslation: (t: TranslationFunction) => string; submitButtonTranslation: (t: TranslationFunction) => string;
@@ -30,7 +30,7 @@ export const SearchEngineForm = (props: SearchEngineFormProps) => {
const [integrationData] = clientApi.integration.allThatSupportSearch.useSuspenseQuery(); const [integrationData] = clientApi.integration.allThatSupportSearch.useSuspenseQuery();
const form = useZodForm(validation.searchEngine.manage, { const form = useZodForm(searchEngineManageSchema, {
initialValues: initialValues ?? { initialValues: initialValues ?? {
name: "", name: "",
short: "", short: "",
@@ -10,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { TranslationFunction } from "@homarr/translation"; import type { TranslationFunction } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import type { validation } from "@homarr/validation"; import type { searchEngineManageSchema } from "@homarr/validation/search-engine";
import { SearchEngineForm } from "../../_form"; import { SearchEngineForm } from "../../_form";
@@ -41,7 +41,7 @@ export const SearchEngineEditForm = ({ searchEngine }: SearchEngineEditFormProps
}); });
const handleSubmit = useCallback( const handleSubmit = useCallback(
(values: z.infer<typeof validation.searchEngine.manage>) => { (values: z.infer<typeof searchEngineManageSchema>) => {
mutate({ mutate({
id: searchEngine.id, id: searchEngine.id,
...values, ...values,
@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { TranslationFunction } from "@homarr/translation"; import type { TranslationFunction } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import type { validation } from "@homarr/validation"; import type { searchEngineManageSchema } from "@homarr/validation/search-engine";
import { SearchEngineForm } from "../_form"; import { SearchEngineForm } from "../_form";
@@ -35,7 +35,7 @@ export const SearchEngineNewForm = () => {
}); });
const handleSubmit = useCallback( const handleSubmit = useCallback(
(values: z.infer<typeof validation.searchEngine.manage>) => { (values: z.infer<typeof searchEngineManageSchema>) => {
mutate(values); mutate(values);
}, },
[mutate], [mutate],
@@ -1,82 +1,23 @@
"use client"; "use client";
import { Button, FileInput, Group, Stack } from "@mantine/core"; import { Button } from "@mantine/core";
import { IconCertificate } from "@tabler/icons-react";
import { z } from "zod";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useModalAction } from "@homarr/modals";
import { createModal, useModalAction } from "@homarr/modals"; import { AddCertificateModal } from "@homarr/modals-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { superRefineCertificateFile } from "@homarr/validation";
export const AddCertificateButton = () => { export const AddCertificateButton = () => {
const { openModal } = useModalAction(AddCertificateModal); const { openModal } = useModalAction(AddCertificateModal);
const t = useI18n(); const t = useI18n();
const handleClick = () => { const handleClick = () => {
openModal({}); openModal({
async onSuccess() {
await revalidatePathActionAsync("/manage/tools/certificates");
},
});
}; };
return <Button onClick={handleClick}>{t("certificate.action.create.label")}</Button>; return <Button onClick={handleClick}>{t("certificate.action.create.label")}</Button>;
}; };
const AddCertificateModal = createModal(({ actions }) => {
const t = useI18n();
const form = useZodForm(
z.object({
file: z.instanceof(File).nullable().superRefine(superRefineCertificateFile),
}),
{
initialValues: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
file: null!,
},
},
);
const { mutateAsync } = clientApi.certificates.addCertificate.useMutation();
return (
<form
onSubmit={form.onSubmit(async (values) => {
const formData = new FormData();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
formData.set("file", values.file!);
await mutateAsync(formData, {
async onSuccess() {
showSuccessNotification({
title: t("certificate.action.create.notification.success.title"),
message: t("certificate.action.create.notification.success.message"),
});
await revalidatePathActionAsync("/manage/tools/certificates");
actions.closeModal();
},
onError() {
showErrorNotification({
title: t("certificate.action.create.notification.error.title"),
message: t("certificate.action.create.notification.error.message"),
});
},
});
})}
>
<Stack>
<FileInput leftSection={<IconCertificate size={16} />} {...form.getInputProps("file")} />
<Group justify="end">
<Button onClick={actions.closeModal} variant="subtle" color="gray">
{t("common.action.cancel")}
</Button>
<Button type="submit" loading={form.submitting}>
{t("common.action.add")}
</Button>
</Group>
</Stack>
</form>
);
}).withOptions({
defaultTitle(t) {
return t("certificate.action.create.label");
},
});
@@ -0,0 +1,52 @@
"use client";
import { ActionIcon } from "@mantine/core";
import { IconTrash } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useConfirmModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
interface RemoveHostnameActionIconProps {
hostname: string;
thumbprint: string;
}
export const RemoveHostnameActionIcon = (input: RemoveHostnameActionIconProps) => {
const { mutateAsync } = clientApi.certificates.removeTrustedHostname.useMutation();
const { openConfirmModal } = useConfirmModal();
const t = useI18n();
const handleRemove = () => {
openConfirmModal({
title: t("certificate.action.removeHostname.label"),
children: t("certificate.action.removeHostname.confirm"),
// eslint-disable-next-line no-restricted-syntax
async onConfirm() {
await mutateAsync(input, {
async onSuccess() {
await revalidatePathActionAsync("/manage/tools/certificates/hostnames");
showSuccessNotification({
title: t("certificate.action.removeHostname.notification.success.title"),
message: t("certificate.action.removeHostname.notification.success.message"),
});
},
onError() {
showErrorNotification({
title: t("certificate.action.removeHostname.notification.error.title"),
message: t("certificate.action.removeHostname.notification.error.message"),
});
},
});
},
});
};
return (
<ActionIcon color="red" variant="subtle" onClick={handleRemove}>
<IconTrash color="red" size={16} stroke={1.5} />
</ActionIcon>
);
};
@@ -0,0 +1,99 @@
import { X509Certificate } from "node:crypto";
import Link from "next/link";
import { notFound } from "next/navigation";
import {
Button,
Group,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from "@mantine/core";
import { IconCertificateOff } from "@tabler/icons-react";
import { auth } from "@homarr/auth/next";
import { getTrustedCertificateHostnamesAsync } from "@homarr/certificates/server";
import { getI18n } from "@homarr/translation/server";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NoResults } from "~/components/no-results";
import { RemoveHostnameActionIcon } from "./_components/remove-hostname";
export default async function TrustedHostnamesPage() {
const session = await auth();
if (!session?.user.permissions.includes("admin")) {
notFound();
}
const t = await getI18n();
const trustedHostnames = await getTrustedCertificateHostnamesAsync().then((hostnames) => {
return hostnames.map((hostname) => {
let subject: string | null;
try {
subject = new X509Certificate(hostname.certificate).subject;
} catch {
subject = null;
}
return {
...hostname,
subject,
};
});
});
return (
<>
<DynamicBreadcrumb />
<Stack>
<Group justify="space-between">
<Stack gap={4}>
<Title>{t("certificate.page.hostnames.title")}</Title>
<Text>{t("certificate.page.hostnames.description")}</Text>
</Stack>
<Button variant="default" component={Link} href="/manage/tools/certificates">
{t("certificate.page.hostnames.toCertificates")}
</Button>
</Group>
{trustedHostnames.length === 0 && (
<NoResults icon={IconCertificateOff} title={t("certificate.page.hostnames.noResults.title")} />
)}
{trustedHostnames.length >= 1 && (
<Table>
<TableThead>
<TableTr>
<TableTh>{t("certificate.field.hostname.label")}</TableTh>
<TableTh>{t("certificate.field.subject.label")}</TableTh>
<TableTh>{t("certificate.field.fingerprint.label")}</TableTh>
<TableTh></TableTh>
</TableTr>
</TableThead>
<TableTbody>
{trustedHostnames.map(({ hostname, subject, thumbprint }) => (
<TableTr key={`${hostname}-${thumbprint}`}>
<TableTd>{hostname}</TableTd>
<TableTd>{subject}</TableTd>
<TableTd>{thumbprint}</TableTd>
<TableTd>
<Group justify="end">
<RemoveHostnameActionIcon hostname={hostname} thumbprint={thumbprint} />
</Group>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
)}
</Stack>
</>
);
}
@@ -1,7 +1,8 @@
import { X509Certificate } from "node:crypto"; import { X509Certificate } from "node:crypto";
import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { Card, Group, SimpleGrid, Stack, Text, Title } from "@mantine/core"; import { Button, Card, Group, SimpleGrid, Stack, Text, Title } from "@mantine/core";
import { IconCertificate, IconCertificateOff } from "@tabler/icons-react"; import { IconAlertTriangle, IconCertificate, IconCertificateOff } from "@tabler/icons-react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { auth } from "@homarr/auth/next"; import { auth } from "@homarr/auth/next";
@@ -31,11 +32,27 @@ export default async function CertificatesPage({ params }: CertificatesPageProps
const t = await getI18n(); const t = await getI18n();
const certificates = await loadCustomRootCertificatesAsync(); const certificates = await loadCustomRootCertificatesAsync();
const x509Certificates = certificates const x509Certificates = certificates
.map((cert) => ({ .map((cert) => {
...cert, try {
x509: new X509Certificate(cert.content), const x509 = new X509Certificate(cert.content);
})) return {
.sort((certA, certB) => certA.x509.validToDate.getTime() - certB.x509.validToDate.getTime()); ...cert,
isError: false,
x509,
} as const;
} catch {
return {
...cert,
isError: true,
x509: null,
} as const;
}
})
.sort((certA, certB) => {
if (certA.isError) return -1;
if (certB.isError) return 1;
return certA.x509.validToDate.getTime() - certB.x509.validToDate.getTime();
});
return ( return (
<> <>
@@ -48,7 +65,12 @@ export default async function CertificatesPage({ params }: CertificatesPageProps
<Text>{t("certificate.page.list.description")}</Text> <Text>{t("certificate.page.list.description")}</Text>
</Stack> </Stack>
<AddCertificateButton /> <Group>
<Button variant="default" component={Link} href="/manage/tools/certificates/hostnames">
{t("certificate.page.list.toHostnames")}
</Button>
<AddCertificateButton />
</Group>
</Group> </Group>
{x509Certificates.length === 0 && ( {x509Certificates.length === 0 && (
@@ -57,32 +79,47 @@ export default async function CertificatesPage({ params }: CertificatesPageProps
<SimpleGrid cols={{ sm: 1, lg: 2, xl: 3 }} spacing="lg"> <SimpleGrid cols={{ sm: 1, lg: 2, xl: 3 }} spacing="lg">
{x509Certificates.map((cert) => ( {x509Certificates.map((cert) => (
<Card key={cert.x509.fingerprint} withBorder> <Card key={cert.fileName} withBorder>
<Group wrap="nowrap"> <Group wrap="nowrap">
<IconCertificate {cert.isError ? (
color={getMantineColor(iconColor(cert.x509.validToDate), 6)} <IconAlertTriangle
style={{ minWidth: 32 }} color={getMantineColor("red", 6)}
size={32} style={{ minWidth: 32 }}
stroke={1.5} size={32}
/> stroke={1.5}
/>
) : (
<IconCertificate
color={getMantineColor(iconColor(cert.x509.validToDate), 6)}
style={{ minWidth: 32 }}
size={32}
stroke={1.5}
/>
)}
<Stack flex={1} gap="xs" maw="calc(100% - 48px)"> <Stack flex={1} gap="xs" maw="calc(100% - 48px)">
<Group justify="space-between" wrap="nowrap"> <Group justify="space-between" wrap="nowrap">
<Text fw={500} lineClamp={1} style={{ wordBreak: "break-all" }}> <Text fw={500} lineClamp={1} style={{ wordBreak: "break-all" }}>
{cert.x509.subject} {cert.isError ? t("certificate.page.list.invalid.title") : cert.x509.subject}
</Text> </Text>
<Text c="gray.6" ta="end" size="sm"> <Text c="gray.6" ta="end" size="sm">
{cert.fileName} {cert.fileName}
</Text> </Text>
</Group> </Group>
<Group justify="space-between"> <Group justify="space-between">
<Text size="sm" c="gray.6" title={cert.x509.validToDate.toISOString()}> {cert.isError ? (
{t("certificate.page.list.expires", { <Text size="sm" c="gray.6">
when: new Intl.RelativeTimeFormat(locale).format( {t("certificate.page.list.invalid.description")}
dayjs(cert.x509.validToDate).diff(dayjs(), "days"), </Text>
"days", ) : (
), <Text size="sm" c="gray.6" title={cert.x509.validToDate.toISOString()}>
})} {t("certificate.page.list.expires", {
</Text> when: new Intl.RelativeTimeFormat(locale).format(
dayjs(cert.x509.validToDate).diff(dayjs(), "days"),
"days",
),
})}
</Text>
)}
<RemoveCertificate fileName={cert.fileName} /> <RemoveCertificate fileName={cert.fileName} />
</Group> </Group>
</Stack> </Stack>
@@ -17,6 +17,7 @@ import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useTimeAgo } from "@homarr/common"; import { useTimeAgo } from "@homarr/common";
import type { ContainerState } from "@homarr/docker"; import type { ContainerState } from "@homarr/docker";
import { containerStateColorMap } from "@homarr/docker/shared";
import { useModalAction } from "@homarr/modals"; import { useModalAction } from "@homarr/modals";
import { AddDockerAppToHomarr } from "@homarr/modals-collection"; import { AddDockerAppToHomarr } from "@homarr/modals-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
@@ -244,21 +245,11 @@ const ContainerActionBarButton = (props: ContainerActionBarButtonProps) => {
); );
}; };
const containerStates = {
created: "cyan",
running: "green",
paused: "yellow",
restarting: "orange",
exited: "red",
removing: "pink",
dead: "dark",
} satisfies Record<ContainerState, MantineColor>;
const ContainerStateBadge = ({ state }: { state: ContainerState }) => { const ContainerStateBadge = ({ state }: { state: ContainerState }) => {
const t = useScopedI18n("docker.field.state.option"); const t = useScopedI18n("docker.field.state.option");
return ( return (
<Badge size="lg" radius="sm" variant="light" w={120} color={containerStates[state]}> <Badge size="lg" radius="sm" variant="light" w={120} color={containerStateColorMap[state]}>
{t(state)} {t(state)}
</Badge> </Badge>
); );
@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { userChangeHomeBoardsSchema } from "@homarr/validation/user";
import type { Board } from "~/app/[locale]/boards/_types"; import type { Board } from "~/app/[locale]/boards/_types";
import { BoardSelect } from "~/components/board/board-select"; import { BoardSelect } from "~/components/board/board-select";
@@ -40,7 +40,7 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
}); });
}, },
}); });
const form = useZodForm(validation.user.changeHomeBoards, { const form = useZodForm(userChangeHomeBoardsSchema, {
initialValues: { initialValues: {
homeBoardId: user.homeBoardId, homeBoardId: user.homeBoardId,
mobileHomeBoardId: user.mobileHomeBoardId, mobileHomeBoardId: user.mobileHomeBoardId,
@@ -73,7 +73,7 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
/> />
<Group justify="end"> <Group justify="end">
<Button type="submit" color="teal" loading={isPending}> <Button type="submit" loading={isPending}>
{t("common.action.save")} {t("common.action.save")}
</Button> </Button>
</Group> </Group>
@@ -82,4 +82,4 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
); );
}; };
type FormType = z.infer<typeof validation.user.changeHomeBoards>; type FormType = z.infer<typeof userChangeHomeBoardsSchema>;
@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { userChangeSearchPreferencesSchema } from "@homarr/validation/user";
interface ChangeSearchPreferencesFormProps { interface ChangeSearchPreferencesFormProps {
user: RouterOutputs["user"]["getById"]; user: RouterOutputs["user"]["getById"];
@@ -37,7 +37,7 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS
}); });
}, },
}); });
const form = useZodForm(validation.user.changeSearchPreferences, { const form = useZodForm(userChangeSearchPreferencesSchema, {
initialValues: { initialValues: {
defaultSearchEngineId: user.defaultSearchEngineId, defaultSearchEngineId: user.defaultSearchEngineId,
openInNewTab: user.openSearchInNewTab, openInNewTab: user.openSearchInNewTab,
@@ -66,7 +66,7 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS
/> />
<Group justify="end"> <Group justify="end">
<Button type="submit" color="teal" loading={isPending}> <Button type="submit" loading={isPending}>
{t("common.action.save")} {t("common.action.save")}
</Button> </Button>
</Group> </Group>
@@ -75,4 +75,4 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS
); );
}; };
type FormType = z.infer<typeof validation.user.changeSearchPreferences>; type FormType = z.infer<typeof userChangeSearchPreferencesSchema>;
@@ -12,7 +12,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { userFirstDayOfWeekSchema } from "@homarr/validation/user";
dayjs.extend(localeData); dayjs.extend(localeData);
@@ -42,7 +42,7 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
}); });
}, },
}); });
const form = useZodForm(validation.user.firstDayOfWeek, { const form = useZodForm(userFirstDayOfWeekSchema, {
initialValues: { initialValues: {
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek, firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
}, },
@@ -71,7 +71,7 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
</Radio.Group> </Radio.Group>
<Group justify="end"> <Group justify="end">
<Button type="submit" color="teal" loading={isPending}> <Button type="submit" loading={isPending}>
{t("common.action.save")} {t("common.action.save")}
</Button> </Button>
</Group> </Group>
@@ -80,4 +80,4 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
); );
}; };
type FormType = z.infer<typeof validation.user.firstDayOfWeek>; type FormType = z.infer<typeof userFirstDayOfWeekSchema>;
@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { userPingIconsEnabledSchema } from "@homarr/validation/user";
interface PingIconsEnabledProps { interface PingIconsEnabledProps {
user: RouterOutputs["user"]["getById"]; user: RouterOutputs["user"]["getById"];
@@ -35,7 +35,7 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
}); });
}, },
}); });
const form = useZodForm(validation.user.pingIconsEnabled, { const form = useZodForm(userPingIconsEnabledSchema, {
initialValues: { initialValues: {
pingIconsEnabled: user.pingIconsEnabled, pingIconsEnabled: user.pingIconsEnabled,
}, },
@@ -57,7 +57,7 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
/> />
<Group justify="end"> <Group justify="end">
<Button type="submit" color="teal" loading={isPending}> <Button type="submit" loading={isPending}>
{t("common.action.save")} {t("common.action.save")}
</Button> </Button>
</Group> </Group>
@@ -66,4 +66,4 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
); );
}; };
type FormType = z.infer<typeof validation.user.pingIconsEnabled>; type FormType = z.infer<typeof userPingIconsEnabledSchema>;
@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { userEditProfileSchema } from "@homarr/validation/user";
interface UserProfileFormProps { interface UserProfileFormProps {
user: RouterOutputs["user"]["getById"]; user: RouterOutputs["user"]["getById"];
@@ -43,7 +43,7 @@ export const UserProfileForm = ({ user }: UserProfileFormProps) => {
}); });
}, },
}); });
const form = useZodForm(validation.user.editProfile.omit({ id: true }), { const form = useZodForm(userEditProfileSchema.omit({ id: true }), {
initialValues: { initialValues: {
name: user.name ?? "", name: user.name ?? "",
email: user.email ?? "", email: user.email ?? "",
@@ -81,7 +81,7 @@ export const UserProfileForm = ({ user }: UserProfileFormProps) => {
{isProviderCredentials && ( {isProviderCredentials && (
<Group justify="end"> <Group justify="end">
<Button type="submit" color="teal" disabled={!form.isDirty()} loading={isPending}> <Button type="submit" disabled={!form.isDirty()} loading={isPending}>
{t("common.action.saveChanges")} {t("common.action.saveChanges")}
</Button> </Button>
</Group> </Group>
@@ -10,7 +10,7 @@ import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui"; import { CustomPasswordInput } from "@homarr/ui";
import { validation } from "@homarr/validation"; import { userChangePasswordSchema } from "@homarr/validation/user";
interface ChangePasswordFormProps { interface ChangePasswordFormProps {
user: RouterOutputs["user"]["getById"]; user: RouterOutputs["user"]["getById"];
@@ -34,7 +34,7 @@ export const ChangePasswordForm = ({ user }: ChangePasswordFormProps) => {
}); });
}, },
}); });
const form = useZodForm(validation.user.changePassword, { const form = useZodForm(userChangePasswordSchema, {
initialValues: { initialValues: {
/* Require previous password if the current user want's to change his password */ /* Require previous password if the current user want's to change his password */
previousPassword: session?.user.id === user.id ? "" : "_", previousPassword: session?.user.id === user.id ? "" : "_",
@@ -85,7 +85,7 @@ export const ChangePasswordForm = ({ user }: ChangePasswordFormProps) => {
/> />
<Group justify="end"> <Group justify="end">
<Button type="submit" color="teal" loading={isPending}> <Button type="submit" loading={isPending}>
{t("common.action.confirm")} {t("common.action.confirm")}
</Button> </Button>
</Group> </Group>
@@ -27,8 +27,8 @@ import { useModalAction } from "@homarr/modals";
import { showErrorNotification } from "@homarr/notifications"; import { showErrorNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput, UserAvatar } from "@homarr/ui"; import { CustomPasswordInput, UserAvatar } from "@homarr/ui";
import { validation } from "@homarr/validation"; import { createCustomErrorParams } from "@homarr/validation/form/i18n";
import { createCustomErrorParams } from "@homarr/validation/form"; import { userPasswordSchema } from "@homarr/validation/user";
import { GroupSelectModal } from "~/components/access/group-select-modal"; import { GroupSelectModal } from "~/components/access/group-select-modal";
import { StepperNavigationComponent } from "./stepper-navigation"; import { StepperNavigationComponent } from "./stepper-navigation";
@@ -84,7 +84,7 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
const securityForm = useZodForm( const securityForm = useZodForm(
z z
.object({ .object({
password: validation.user.password, password: userPasswordSchema,
confirmPassword: z.string(), confirmPassword: z.string(),
}) })
.refine((data) => data.password === data.confirmPassword, { .refine((data) => data.password === data.confirmPassword, {
@@ -8,7 +8,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { groupUpdateSchema } from "@homarr/validation/group";
interface RenameGroupFormProps { interface RenameGroupFormProps {
group: { group: {
@@ -21,7 +21,7 @@ interface RenameGroupFormProps {
export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => { export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => {
const t = useI18n(); const t = useI18n();
const { mutate, isPending } = clientApi.group.updateGroup.useMutation(); const { mutate, isPending } = clientApi.group.updateGroup.useMutation();
const form = useZodForm(validation.group.update.pick({ name: true }), { const form = useZodForm(groupUpdateSchema.pick({ name: true }), {
initialValues: { initialValues: {
name: group.name, name: group.name,
}, },
@@ -68,7 +68,7 @@ export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => {
{!disabled && ( {!disabled && (
<Group justify="end"> <Group justify="end">
<Button type="submit" color="teal" loading={isPending}> <Button type="submit" loading={isPending}>
{t("common.action.saveChanges")} {t("common.action.saveChanges")}
</Button> </Button>
</Group> </Group>
@@ -40,9 +40,5 @@ export const AddGroupMember = ({ groupId, presentUserIds }: AddGroupMemberProps)
); );
}, [openModal, presentUserIds, groupId, mutateAsync, tMembersAdd]); }, [openModal, presentUserIds, groupId, mutateAsync, tMembersAdd]);
return ( return <MobileAffixButton onClick={handleAddMember}>{tMembersAdd("label")}</MobileAffixButton>;
<MobileAffixButton color="teal" onClick={handleAddMember}>
{tMembersAdd("label")}
</MobileAffixButton>
);
}; };
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useCallback } from "react";
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import { useCallback } from "react";
import { Button, Card, Group, Switch, Text, Transition } from "@mantine/core"; import { Button, Card, Group, Switch, Text, Transition } from "@mantine/core";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
@@ -108,7 +108,7 @@ export const SaveAffix = ({ groupId }: SaveAffixProps) => {
<Button disabled={isPending} onClick={form.reset}> <Button disabled={isPending} onClick={form.reset}>
{t("common.action.discard")} {t("common.action.discard")}
</Button> </Button>
<Button color="teal" loading={isPending} onClick={handleSubmit}> <Button loading={isPending} onClick={handleSubmit}>
{t("common.action.saveChanges")} {t("common.action.saveChanges")}
</Button> </Button>
</Group> </Group>
@@ -6,7 +6,7 @@ import { clientApi } from "@homarr/api/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { groupSettingsSchema } from "@homarr/validation/group";
import { BoardSelect } from "~/components/board/board-select"; import { BoardSelect } from "~/components/board/board-select";
@@ -19,7 +19,7 @@ interface GroupHomeBoardsProps {
export const GroupHomeBoards = ({ homeBoardId, mobileHomeBoardId, groupId }: GroupHomeBoardsProps) => { export const GroupHomeBoards = ({ homeBoardId, mobileHomeBoardId, groupId }: GroupHomeBoardsProps) => {
const t = useI18n(); const t = useI18n();
const [availableBoards] = clientApi.board.getBoardsForGroup.useSuspenseQuery({ groupId }); const [availableBoards] = clientApi.board.getBoardsForGroup.useSuspenseQuery({ groupId });
const form = useZodForm(validation.group.settings.pick({ homeBoardId: true, mobileHomeBoardId: true }), { const form = useZodForm(groupSettingsSchema.pick({ homeBoardId: true, mobileHomeBoardId: true }), {
initialValues: { initialValues: {
homeBoardId, homeBoardId,
mobileHomeBoardId, mobileHomeBoardId,
@@ -71,7 +71,7 @@ export const GroupHomeBoards = ({ homeBoardId, mobileHomeBoardId, groupId }: Gro
/> />
<Group justify="end"> <Group justify="end">
<Button type="submit" color="teal" loading={isPending}> <Button type="submit" loading={isPending}>
{t("common.action.save")} {t("common.action.save")}
</Button> </Button>
</Group> </Group>
@@ -57,9 +57,5 @@ const AddGroup = () => {
openModal(); openModal();
}, [openModal]); }, [openModal]);
return ( return <MobileAffixButton onClick={handleAddGroup}>{t("group.action.create.label")}</MobileAffixButton>;
<MobileAffixButton onClick={handleAddGroup} color="teal">
{t("group.action.create.label")}
</MobileAffixButton>
);
}; };
@@ -264,7 +264,7 @@ const SaveAffix = ({ visible, isPending, onDiscard, onSave }: SaveAffixProps) =>
<Button disabled={isPending} onClick={onDiscard}> <Button disabled={isPending} onClick={onDiscard}>
{t("common.action.discard")} {t("common.action.discard")}
</Button> </Button>
<Button color="teal" loading={isPending} onClick={onSave}> <Button loading={isPending} onClick={onSave}>
{t("common.action.saveChanges")} {t("common.action.saveChanges")}
</Button> </Button>
</Group> </Group>
@@ -89,11 +89,7 @@ const RenderTopToolbarCustomActions = () => {
openModal(); openModal();
}, [openModal]); }, [openModal]);
return ( return <Button onClick={handleNewInvite}>{t("action.new.title")}</Button>;
<Button color="teal" onClick={handleNewInvite}>
{t("action.new.title")}
</Button>
);
}; };
const RenderRowActions = ({ row }: { row: MRT_Row<RouterOutputs["invite"]["getAll"][number]> }) => { const RenderRowActions = ({ row }: { row: MRT_Row<RouterOutputs["invite"]["getAll"][number]> }) => {
@@ -11,7 +11,7 @@ import { useModalAction } from "@homarr/modals";
import { showSuccessNotification } from "@homarr/notifications"; import { showSuccessNotification } from "@homarr/notifications";
import { useSettings } from "@homarr/settings"; import { useSettings } from "@homarr/settings";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import type { BoardItemAdvancedOptions } from "@homarr/validation"; import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets"; import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
import { WidgetError } from "@homarr/widgets/errors"; import { WidgetError } from "@homarr/widgets/errors";
import { WidgetEditModal } from "@homarr/widgets/modals"; import { WidgetEditModal } from "@homarr/widgets/modals";
@@ -48,6 +48,7 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
options: reduceWidgetOptionsWithDefaultValues(kind, settings, {}), options: reduceWidgetOptionsWithDefaultValues(kind, settings, {}),
integrationIds: [], integrationIds: [],
advancedOptions: { advancedOptions: {
title: null,
customCssClasses: [], customCssClasses: [],
borderColor: "", borderColor: "",
}, },
@@ -82,7 +82,7 @@ export const GroupAccessForm = <TPermission extends string>({
<Button rightSection={<IconPlus size="1rem" />} variant="light" onClick={handleAddUser}> <Button rightSection={<IconPlus size="1rem" />} variant="light" onClick={handleAddUser}>
{t("common.action.add")} {t("common.action.add")}
</Button> </Button>
<Button type="submit" loading={isPending} color="teal"> <Button type="submit" loading={isPending}>
{t("permission.action.saveGroup")} {t("permission.action.saveGroup")}
</Button> </Button>
</Group> </Group>
@@ -104,7 +104,7 @@ export const UsersAccessForm = <TPermission extends string>({
<Button rightSection={<IconPlus size="1rem" />} variant="light" onClick={handleAddUser}> <Button rightSection={<IconPlus size="1rem" />} variant="light" onClick={handleAddUser}>
{t("common.action.add")} {t("common.action.add")}
</Button> </Button>
<Button type="submit" loading={isPending} color="teal"> <Button type="submit" loading={isPending}>
{t("permission.action.saveUser")} {t("permission.action.saveUser")}
</Button> </Button>
</Group> </Group>
@@ -9,10 +9,11 @@ import { getSectionElements } from "./section-elements";
export interface CreateItemInput { export interface CreateItemInput {
kind: WidgetKind; kind: WidgetKind;
options?: Record<string, unknown>;
} }
export const createItemCallback = export const createItemCallback =
({ kind }: CreateItemInput) => ({ kind, options = {} }: CreateItemInput) =>
(previous: Board): Board => { (previous: Board): Board => {
const firstSection = previous.sections const firstSection = previous.sections
.filter((section): section is EmptySection => section.kind === "empty") .filter((section): section is EmptySection => section.kind === "empty")
@@ -24,10 +25,11 @@ export const createItemCallback =
const widget = { const widget = {
id: createId(), id: createId(),
kind, kind,
options: {}, options,
layouts: createItemLayouts(previous, firstSection), layouts: createItemLayouts(previous, firstSection),
integrationIds: [], integrationIds: [],
advancedOptions: { advancedOptions: {
title: null,
customCssClasses: [], customCssClasses: [],
borderColor: "", borderColor: "",
}, },
@@ -20,7 +20,7 @@ describe("item actions duplicate-item", () => {
kind: itemKind, kind: itemKind,
integrationIds: ["1"], integrationIds: ["1"],
options: { address: "localhost" }, options: { address: "localhost" },
advancedOptions: { customCssClasses: ["test"], borderColor: "#ff0000" }, advancedOptions: { title: "The best one", customCssClasses: ["test"], borderColor: "#ff0000" },
}) })
.addLayout({ layoutId, sectionId: currentSectionId, ...currentItemSize }) .addLayout({ layoutId, sectionId: currentSectionId, ...currentItemSize })
.build(); .build();
@@ -13,6 +13,7 @@ export class ItemMockBuilder {
layouts: [], layouts: [],
integrationIds: [], integrationIds: [],
advancedOptions: { advancedOptions: {
title: null,
customCssClasses: [], customCssClasses: [],
borderColor: "", borderColor: "",
}, },
@@ -1,7 +1,7 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useUpdateBoard } from "@homarr/boards/updater"; import { useUpdateBoard } from "@homarr/boards/updater";
import type { BoardItemAdvancedOptions } from "@homarr/validation"; import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
import type { CreateItemInput } from "./actions/create-item"; import type { CreateItemInput } from "./actions/create-item";
import { createItemCallback } from "./actions/create-item"; import { createItemCallback } from "./actions/create-item";
@@ -0,0 +1,12 @@
.badge {
@mixin dark {
--background-color: rgb(from var(--mantine-color-dark-6) r g b / var(--opacity));
--border-color: rgb(from var(--mantine-color-dark-4) r g b / var(--opacity));
}
@mixin light {
--background-color: rgb(from var(--mantine-color-white) r g b / var(--opacity));
--border-color: rgb(from var(--mantine-color-gray-3) r g b / var(--opacity));
}
background-color: var(--background-color) !important;
border-color: var(--border-color) !important;
}
@@ -1,12 +1,10 @@
import { Card } from "@mantine/core"; import { Badge, Card } from "@mantine/core";
import { useElementSize } from "@mantine/hooks"; import { useElementSize } from "@mantine/hooks";
import { QueryErrorResetBoundary } from "@tanstack/react-query"; import { QueryErrorResetBoundary } from "@tanstack/react-query";
import combineClasses from "clsx"; import combineClasses from "clsx";
import { NoIntegrationSelectedError } from "node_modules/@homarr/widgets/src/errors"; import { NoIntegrationSelectedError } from "node_modules/@homarr/widgets/src/errors";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
import { useSession } from "@homarr/auth/client";
import { isWidgetRestricted } from "@homarr/auth/shared";
import { useRequiredBoard } from "@homarr/boards/context"; import { useRequiredBoard } from "@homarr/boards/context";
import { useEditMode } from "@homarr/boards/edit-mode"; import { useEditMode } from "@homarr/boards/edit-mode";
import { useSettings } from "@homarr/settings"; import { useSettings } from "@homarr/settings";
@@ -16,8 +14,8 @@ import { WidgetError } from "@homarr/widgets/errors";
import type { SectionItem } from "~/app/[locale]/boards/_types"; import type { SectionItem } from "~/app/[locale]/boards/_types";
import classes from "../sections/item.module.css"; import classes from "../sections/item.module.css";
import { useItemActions } from "./item-actions"; import { useItemActions } from "./item-actions";
import itemContentClasses from "./item-content.module.css";
import { BoardItemMenu } from "./item-menu"; import { BoardItemMenu } from "./item-menu";
import { RestrictedWidgetContent } from "./restricted";
interface BoardItemContentProps { interface BoardItemContentProps {
item: SectionItem; item: SectionItem;
@@ -28,28 +26,51 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
const board = useRequiredBoard(); const board = useRequiredBoard();
return ( return (
<Card <>
ref={ref} <Card
className={combineClasses( ref={ref}
classes.itemCard, className={combineClasses(
`${item.kind}-wrapper`, classes.itemCard,
"grid-stack-item-content", `${item.kind}-wrapper`,
item.advancedOptions.customCssClasses.join(" "), "grid-stack-item-content",
item.advancedOptions.customCssClasses.join(" "),
)}
radius={board.itemRadius}
withBorder
styles={{
root: {
"--opacity": board.opacity / 100,
containerType: "size",
overflow: item.kind === "iframe" ? "hidden" : undefined,
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
},
}}
p={0}
>
<InnerContent item={item} width={width} height={height} />
</Card>
{item.advancedOptions.title?.trim() && (
<Badge
pos="absolute"
// It's 4 because of the mantine-react-table that has z-index 3
style={{ zIndex: 4 }}
top={2}
left={16}
size="xs"
radius={board.itemRadius}
styles={{
root: {
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
"--opacity": board.opacity / 100,
},
}}
className={itemContentClasses.badge}
c="var(--mantine-color-text)"
>
{item.advancedOptions.title}
</Badge>
)} )}
radius={board.itemRadius} </>
withBorder
styles={{
root: {
"--opacity": board.opacity / 100,
containerType: "size",
overflow: item.kind === "iframe" ? "hidden" : undefined,
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
},
}}
p={0}
>
<InnerContent item={item} width={width} height={height} />
</Card>
); );
}; };
@@ -62,7 +83,6 @@ interface InnerContentProps {
const InnerContent = ({ item, ...dimensions }: InnerContentProps) => { const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
const settings = useSettings(); const settings = useSettings();
const board = useRequiredBoard(); const board = useRequiredBoard();
const { data: session } = useSession();
const [isEditMode] = useEditMode(); const [isEditMode] = useEditMode();
const Comp = loadWidgetDynamic(item.kind); const Comp = loadWidgetDynamic(item.kind);
const { definition } = widgetImports[item.kind]; const { definition } = widgetImports[item.kind];
@@ -74,16 +94,6 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
const widgetSupportsIntegrations = const widgetSupportsIntegrations =
"supportedIntegrations" in definition && definition.supportedIntegrations.length >= 1; "supportedIntegrations" in definition && definition.supportedIntegrations.length >= 1;
if (
isWidgetRestricted({
definition,
user: session?.user ?? null,
check: (level) => level === "all",
})
) {
return <RestrictedWidgetContent kind={item.kind} />;
}
return ( return (
<QueryErrorResetBoundary> <QueryErrorResetBoundary>
{({ reset }) => ( {({ reset }) => (
@@ -115,8 +125,8 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
setOptions={(partialNewOptions) => setOptions={(partialNewOptions) =>
updateOptions({ updateOptions({
newOptions: { newOptions: {
...partialNewOptions.newOptions,
...options, ...options,
...partialNewOptions.newOptions,
}, },
}) })
} }
@@ -3,8 +3,6 @@ import { ActionIcon, Menu } from "@mantine/core";
import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil, IconTrash } from "@tabler/icons-react"; import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil, IconTrash } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { isWidgetRestricted } from "@homarr/auth/shared";
import { useEditMode } from "@homarr/boards/edit-mode"; import { useEditMode } from "@homarr/boards/edit-mode";
import { useConfirmModal, useModalAction } from "@homarr/modals"; import { useConfirmModal, useModalAction } from "@homarr/modals";
import { useSettings } from "@homarr/settings"; import { useSettings } from "@homarr/settings";
@@ -39,7 +37,6 @@ export const BoardItemMenu = ({
const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]); const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]);
const { gridstack } = useSectionContext().refs; const { gridstack } = useSectionContext().refs;
const settings = useSettings(); const settings = useSettings();
const { data: session } = useSession();
// Reset error boundary on next render if item has been edited // Reset error boundary on next render if item has been edited
useEffect(() => { useEffect(() => {
@@ -94,16 +91,6 @@ export const BoardItemMenu = ({
}); });
}; };
if (
isWidgetRestricted({
definition: currentDefinition,
user: session?.user ?? null,
check: (level) => level !== "none",
})
) {
return null;
}
return ( return (
<Menu withinPortal withArrow position="right-start" arrowPosition="center"> <Menu withinPortal withArrow position="right-start" arrowPosition="center">
<Menu.Target> <Menu.Target>
@@ -2,8 +2,6 @@ import { useMemo, useState } from "react";
import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core"; import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react"; import { IconSearch } from "@tabler/icons-react";
import { useSession } from "@homarr/auth/client";
import { isWidgetRestricted } from "@homarr/auth/shared";
import { objectEntries } from "@homarr/common"; import { objectEntries } from "@homarr/common";
import type { WidgetKind } from "@homarr/definitions"; import type { WidgetKind } from "@homarr/definitions";
import { createModal } from "@homarr/modals"; import { createModal } from "@homarr/modals";
@@ -17,18 +15,10 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const t = useI18n(); const t = useI18n();
const { createItem } = useItemActions(); const { createItem } = useItemActions();
const { data: session } = useSession();
const items = useMemo( const items = useMemo(
() => () =>
objectEntries(widgetImports) objectEntries(widgetImports)
.filter(([, value]) => {
return !isWidgetRestricted({
definition: value.definition,
user: session?.user ?? null,
check: (level) => level !== "none",
});
})
.map(([kind, value]) => ({ .map(([kind, value]) => ({
kind, kind,
icon: value.definition.icon, icon: value.definition.icon,
@@ -36,7 +26,7 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
description: t(`widget.${kind}.description`), description: t(`widget.${kind}.description`),
})) }))
.sort((itemA, itemB) => itemA.name.localeCompare(itemB.name)), .sort((itemA, itemB) => itemA.name.localeCompare(itemB.name)),
[t, session?.user], [t],
); );
const filteredItems = useMemo( const filteredItems = useMemo(
@@ -1,28 +0,0 @@
import { Center, Group, Stack, Text } from "@mantine/core";
import { IconShield } from "@tabler/icons-react";
import type { WidgetKind } from "@homarr/definitions";
import { useScopedI18n } from "@homarr/translation/client";
interface RestrictedWidgetProps {
kind: WidgetKind;
}
export const RestrictedWidgetContent = ({ kind }: RestrictedWidgetProps) => {
const tCurrentWidget = useScopedI18n(`widget.${kind}`);
const tCommonWidget = useScopedI18n("widget.common");
return (
<Center h="100%">
<Stack ta="center" gap="xs" align="center">
<Group gap="sm">
<IconShield size={16} />
<Text size="sm" fw="bold">
{tCommonWidget("restricted.title")}
</Text>
</Group>
<Text size="sm">{tCommonWidget("restricted.description", { name: tCurrentWidget("name") })}</Text>
</Stack>
</Center>
);
};
@@ -7,7 +7,7 @@ import { clientApi } from "@homarr/api/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals"; import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation"; import { boardRenameSchema } from "@homarr/validation/board";
interface InnerProps { interface InnerProps {
id: string; id: string;
@@ -26,7 +26,7 @@ export const BoardRenameModal = createModal<InnerProps>(({ actions, innerProps }
void utils.board.getHomeBoard.invalidate(); void utils.board.getHomeBoard.invalidate();
}, },
}); });
const form = useZodForm(validation.board.rename.omit({ id: true }), { const form = useZodForm(boardRenameSchema.omit({ id: true }), {
initialValues: { initialValues: {
name: innerProps.previousName, name: innerProps.previousName,
}, },
@@ -66,4 +66,4 @@ export const BoardRenameModal = createModal<InnerProps>(({ actions, innerProps }
defaultTitle: (t) => t("board.setting.section.dangerZone.action.rename.modal.title"), defaultTitle: (t) => t("board.setting.section.dangerZone.action.rename.modal.title"),
}); });
type FormType = Omit<z.infer<(typeof validation)["board"]["rename"]>, "id">; type FormType = Omit<z.infer<typeof boardRenameSchema>, "id">;
@@ -40,9 +40,7 @@ export const CategoryEditModal = createModal<InnerProps>(({ actions, innerProps
<Button onClick={actions.closeModal} variant="subtle" color="gray"> <Button onClick={actions.closeModal} variant="subtle" color="gray">
{t("common.action.cancel")} {t("common.action.cancel")}
</Button> </Button>
<Button type="submit" color="teal"> <Button type="submit">{innerProps.submitLabel}</Button>
{innerProps.submitLabel}
</Button>
</Group> </Group>
</Stack> </Stack>
</form> </form>
@@ -1,5 +1,5 @@
import type { WidgetKind } from "@homarr/definitions"; import type { WidgetKind } from "@homarr/definitions";
import type { SettingsContextProps } from "@homarr/settings"; import type { SettingsContextProps } from "@homarr/settings/creator";
import type { WidgetComponentProps } from "@homarr/widgets"; import type { WidgetComponentProps } from "@homarr/widgets";
import { reduceWidgetOptionsWithDefaultValues } from "@homarr/widgets"; import { reduceWidgetOptionsWithDefaultValues } from "@homarr/widgets";
@@ -2,7 +2,7 @@ import { useCallback } from "react";
import type { z } from "zod"; import type { z } from "zod";
import { useUpdateBoard } from "@homarr/boards/updater"; import { useUpdateBoard } from "@homarr/boards/updater";
import type { dynamicSectionOptionsSchema } from "@homarr/validation"; import type { dynamicSectionOptionsSchema } from "@homarr/validation/shared";
import { addDynamicSectionCallback } from "./actions/add-dynamic-section"; import { addDynamicSectionCallback } from "./actions/add-dynamic-section";
import type { RemoveDynamicSectionInput } from "./actions/remove-dynamic-section"; import type { RemoveDynamicSectionInput } from "./actions/remove-dynamic-section";
@@ -6,7 +6,7 @@ import type { z } from "zod";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals"; import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { dynamicSectionOptionsSchema } from "@homarr/validation"; import { dynamicSectionOptionsSchema } from "@homarr/validation/shared";
interface ModalProps { interface ModalProps {
value: z.infer<typeof dynamicSectionOptionsSchema>; value: z.infer<typeof dynamicSectionOptionsSchema>;
@@ -48,9 +48,7 @@ export const DynamicSectionEditModal = createModal<ModalProps>(({ actions, inner
<Button onClick={actions.closeModal} variant="subtle" color="gray"> <Button onClick={actions.closeModal} variant="subtle" color="gray">
{t("common.action.cancel")} {t("common.action.cancel")}
</Button> </Button>
<Button type="submit" color="teal"> <Button type="submit">{t("common.action.saveChanges")}</Button>
{t("common.action.saveChanges")}
</Button>
</Group> </Group>
</Group> </Group>
</Stack> </Stack>
@@ -0,0 +1,93 @@
"use client";
import type { PropsWithChildren } from "react";
import { Suspense, use } from "react";
import { Indicator, Menu, Text } from "@mantine/core";
import { IconBellRinging } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { useScopedI18n } from "@homarr/translation/client";
interface UpdateIndicatorProps extends PropsWithChildren {
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]> | undefined;
disabled: boolean;
}
export const UpdateIndicator = ({ children, availableUpdatesPromise, disabled }: UpdateIndicatorProps) => {
if (disabled || availableUpdatesPromise === undefined) {
return children;
}
return (
<Suspense fallback={children}>
<InnerUpdateIndicator availableUpdatesPromise={availableUpdatesPromise} disabled={disabled}>
{children}
</InnerUpdateIndicator>
</Suspense>
);
};
interface InnerUpdateIndicatorProps extends PropsWithChildren {
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]>;
disabled: boolean;
}
const InnerUpdateIndicator = ({ children, disabled, availableUpdatesPromise }: InnerUpdateIndicatorProps) => {
const availableUpdates = use(availableUpdatesPromise);
return (
<Indicator
disabled={!availableUpdates || availableUpdates.length === 0 || disabled}
size={15}
processing
withBorder
>
{children}
</Indicator>
);
};
interface AvailableUpdatesMenuItemProps {
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]> | undefined;
}
export const AvailableUpdatesMenuItem = ({ availableUpdatesPromise }: AvailableUpdatesMenuItemProps) => {
if (availableUpdatesPromise === undefined) {
return null;
}
return (
<Suspense fallback={null}>
<InnerAvailableUpdatesMenuItem availableUpdatesPromise={availableUpdatesPromise} />
</Suspense>
);
};
interface InnerAvailableUpdatesMenuItemProps {
availableUpdatesPromise: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]>;
}
const InnerAvailableUpdatesMenuItem = ({ availableUpdatesPromise }: InnerAvailableUpdatesMenuItemProps) => {
const t = useScopedI18n("common.userAvatar.menu");
const availableUpdates = use(availableUpdatesPromise);
if (availableUpdates === undefined || availableUpdates.length === 0) {
return null;
}
const latestUpdate = availableUpdates.at(0);
if (!latestUpdate) return null;
return (
<>
<Menu.Item component={"a"} href={latestUpdate.url} target="_blank" leftSection={<IconBellRinging size="1rem" />}>
<Text fw="bold" size="sm">
{t("updateAvailable", {
countUpdates: String(availableUpdates.length),
tag: latestUpdate.tagName,
})}
</Text>
</Menu.Item>
<Menu.Divider />
</>
);
};
@@ -1,21 +1,25 @@
import { Indicator, UnstyledButton } from "@mantine/core"; import { Suspense } from "react";
import { UnstyledButton } from "@mantine/core";
import { api } from "@homarr/api/server"; import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next"; import { auth } from "@homarr/auth/next";
import { CurrentUserAvatar } from "~/components/user-avatar"; import { CurrentUserAvatar } from "~/components/user-avatar";
import { UserAvatarMenu } from "~/components/user-avatar-menu"; import { UserAvatarMenu } from "~/components/user-avatar-menu";
import { UpdateIndicator } from "./update";
export const UserButton = async () => { export const UserButton = async () => {
const session = await auth(); const session = await auth();
const isAdmin = session?.user.permissions.includes("admin"); const isAdmin = session?.user.permissions.includes("admin");
const data = isAdmin ? await api.updateChecker.getAvailableUpdates() : undefined; const availableUpdatesPromise = isAdmin ? api.updateChecker.getAvailableUpdates() : undefined;
return ( return (
<UserAvatarMenu availableUpdates={data}> <UserAvatarMenu availableUpdatesPromise={availableUpdatesPromise}>
<UnstyledButton> <UnstyledButton>
<Indicator disabled={data?.length === 0 || !isAdmin} size={15} processing withBorder> <Suspense fallback={<CurrentUserAvatar size="md" />}>
<CurrentUserAvatar size="md" /> <UpdateIndicator availableUpdatesPromise={availableUpdatesPromise} disabled={!isAdmin}>
</Indicator> <CurrentUserAvatar size="md" />
</UpdateIndicator>
</Suspense>
</UnstyledButton> </UnstyledButton>
</UserAvatarMenu> </UserAvatarMenu>
); );
@@ -11,11 +11,11 @@ interface LogoProps {
export const Logo = ({ size = 60, shouldUseNextImage = false, src, alt }: LogoProps) => export const Logo = ({ size = 60, shouldUseNextImage = false, src, alt }: LogoProps) =>
shouldUseNextImage ? ( shouldUseNextImage ? (
<Image src={src} alt={alt} width={size} height={size} /> <Image className="logo" src={src} alt={alt} width={size} height={size} />
) : ( ) : (
// we only want to use next/image for logos that we are sure will be preloaded and are allowed // we only want to use next/image for logos that we are sure will be preloaded and are allowed
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
<img src={src} alt={alt} width={size} height={size} /> <img className="logo" src={src} alt={alt} width={size} height={size} />
); );
const logoWithTitleSizes = { const logoWithTitleSizes = {
@@ -7,7 +7,6 @@ import { useRouter } from "next/navigation";
import { Center, Menu, Stack, Text, useMantineColorScheme } from "@mantine/core"; import { Center, Menu, Stack, Text, useMantineColorScheme } from "@mantine/core";
import { useHotkeys, useTimeout } from "@mantine/hooks"; import { useHotkeys, useTimeout } from "@mantine/hooks";
import { import {
IconBellRinging,
IconCheck, IconCheck,
IconHome, IconHome,
IconLogin, IconLogin,
@@ -25,13 +24,14 @@ import { useScopedI18n } from "@homarr/translation/client";
import { useAuthContext } from "~/app/[locale]/_client-providers/session"; import { useAuthContext } from "~/app/[locale]/_client-providers/session";
import { CurrentLanguageCombobox } from "./language/current-language-combobox"; import { CurrentLanguageCombobox } from "./language/current-language-combobox";
import { AvailableUpdatesMenuItem } from "./layout/header/update";
interface UserAvatarMenuProps { interface UserAvatarMenuProps {
children: ReactNode; children: ReactNode;
availableUpdates?: RouterOutputs["updateChecker"]["getAvailableUpdates"]; availableUpdatesPromise?: Promise<RouterOutputs["updateChecker"]["getAvailableUpdates"]>;
} }
export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuProps) => { export const UserAvatarMenu = ({ children, availableUpdatesPromise }: UserAvatarMenuProps) => {
const t = useScopedI18n("common.userAvatar.menu"); const t = useScopedI18n("common.userAvatar.menu");
const { colorScheme, toggleColorScheme } = useMantineColorScheme(); const { colorScheme, toggleColorScheme } = useMantineColorScheme();
useHotkeys([["mod+J", toggleColorScheme]]); useHotkeys([["mod+J", toggleColorScheme]]);
@@ -65,24 +65,7 @@ export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuPro
// We use keepMounted so we can add event listeners to prevent navigating away without saving the board // We use keepMounted so we can add event listeners to prevent navigating away without saving the board
<Menu width={300} withArrow withinPortal keepMounted> <Menu width={300} withArrow withinPortal keepMounted>
<Menu.Dropdown> <Menu.Dropdown>
{availableUpdates && availableUpdates.length > 0 && availableUpdates[0] && ( <AvailableUpdatesMenuItem availableUpdatesPromise={availableUpdatesPromise} />
<>
<Menu.Item
component={"a"}
href={availableUpdates[0].url}
target="_blank"
leftSection={<IconBellRinging size="1rem" />}
>
<Text fw="bold" size="sm">
{t("updateAvailable", {
countUpdates: String(availableUpdates.length),
tag: availableUpdates[0].tagName,
})}
</Text>
</Menu.Item>
<Menu.Divider />
</>
)}
<Menu.Item onClick={toggleColorScheme} leftSection={<ColorSchemeIcon size="1rem" />}> <Menu.Item onClick={toggleColorScheme} leftSection={<ColorSchemeIcon size="1rem" />}>
{colorSchemeText} {colorSchemeText}
</Menu.Item> </Menu.Item>
+17 -6
View File
@@ -5,25 +5,36 @@ import SuperJSON from "superjson";
import type { AppRouter } from "@homarr/api"; import type { AppRouter } from "@homarr/api";
import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared"; import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
import { localeCookieKey } from "@homarr/definitions";
import type { SupportedLanguage } from "@homarr/translation";
import { supportedLanguages } from "@homarr/translation";
import { createI18nMiddleware } from "@homarr/translation/middleware"; import { createI18nMiddleware } from "@homarr/translation/middleware";
export async function middleware(request: NextRequest) { let isOnboardingFinished = false;
// fetch api does not work because window is not defined and we need to construct the url from the headers
// In next 15 we will be able to use node apis and such the db directly
const culture = await serverFetchApi.serverSettings.getCulture.query();
export async function middleware(request: NextRequest) {
// Redirect to onboarding if it's not finished yet // Redirect to onboarding if it's not finished yet
const pathname = request.nextUrl.pathname; const pathname = request.nextUrl.pathname;
if (!pathname.endsWith("/init")) {
if (!isOnboardingFinished && !pathname.endsWith("/init")) {
const currentOnboardingStep = await serverFetchApi.onboard.currentStep.query(); const currentOnboardingStep = await serverFetchApi.onboard.currentStep.query();
if (currentOnboardingStep.current !== "finish") { if (currentOnboardingStep.current !== "finish") {
return NextResponse.redirect(new URL("/init", request.url)); return NextResponse.redirect(new URL("/init", request.url));
} }
isOnboardingFinished = true;
}
// Only run this if the user has not already configured their language
const currentLocale = request.cookies.get(localeCookieKey)?.value;
let defaultLocale: SupportedLanguage = "en";
if (!currentLocale || !supportedLanguages.includes(currentLocale as SupportedLanguage)) {
defaultLocale = await serverFetchApi.serverSettings.getCulture.query().then((culture) => culture.defaultLocale);
} }
// We don't want to fallback to accept-language header so we clear it // We don't want to fallback to accept-language header so we clear it
request.headers.set("accept-language", ""); request.headers.set("accept-language", "");
const next = createI18nMiddleware(culture.defaultLocale); const next = createI18nMiddleware(defaultLocale);
return next(request); return next(request);
} }
+6
View File
@@ -6,6 +6,12 @@
--gridstack-cell-size: 0; --gridstack-cell-size: 0;
} }
.ui-resizable-handle {
// Override default z-index of 100 to prevent it from being on top of the header.
// See https://github.com/homarr-labs/homarr/issues/1999
z-index: 1 !important;
}
.grid-stack-placeholder > .placeholder-content { .grid-stack-placeholder > .placeholder-content {
background-color: rgb(248, 249, 250) !important; background-color: rgb(248, 249, 250) !important;
border-radius: 12px; border-radius: 12px;
+2 -1
View File
@@ -3,7 +3,8 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"~/*": ["./src/*"] "~/*": ["./src/*"],
"@homarr/node-unifi": ["../../node_modules/@types/node-unifi"]
}, },
"plugins": [ "plugins": [
{ {
+8 -7
View File
@@ -10,7 +10,7 @@
"main": "./src/main.ts", "main": "./src/main.ts",
"types": "./src/main.ts", "types": "./src/main.ts",
"scripts": { "scripts": {
"build": "esbuild src/main.ts --bundle --platform=node --loader:.scss=text --external:@opentelemetry/api --outfile=tasks.cjs", "build": "esbuild src/main.ts --bundle --platform=node --loader:.scss=text --external:*.node --external:@opentelemetry/api --external:deasync --outfile=tasks.cjs",
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts", "dev": "pnpm with-env tsx ./src/main.ts",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "prettier --check . --ignore-path ../../.gitignore",
@@ -36,19 +36,20 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0", "@homarr/widgets": "workspace:^0.1.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dotenv": "^16.4.7", "dotenv": "^16.5.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"undici": "7.6.0" "undici": "7.10.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.13.14", "@types/node": "^22.15.30",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"eslint": "^9.23.0", "esbuild": "^0.25.5",
"eslint": "^9.28.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"tsx": "4.19.3", "tsx": "4.19.4",
"typescript": "^5.8.2" "typescript": "^5.8.3"
} }
} }
+2 -1
View File
@@ -3,7 +3,8 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"~/*": ["src/*"] "~/*": ["src/*"],
"@homarr/node-unifi": ["../../node_modules/@types/node-unifi"]
}, },
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
}, },
+8 -7
View File
@@ -7,7 +7,7 @@
"main": "./src/main.ts", "main": "./src/main.ts",
"types": "./src/main.ts", "types": "./src/main.ts",
"scripts": { "scripts": {
"build": "esbuild src/main.ts --bundle --platform=node --outfile=wssServer.cjs --external:bcrypt --external:@opentelemetry/api --external:cpu-features --loader:.html=text --loader:.scss=text --loader:.node=text", "build": "esbuild src/main.ts --bundle --platform=node --outfile=wssServer.cjs --external:bcrypt --external:@opentelemetry/api --external:deasync --external:cpu-features --loader:.html=text --loader:.scss=text --loader:.node=text",
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts", "dev": "pnpm with-env tsx ./src/main.ts",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "prettier --check . --ignore-path ../../.gitignore",
@@ -25,17 +25,18 @@
"@homarr/log": "workspace:^", "@homarr/log": "workspace:^",
"@homarr/redis": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"dotenv": "^16.4.7", "dotenv": "^16.5.0",
"tsx": "4.19.3", "tsx": "4.19.4",
"ws": "^8.18.1" "ws": "^8.18.2"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.0", "@types/ws": "^8.18.1",
"eslint": "^9.23.0", "esbuild": "^0.25.5",
"eslint": "^9.28.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.2" "typescript": "^5.8.3"
} }
} }
+2 -1
View File
@@ -3,7 +3,8 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"~/*": ["src/*"] "~/*": ["src/*"],
"@homarr/node-unifi": ["../../node_modules/@types/node-unifi"]
}, },
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
}, },
+3 -1
View File
@@ -120,4 +120,6 @@ You can also support us by helping with [translating the entire project](https:/
Thanks to your generous sponsors we can continue to build Homarr. Check them out for high quality and easy to use development tools. Thanks to your generous sponsors we can continue to build Homarr. Check them out for high quality and easy to use development tools.
Feel free to contact us at homarr-labs@proton.me if you wish to become a sponsor. Feel free to contact us at homarr-labs@proton.me if you wish to become a sponsor.
[![Covered by Argos Visual Testing](https://argos-ci.com/badge-large.svg)](https://argos-ci.com?utm_source=%5Bhomarr%5D&utm_campaign=oss) [![Covered by Argos Visual Testing](https://argos-ci.com/badge-large.svg)](https://argos-ci.com?utm_source=%5Bhomarr%5D&utm_campaign=oss) \
[![Supported by PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=homarr-v1)
+1
View File
@@ -22,6 +22,7 @@ http {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
client_max_body_size 32M;
} }
} }
} }
+20 -20
View File
@@ -1,6 +1,6 @@
{ {
"name": "homarr", "name": "homarr",
"version": "1.13.1", "version": "1.23.0",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "cross-env CI=true turbo build", "build": "cross-env CI=true turbo build",
@@ -35,27 +35,27 @@
"@semantic-release/changelog": "^6.0.3", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^11.0.1", "@semantic-release/github": "^11.0.3",
"@semantic-release/npm": "^12.0.1", "@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.3", "@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.4.4", "@turbo/gen": "^2.5.4",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.5.1",
"@vitest/coverage-v8": "^3.0.9", "@vitest/coverage-v8": "^3.2.2",
"@vitest/ui": "^3.0.9", "@vitest/ui": "^3.2.2",
"conventional-changelog-conventionalcommits": "^8.0.0", "conventional-changelog-conventionalcommits": "^9.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"jsdom": "^26.0.0", "jsdom": "^26.1.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"semantic-release": "^24.2.3", "semantic-release": "^24.2.5",
"testcontainers": "^10.23.0", "testcontainers": "^11.0.1",
"turbo": "^2.4.4", "turbo": "^2.5.4",
"typescript": "^5.8.2", "typescript": "^5.8.3",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.9" "vitest": "^3.2.2"
}, },
"packageManager": "pnpm@10.7.0", "packageManager": "pnpm@10.11.1",
"engines": { "engines": {
"node": ">=22.14.0" "node": ">=22.16.0"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
@@ -70,16 +70,16 @@
"tree-sitter-json" "tree-sitter-json"
], ],
"overrides": { "overrides": {
"proxmox-api>undici": "7.6.0" "proxmox-api>undici": "7.10.0"
},
"patchedDependencies": {
"pretty-print-error": "patches/pretty-print-error.patch"
}, },
"allowUnusedPatches": true, "allowUnusedPatches": true,
"ignoredBuiltDependencies": [ "ignoredBuiltDependencies": [
"@scarf/scarf", "@scarf/scarf",
"core-js-pure", "core-js-pure",
"protobufjs" "protobufjs"
] ],
"patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch"
}
} }
} }
+2 -2
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.23.0", "eslint": "^9.28.0",
"typescript": "^5.8.2" "typescript": "^5.8.3"
} }
} }
+11 -10
View File
@@ -40,25 +40,26 @@
"@homarr/request-handler": "workspace:^0.1.0", "@homarr/request-handler": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.1.0", "@kubernetes/client-node": "^1.3.0",
"@trpc/client": "^11.0.1", "@tanstack/react-query": "^5.80.6",
"@trpc/react-query": "^11.0.1", "@trpc/client": "^11.3.1",
"@trpc/server": "^11.0.1", "@trpc/react-query": "^11.3.1",
"@trpc/server": "^11.3.1",
"@trpc/tanstack-react-query": "^11.3.1",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"next": "15.2.4", "next": "15.3.3",
"pretty-print-error": "^1.1.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"trpc-to-openapi": "^2.1.3", "trpc-to-openapi": "^2.3.1",
"zod": "^3.24.2" "zod": "^3.25.56"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.23.0", "eslint": "^9.28.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.2" "typescript": "^5.8.3"
} }
} }
+2 -2
View File
@@ -20,7 +20,7 @@ export type IntegrationAction = "query" | "interact";
* @param action query for showing data or interact for mutating data * @param action query for showing data or interact for mutating data
* @param kinds kinds of integrations that are supported * @param kinds kinds of integrations that are supported
* @returns middleware that can be used with trpc * @returns middleware that can be used with trpc
* @example publicProcedure.unstable_concat(createOneIntegrationMiddleware("query", "piHole", "homeAssistant")).query(...) * @example publicProcedure.concat(createOneIntegrationMiddleware("query", "piHole", "homeAssistant")).query(...)
* @throws TRPCError NOT_FOUND if the integration was not found * @throws TRPCError NOT_FOUND if the integration was not found
* @throws TRPCError FORBIDDEN if the user does not have permission to perform the specified action on the specified integration * @throws TRPCError FORBIDDEN if the user does not have permission to perform the specified action on the specified integration
*/ */
@@ -82,7 +82,7 @@ export const createOneIntegrationMiddleware = <TKind extends IntegrationKind>(
* @param action query for showing data or interact for mutating data * @param action query for showing data or interact for mutating data
* @param kinds kinds of integrations that are supported * @param kinds kinds of integrations that are supported
* @returns middleware that can be used with trpc * @returns middleware that can be used with trpc
* @example publicProcedure.unstable_concat(createManyIntegrationMiddleware("query", "piHole", "homeAssistant")).query(...) * @example publicProcedure.concat(createManyIntegrationMiddleware("query", "piHole", "homeAssistant")).query(...)
* @throws TRPCError NOT_FOUND if the integration was not found * @throws TRPCError NOT_FOUND if the integration was not found
* @throws TRPCError FORBIDDEN if the user does not have permission to perform the specified action on at least one of the specified integrations * @throws TRPCError FORBIDDEN if the user does not have permission to perform the specified action on at least one of the specified integrations
*/ */
+8 -7
View File
@@ -5,7 +5,8 @@ import { asc, createId, eq, inArray, like } from "@homarr/db";
import { apps } from "@homarr/db/schema"; import { apps } from "@homarr/db/schema";
import { selectAppSchema } from "@homarr/db/validationSchemas"; import { selectAppSchema } from "@homarr/db/validationSchemas";
import { getIconForName } from "@homarr/icons"; import { getIconForName } from "@homarr/icons";
import { validation } from "@homarr/validation"; import { appCreateManySchema, appEditSchema, appManageSchema } from "@homarr/validation/app";
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
import { convertIntersectionToZodObject } from "../schema-merger"; import { convertIntersectionToZodObject } from "../schema-merger";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
@@ -15,7 +16,7 @@ const defaultIcon = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@mas
export const appRouter = createTRPCRouter({ export const appRouter = createTRPCRouter({
getPaginated: protectedProcedure getPaginated: protectedProcedure
.input(validation.common.paginated) .input(paginatedSchema)
.output(z.object({ items: z.array(selectAppSchema), totalCount: z.number() })) .output(z.object({ items: z.array(selectAppSchema), totalCount: z.number() }))
.meta({ openapi: { method: "GET", path: "/api/apps/paginated", tags: ["apps"], protect: true } }) .meta({ openapi: { method: "GET", path: "/api/apps/paginated", tags: ["apps"], protect: true } })
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
@@ -83,7 +84,7 @@ export const appRouter = createTRPCRouter({
}); });
}), }),
byId: publicProcedure byId: publicProcedure
.input(validation.common.byId) .input(byIdSchema)
.output(selectAppSchema) .output(selectAppSchema)
.meta({ openapi: { method: "GET", path: "/api/apps/{id}", tags: ["apps"], protect: true } }) .meta({ openapi: { method: "GET", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
@@ -115,7 +116,7 @@ export const appRouter = createTRPCRouter({
}), }),
create: permissionRequiredProcedure create: permissionRequiredProcedure
.requiresPermission("app-create") .requiresPermission("app-create")
.input(validation.app.manage) .input(appManageSchema)
.output(z.object({ appId: z.string() })) .output(z.object({ appId: z.string() }))
.meta({ openapi: { method: "POST", path: "/api/apps", tags: ["apps"], protect: true } }) .meta({ openapi: { method: "POST", path: "/api/apps", tags: ["apps"], protect: true } })
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
@@ -133,7 +134,7 @@ export const appRouter = createTRPCRouter({
}), }),
createMany: permissionRequiredProcedure createMany: permissionRequiredProcedure
.requiresPermission("app-create") .requiresPermission("app-create")
.input(validation.app.createMany) .input(appCreateManySchema)
.output(z.void()) .output(z.void())
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await ctx.db.insert(apps).values( await ctx.db.insert(apps).values(
@@ -148,7 +149,7 @@ export const appRouter = createTRPCRouter({
}), }),
update: permissionRequiredProcedure update: permissionRequiredProcedure
.requiresPermission("app-modify-all") .requiresPermission("app-modify-all")
.input(convertIntersectionToZodObject(validation.app.edit)) .input(convertIntersectionToZodObject(appEditSchema))
.output(z.void()) .output(z.void())
.meta({ openapi: { method: "PATCH", path: "/api/apps/{id}", tags: ["apps"], protect: true } }) .meta({ openapi: { method: "PATCH", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
@@ -178,7 +179,7 @@ export const appRouter = createTRPCRouter({
.requiresPermission("app-full-all") .requiresPermission("app-full-all")
.output(z.void()) .output(z.void())
.meta({ openapi: { method: "DELETE", path: "/api/apps/{id}", tags: ["apps"], protect: true } }) .meta({ openapi: { method: "DELETE", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
.input(validation.common.byId) .input(byIdSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await ctx.db.delete(apps).where(eq(apps.id, input.id)); await ctx.db.delete(apps).where(eq(apps.id, input.id));
}), }),
+351 -264
View File
@@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server";
import superjson from "superjson"; import superjson from "superjson";
import { z } from "zod"; import { z } from "zod";
import { constructBoardPermissions, isWidgetRestricted } from "@homarr/auth/shared"; import { constructBoardPermissions } from "@homarr/auth/shared";
import type { DeviceType } from "@homarr/common/server"; import type { DeviceType } from "@homarr/common/server";
import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db"; import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db";
import { and, asc, createId, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db"; import { and, asc, createId, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db";
@@ -37,10 +37,22 @@ import {
import { importOldmarrAsync } from "@homarr/old-import"; import { importOldmarrAsync } from "@homarr/old-import";
import { importJsonFileSchema } from "@homarr/old-import/shared"; import { importJsonFileSchema } from "@homarr/old-import/shared";
import { oldmarrConfigSchema } from "@homarr/old-schema"; import { oldmarrConfigSchema } from "@homarr/old-schema";
import type { BoardItemAdvancedOptions } from "@homarr/validation"; import {
import { sectionSchema, sharedItemSchema, validation, zodUnionFromArray } from "@homarr/validation"; boardByNameSchema,
boardChangeVisibilitySchema,
boardCreateSchema,
boardDuplicateSchema,
boardRenameSchema,
boardSaveLayoutsSchema,
boardSavePartialSettingsSchema,
boardSavePermissionsSchema,
boardSaveSchema,
} from "@homarr/validation/board";
import { byIdSchema } from "@homarr/validation/common";
import { zodUnionFromArray } from "@homarr/validation/enums";
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
import { sectionSchema, sharedItemSchema } from "@homarr/validation/shared";
import { widgetImports } from "../../../widgets/src";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
import { throwIfActionForbiddenAsync } from "./board/board-access"; import { throwIfActionForbiddenAsync } from "./board/board-access";
import { generateResponsiveGridFor } from "./board/grid-algorithm"; import { generateResponsiveGridFor } from "./board/grid-algorithm";
@@ -248,7 +260,7 @@ export const boardRouter = createTRPCRouter({
}), }),
createBoard: permissionRequiredProcedure createBoard: permissionRequiredProcedure
.requiresPermission("board-create") .requiresPermission("board-create")
.input(validation.board.create) .input(boardCreateSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const boardId = createId(); const boardId = createId();
@@ -292,7 +304,7 @@ export const boardRouter = createTRPCRouter({
}), }),
duplicateBoard: permissionRequiredProcedure duplicateBoard: permissionRequiredProcedure
.requiresPermission("board-create") .requiresPermission("board-create")
.input(validation.board.duplicate) .input(boardDuplicateSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view"); await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view");
await noBoardWithSimilarNameAsync(ctx.db, input.name); await noBoardWithSimilarNameAsync(ctx.db, input.name);
@@ -324,13 +336,6 @@ export const boardRouter = createTRPCRouter({
} }
const { sections: boardSections, items: boardItems, layouts: boardLayouts, ...boardProps } = board; const { sections: boardSections, items: boardItems, layouts: boardLayouts, ...boardProps } = board;
const allowedBoardItems = boardItems.filter((item) => {
return !isWidgetRestricted({
definition: widgetImports[item.kind].definition,
user: ctx.session.user,
check: (level) => level !== "none",
});
});
const newBoardId = createId(); const newBoardId = createId();
@@ -378,8 +383,8 @@ export const boardRouter = createTRPCRouter({
), ),
); );
const itemMap = new Map<string, string>(allowedBoardItems.map((item) => [item.id, createId()])); const itemMap = new Map<string, string>(boardItems.map((item) => [item.id, createId()]));
const itemsToInsert: InferInsertModel<typeof items>[] = allowedBoardItems.map( const itemsToInsert: InferInsertModel<typeof items>[] = boardItems.map(
({ integrations: _, layouts: _layouts, ...item }) => ({ ({ integrations: _, layouts: _layouts, ...item }) => ({
...item, ...item,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -388,7 +393,7 @@ export const boardRouter = createTRPCRouter({
}), }),
); );
const itemLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = allowedBoardItems.flatMap((item) => const itemLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = boardItems.flatMap((item) =>
item.layouts.map( item.layouts.map(
(layoutSection): InferInsertModel<typeof itemLayouts> => ({ (layoutSection): InferInsertModel<typeof itemLayouts> => ({
...layoutSection, ...layoutSection,
@@ -421,7 +426,7 @@ export const boardRouter = createTRPCRouter({
) )
.then((result) => result.map((row) => row.id)); .then((result) => result.map((row) => row.id));
const itemIntegrationsToInsert = allowedBoardItems.flatMap((item) => const itemIntegrationsToInsert = boardItems.flatMap((item) =>
item.integrations item.integrations
// Restrict integrations to only those the user has access to // Restrict integrations to only those the user has access to
.filter(({ integrationId }) => integrationIdsWithAccess.includes(integrationId) || hasAccessForAll) .filter(({ integrationId }) => integrationIdsWithAccess.includes(integrationId) || hasAccessForAll)
@@ -514,34 +519,32 @@ export const boardRouter = createTRPCRouter({
}, },
}); });
}), }),
renameBoard: protectedProcedure.input(validation.board.rename).mutation(async ({ ctx, input }) => { renameBoard: protectedProcedure.input(boardRenameSchema).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full"); await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]); await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]);
await ctx.db.update(boards).set({ name: input.name }).where(eq(boards.id, input.id)); await ctx.db.update(boards).set({ name: input.name }).where(eq(boards.id, input.id));
}), }),
changeBoardVisibility: protectedProcedure changeBoardVisibility: protectedProcedure.input(boardChangeVisibilitySchema).mutation(async ({ ctx, input }) => {
.input(validation.board.changeVisibility) await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
.mutation(async ({ ctx, input }) => { const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board");
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board");
if ( if (
input.visibility !== "public" && input.visibility !== "public" &&
(boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id) (boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id)
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Cannot make home board private", message: "Cannot make home board private",
}); });
} }
await ctx.db await ctx.db
.update(boards) .update(boards)
.set({ isPublic: input.visibility === "public" }) .set({ isPublic: input.visibility === "public" })
.where(eq(boards.id, input.id)); .where(eq(boards.id, input.id));
}), }),
deleteBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => { deleteBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full"); await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
@@ -580,13 +583,13 @@ export const boardRouter = createTRPCRouter({
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null); return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
}), }),
getBoardByName: publicProcedure.input(validation.board.byName).query(async ({ input, ctx }) => { getBoardByName: publicProcedure.input(boardByNameSchema).query(async ({ input, ctx }) => {
const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase()); const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase());
await throwIfActionForbiddenAsync(ctx, boardWhere, "view"); await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null); return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
}), }),
saveLayouts: protectedProcedure.input(validation.board.saveLayouts).mutation(async ({ ctx, input }) => { saveLayouts: protectedProcedure.input(boardSaveLayoutsSchema).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify"); await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id); const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
@@ -712,7 +715,7 @@ export const boardRouter = createTRPCRouter({
} }
}), }),
savePartialBoardSettings: protectedProcedure savePartialBoardSettings: protectedProcedure
.input(validation.board.savePartialSettings.and(z.object({ id: z.string() }))) .input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() })))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify"); await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
@@ -746,145 +749,110 @@ export const boardRouter = createTRPCRouter({
}) })
.where(eq(boards.id, input.id)); .where(eq(boards.id, input.id));
}), }),
saveBoard: protectedProcedure.input(validation.board.save).mutation(async ({ input, ctx }) => { saveBoard: protectedProcedure.input(boardSaveSchema).mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify"); await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
const dbBoard = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id); const dbBoard = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
const addedSections = filterAddedItems(input.sections, dbBoard.sections);
const sectionsToInsert = addedSections.map(
(section): InferInsertModel<typeof sections> => ({
id: section.id,
kind: section.kind,
yOffset: section.kind !== "dynamic" ? section.yOffset : null,
xOffset: section.kind === "dynamic" ? null : 0,
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
name: "name" in section ? section.name : null,
boardId: dbBoard.id,
}),
);
const sectionLayoutsToInsert = addedSections
.filter((section) => section.kind === "dynamic")
.flatMap((section) =>
section.layouts.map(
(sectionLayout): InferInsertModel<typeof sectionLayouts> => ({
layoutId: sectionLayout.layoutId,
sectionId: section.id,
parentSectionId: sectionLayout.parentSectionId,
height: sectionLayout.height,
width: sectionLayout.width,
xOffset: sectionLayout.xOffset,
yOffset: sectionLayout.yOffset,
}),
),
);
const addedItems = filterAddedItems(input.items, dbBoard.items).filter((item) => {
return !isWidgetRestricted({
definition: widgetImports[item.kind].definition,
user: ctx.session.user,
check: (level) => level !== "none",
});
});
const itemsToInsert = addedItems.map(
(item): InferInsertModel<typeof items> => ({
id: item.id,
kind: item.kind,
options: superjson.stringify(item.options),
advancedOptions: superjson.stringify(item.advancedOptions),
boardId: dbBoard.id,
}),
);
const itemLayoutsToInsert = addedItems.flatMap((item) =>
item.layouts.map(
(layoutSection): InferInsertModel<typeof itemLayouts> => ({
layoutId: layoutSection.layoutId,
sectionId: layoutSection.sectionId,
itemId: item.id,
height: layoutSection.height,
width: layoutSection.width,
xOffset: layoutSection.xOffset,
yOffset: layoutSection.yOffset,
}),
),
);
const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) =>
integrationIds.map((integrationId) => ({
integrationId,
itemId,
})),
);
const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) =>
integrationIds.map((integrationId) => ({
integrationId,
itemId,
})),
);
const addedIntegrationRelations = inputIntegrationRelations.filter(
(inputRelation) =>
!dbIntegrationRelations.some(
(dbRelation) =>
dbRelation.itemId === inputRelation.itemId && dbRelation.integrationId === inputRelation.integrationId,
),
);
const integrationItemsToInsert = addedIntegrationRelations.map((relation) => ({
itemId: relation.itemId,
integrationId: relation.integrationId,
}));
const updatedItems = filterUpdatedItems(input.items, dbBoard.items).filter((item) => {
return !isWidgetRestricted({
definition: widgetImports[item.kind].definition,
user: ctx.session.user,
check: (level) => level !== "none",
});
});
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
const removedIntegrationRelations = dbIntegrationRelations.filter(
(dbRelation) =>
!inputIntegrationRelations.some(
(inputRelation) =>
dbRelation.itemId === inputRelation.itemId && dbRelation.integrationId === inputRelation.integrationId,
),
);
const removedItems = filterRemovedItems(input.items, dbBoard.items).filter((item) => {
return !isWidgetRestricted({
definition: widgetImports[item.kind].definition,
user: ctx.session.user,
check: (level) => level !== "none",
});
});
const itemIdsToRemove = removedItems.map((item) => item.id);
const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
const sectionIdsToRemove = removedSections.map((section) => section.id);
await handleTransactionsAsync(ctx.db, { await handleTransactionsAsync(ctx.db, {
async handleAsync(db, schema) { async handleAsync(db, schema) {
await db.transaction(async (transaction) => { await db.transaction(async (transaction) => {
if (sectionsToInsert.length > 0) { const addedSections = filterAddedItems(input.sections, dbBoard.sections);
await transaction.insert(schema.sections).values(sectionsToInsert);
if (addedSections.length > 0) {
await transaction.insert(schema.sections).values(
addedSections.map((section) => ({
id: section.id,
kind: section.kind,
yOffset: section.kind !== "dynamic" ? section.yOffset : null,
xOffset: section.kind === "dynamic" ? null : 0,
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
name: "name" in section ? section.name : null,
boardId: dbBoard.id,
})),
);
if (addedSections.some((section) => section.kind === "dynamic")) {
await transaction.insert(schema.sectionLayouts).values(
addedSections
.filter((section) => section.kind === "dynamic")
.flatMap((section) =>
section.layouts.map(
(sectionLayout): InferInsertModel<typeof schema.sectionLayouts> => ({
layoutId: sectionLayout.layoutId,
sectionId: section.id,
parentSectionId: sectionLayout.parentSectionId,
height: sectionLayout.height,
width: sectionLayout.width,
xOffset: sectionLayout.xOffset,
yOffset: sectionLayout.yOffset,
}),
),
),
);
}
} }
if (sectionLayoutsToInsert.length > 0) { const addedItems = filterAddedItems(input.items, dbBoard.items);
await transaction.insert(schema.sectionLayouts).values(sectionLayoutsToInsert);
if (addedItems.length > 0) {
await transaction.insert(schema.items).values(
addedItems.map((item) => ({
id: item.id,
kind: item.kind,
options: superjson.stringify(item.options),
advancedOptions: superjson.stringify(item.advancedOptions),
boardId: dbBoard.id,
})),
);
await transaction.insert(schema.itemLayouts).values(
addedItems.flatMap((item) =>
item.layouts.map(
(layoutSection): InferInsertModel<typeof schema.itemLayouts> => ({
layoutId: layoutSection.layoutId,
sectionId: layoutSection.sectionId,
itemId: item.id,
height: layoutSection.height,
width: layoutSection.width,
xOffset: layoutSection.xOffset,
yOffset: layoutSection.yOffset,
}),
),
),
);
} }
if (itemsToInsert.length > 0) { const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) =>
await transaction.insert(schema.items).values(itemsToInsert); integrationIds.map((integrationId) => ({
} integrationId,
if (itemLayoutsToInsert.length > 0) { itemId,
await transaction.insert(schema.itemLayouts).values(itemLayoutsToInsert); })),
);
const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) =>
integrationIds.map((integrationId) => ({
integrationId,
itemId,
})),
);
const addedIntegrationRelations = inputIntegrationRelations.filter(
(inputRelation) =>
!dbIntegrationRelations.some(
(dbRelation) =>
dbRelation.itemId === inputRelation.itemId &&
dbRelation.integrationId === inputRelation.integrationId,
),
);
if (addedIntegrationRelations.length > 0) {
await transaction.insert(schema.integrationItems).values(
addedIntegrationRelations.map((relation) => ({
itemId: relation.itemId,
integrationId: relation.integrationId,
})),
);
} }
if (integrationItemsToInsert.length > 0) { const updatedItems = filterUpdatedItems(input.items, dbBoard.items);
await transaction.insert(schema.integrationItems).values(integrationItemsToInsert);
}
for (const item of updatedItems) { for (const item of updatedItems) {
await transaction await transaction
@@ -915,6 +883,8 @@ export const boardRouter = createTRPCRouter({
} }
} }
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
for (const section of updatedSections) { for (const section of updatedSections) {
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id); const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
await transaction await transaction
@@ -948,6 +918,15 @@ export const boardRouter = createTRPCRouter({
} }
} }
const removedIntegrationRelations = dbIntegrationRelations.filter(
(dbRelation) =>
!inputIntegrationRelations.some(
(inputRelation) =>
dbRelation.itemId === inputRelation.itemId &&
dbRelation.integrationId === inputRelation.integrationId,
),
);
for (const relation of removedIntegrationRelations) { for (const relation of removedIntegrationRelations) {
await transaction await transaction
.delete(schema.integrationItems) .delete(schema.integrationItems)
@@ -959,36 +938,134 @@ export const boardRouter = createTRPCRouter({
); );
} }
if (itemIdsToRemove.length > 0) { const removedItems = filterRemovedItems(input.items, dbBoard.items);
await transaction.delete(schema.items).where(inArray(schema.items.id, itemIdsToRemove));
const itemIds = removedItems.map((item) => item.id);
if (itemIds.length > 0) {
await transaction.delete(schema.items).where(inArray(schema.items.id, itemIds));
} }
if (sectionIdsToRemove.length > 0) { const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
await transaction.delete(schema.sections).where(inArray(schema.sections.id, sectionIdsToRemove)); const sectionIds = removedSections.map((section) => section.id);
if (sectionIds.length > 0) {
await transaction.delete(schema.sections).where(inArray(schema.sections.id, sectionIds));
} }
}); });
}, },
handleSync(db) { handleSync(db) {
db.transaction((transaction) => { db.transaction((transaction) => {
if (sectionsToInsert.length > 0) { const addedSections = filterAddedItems(input.sections, dbBoard.sections);
transaction.insert(sections).values(sectionsToInsert).run();
if (addedSections.length > 0) {
transaction
.insert(sections)
.values(
addedSections.map((section) => ({
id: section.id,
kind: section.kind,
yOffset: section.kind !== "dynamic" ? section.yOffset : null,
xOffset: section.kind === "dynamic" ? null : 0,
options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON,
name: "name" in section ? section.name : null,
boardId: dbBoard.id,
})),
)
.run();
if (addedSections.some((section) => section.kind === "dynamic")) {
transaction
.insert(sectionLayouts)
.values(
addedSections
.filter((section) => section.kind === "dynamic")
.flatMap((section) =>
section.layouts.map(
(sectionLayout): InferInsertModel<typeof sectionLayouts> => ({
layoutId: sectionLayout.layoutId,
sectionId: section.id,
parentSectionId: sectionLayout.parentSectionId,
height: sectionLayout.height,
width: sectionLayout.width,
xOffset: sectionLayout.xOffset,
yOffset: sectionLayout.yOffset,
}),
),
),
)
.run();
}
} }
if (sectionLayoutsToInsert.length > 0) { const addedItems = filterAddedItems(input.items, dbBoard.items);
transaction.insert(sectionLayouts).values(sectionLayoutsToInsert).run();
if (addedItems.length > 0) {
transaction
.insert(items)
.values(
addedItems.map((item) => ({
id: item.id,
kind: item.kind,
options: superjson.stringify(item.options),
advancedOptions: superjson.stringify(item.advancedOptions),
boardId: dbBoard.id,
})),
)
.run();
transaction
.insert(itemLayouts)
.values(
addedItems.flatMap((item) =>
item.layouts.map(
(layoutSection): InferInsertModel<typeof itemLayouts> => ({
layoutId: layoutSection.layoutId,
sectionId: layoutSection.sectionId,
itemId: item.id,
height: layoutSection.height,
width: layoutSection.width,
xOffset: layoutSection.xOffset,
yOffset: layoutSection.yOffset,
}),
),
),
)
.run();
} }
if (itemsToInsert.length > 0) { const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) =>
transaction.insert(items).values(itemsToInsert).run(); integrationIds.map((integrationId) => ({
integrationId,
itemId,
})),
);
const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) =>
integrationIds.map((integrationId) => ({
integrationId,
itemId,
})),
);
const addedIntegrationRelations = inputIntegrationRelations.filter(
(inputRelation) =>
!dbIntegrationRelations.some(
(dbRelation) =>
dbRelation.itemId === inputRelation.itemId &&
dbRelation.integrationId === inputRelation.integrationId,
),
);
if (addedIntegrationRelations.length > 0) {
transaction
.insert(integrationItems)
.values(
addedIntegrationRelations.map((relation) => ({
itemId: relation.itemId,
integrationId: relation.integrationId,
})),
)
.run();
} }
if (itemLayoutsToInsert.length > 0) { const updatedItems = filterUpdatedItems(input.items, dbBoard.items);
transaction.insert(itemLayouts).values(itemLayoutsToInsert).run();
}
if (integrationItemsToInsert.length > 0) {
transaction.insert(integrationItems).values(integrationItemsToInsert).run();
}
for (const item of updatedItems) { for (const item of updatedItems) {
transaction transaction
@@ -1016,6 +1093,8 @@ export const boardRouter = createTRPCRouter({
} }
} }
const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections);
for (const section of updatedSections) { for (const section of updatedSections) {
const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id); const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id);
transaction transaction
@@ -1048,6 +1127,15 @@ export const boardRouter = createTRPCRouter({
} }
} }
const removedIntegrationRelations = dbIntegrationRelations.filter(
(dbRelation) =>
!inputIntegrationRelations.some(
(inputRelation) =>
dbRelation.itemId === inputRelation.itemId &&
dbRelation.integrationId === inputRelation.integrationId,
),
);
for (const relation of removedIntegrationRelations) { for (const relation of removedIntegrationRelations) {
transaction transaction
.delete(integrationItems) .delete(integrationItems)
@@ -1060,19 +1148,24 @@ export const boardRouter = createTRPCRouter({
.run(); .run();
} }
if (itemIdsToRemove.length > 0) { const removedItems = filterRemovedItems(input.items, dbBoard.items);
transaction.delete(items).where(inArray(items.id, itemIdsToRemove)).run();
const itemIds = removedItems.map((item) => item.id);
if (itemIds.length > 0) {
transaction.delete(items).where(inArray(items.id, itemIds)).run();
} }
if (sectionIdsToRemove.length > 0) { const removedSections = filterRemovedItems(input.sections, dbBoard.sections);
transaction.delete(sections).where(inArray(sections.id, sectionIdsToRemove)).run(); const sectionIds = removedSections.map((section) => section.id);
if (sectionIds.length > 0) {
transaction.delete(sections).where(inArray(sections.id, sectionIds)).run();
} }
}); });
}, },
}); });
}), }),
getBoardPermissions: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => {
getBoardPermissions: protectedProcedure.input(validation.board.permissions).query(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full"); await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({ const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({
@@ -1143,99 +1236,93 @@ export const boardRouter = createTRPCRouter({
}), }),
}; };
}), }),
saveUserBoardPermissions: protectedProcedure saveUserBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => {
.input(validation.board.savePermissions) await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
.mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
await handleTransactionsAsync(ctx.db, { await handleTransactionsAsync(ctx.db, {
async handleAsync(db, schema) { async handleAsync(db, schema) {
await db.transaction(async (transaction) => { await db.transaction(async (transaction) => {
await transaction await transaction.delete(schema.boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId));
.delete(schema.boardUserPermissions) if (input.permissions.length === 0) {
.where(eq(boardUserPermissions.boardId, input.entityId)); return;
if (input.permissions.length === 0) { }
return; await transaction.insert(schema.boardUserPermissions).values(
} input.permissions.map((permission) => ({
await transaction.insert(schema.boardUserPermissions).values( userId: permission.principalId,
permission: permission.permission,
boardId: input.entityId,
})),
);
});
},
handleSync(db) {
db.transaction((transaction) => {
transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId)).run();
if (input.permissions.length === 0) {
return;
}
transaction
.insert(boardUserPermissions)
.values(
input.permissions.map((permission) => ({ input.permissions.map((permission) => ({
userId: permission.principalId, userId: permission.principalId,
permission: permission.permission, permission: permission.permission,
boardId: input.entityId, boardId: input.entityId,
})), })),
); )
}); .run();
}, });
handleSync(db) { },
db.transaction((transaction) => { });
transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId)).run(); }),
if (input.permissions.length === 0) { saveGroupBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => {
return; await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
}
transaction
.insert(boardUserPermissions)
.values(
input.permissions.map((permission) => ({
userId: permission.principalId,
permission: permission.permission,
boardId: input.entityId,
})),
)
.run();
});
},
});
}),
saveGroupBoardPermissions: protectedProcedure
.input(validation.board.savePermissions)
.mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
await handleTransactionsAsync(ctx.db, { await handleTransactionsAsync(ctx.db, {
async handleAsync(db, schema) { async handleAsync(db, schema) {
await db.transaction(async (transaction) => { await db.transaction(async (transaction) => {
await transaction await transaction
.delete(schema.boardGroupPermissions) .delete(schema.boardGroupPermissions)
.where(eq(boardGroupPermissions.boardId, input.entityId)); .where(eq(boardGroupPermissions.boardId, input.entityId));
if (input.permissions.length === 0) { if (input.permissions.length === 0) {
return; return;
} }
await transaction.insert(schema.boardGroupPermissions).values( await transaction.insert(schema.boardGroupPermissions).values(
input.permissions.map((permission) => ({
groupId: permission.principalId,
permission: permission.permission,
boardId: input.entityId,
})),
);
});
},
handleSync(db) {
db.transaction((transaction) => {
transaction.delete(boardGroupPermissions).where(eq(boardGroupPermissions.boardId, input.entityId)).run();
if (input.permissions.length === 0) {
return;
}
transaction
.insert(boardGroupPermissions)
.values(
input.permissions.map((permission) => ({ input.permissions.map((permission) => ({
groupId: permission.principalId, groupId: permission.principalId,
permission: permission.permission, permission: permission.permission,
boardId: input.entityId, boardId: input.entityId,
})), })),
); )
}); .run();
}, });
handleSync(db) { },
db.transaction((transaction) => { });
transaction.delete(boardGroupPermissions).where(eq(boardGroupPermissions.boardId, input.entityId)).run(); }),
if (input.permissions.length === 0) {
return;
}
transaction
.insert(boardGroupPermissions)
.values(
input.permissions.map((permission) => ({
groupId: permission.principalId,
permission: permission.permission,
boardId: input.entityId,
})),
)
.run();
});
},
});
}),
importOldmarrConfig: permissionRequiredProcedure importOldmarrConfig: permissionRequiredProcedure
.requiresPermission("board-create") .requiresPermission("board-create")
.input(importJsonFileSchema) .input(importJsonFileSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const content = await input.file.text(); const content = await input.file.text();
const oldmarr = oldmarrConfigSchema.parse(JSON.parse(content)); const oldmarr = oldmarrConfigSchema.parse(JSON.parse(content));
await importOldmarrAsync(ctx.db, oldmarr, input.configuration, ctx.session); await importOldmarrAsync(ctx.db, oldmarr, input.configuration);
}), }),
}); });
@@ -1,8 +1,13 @@
import { X509Certificate } from "node:crypto";
import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { zfd } from "zod-form-data"; import { zfd } from "zod-form-data";
import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server"; import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server";
import { superRefineCertificateFile, validation } from "@homarr/validation"; import { and, eq } from "@homarr/db";
import { trustedCertificateHostnames } from "@homarr/db/schema";
import { logger } from "@homarr/log";
import { certificateValidFileNameSchema, superRefineCertificateFile } from "@homarr/validation/certificates";
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc"; import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
@@ -16,12 +21,106 @@ export const certificateRouter = createTRPCRouter({
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const content = await input.file.text(); const content = await input.file.text();
// Validate the certificate
let x509Certificate: X509Certificate;
try {
x509Certificate = new X509Certificate(content);
logger.info("Adding trusted certificate", {
subject: x509Certificate.subject,
issuer: x509Certificate.issuer,
});
} catch {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid certificate",
});
}
await addCustomRootCertificateAsync(input.file.name, content); await addCustomRootCertificateAsync(input.file.name, content);
logger.info("Added trusted certificate", {
subject: x509Certificate.subject,
issuer: x509Certificate.issuer,
});
}),
trustHostnameMismatch: permissionRequiredProcedure
.requiresPermission("admin")
.input(z.object({ hostname: z.string(), certificate: z.string() }))
.mutation(async ({ ctx, input }) => {
// Validate the certificate
let x509Certificate: X509Certificate;
try {
x509Certificate = new X509Certificate(input.certificate);
logger.info("Adding trusted hostname", {
subject: x509Certificate.subject,
issuer: x509Certificate.issuer,
thumbprint: x509Certificate.fingerprint256,
hostname: input.hostname,
});
} catch {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid certificate",
});
}
await ctx.db.insert(trustedCertificateHostnames).values({
hostname: input.hostname,
thumbprint: x509Certificate.fingerprint256,
certificate: input.certificate,
});
logger.info("Added trusted hostname", {
subject: x509Certificate.subject,
issuer: x509Certificate.issuer,
thumbprint: x509Certificate.fingerprint256,
hostname: input.hostname,
});
}),
removeTrustedHostname: permissionRequiredProcedure
.requiresPermission("admin")
.input(z.object({ hostname: z.string(), thumbprint: z.string() }))
.mutation(async ({ ctx, input }) => {
logger.info("Removing trusted hostname", {
hostname: input.hostname,
thumbprint: input.thumbprint,
});
const dbResult = await ctx.db
.delete(trustedCertificateHostnames)
.where(
and(
eq(trustedCertificateHostnames.hostname, input.hostname),
eq(trustedCertificateHostnames.thumbprint, input.thumbprint),
),
);
logger.info("Removed trusted hostname", {
hostname: input.hostname,
thumbprint: input.thumbprint,
count: dbResult.changes,
});
}), }),
removeCertificate: permissionRequiredProcedure removeCertificate: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(z.object({ fileName: validation.certificates.validFileNameSchema })) .input(z.object({ fileName: certificateValidFileNameSchema }))
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
await removeCustomRootCertificateAsync(input.fileName); logger.info("Removing trusted certificate", {
fileName: input.fileName,
});
const certificate = await removeCustomRootCertificateAsync(input.fileName);
if (!certificate) return;
// Delete all trusted hostnames for this certificate
await ctx.db
.delete(trustedCertificateHostnames)
.where(eq(trustedCertificateHostnames.thumbprint, certificate.fingerprint256));
logger.info("Removed trusted certificate", {
fileName: input.fileName,
subject: certificate.subject,
issuer: certificate.issuer,
});
}), }),
}); });
+37 -89
View File
@@ -1,94 +1,52 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { z } from "zod"; import { z } from "zod";
import { db, like, or } from "@homarr/db"; import type { Container, ContainerState, Docker, Port } from "@homarr/docker";
import { icons } from "@homarr/db/schema";
import { DockerSingleton } from "@homarr/docker"; import { DockerSingleton } from "@homarr/docker";
import type { Container, ContainerInfo, ContainerState, Docker, Port } from "@homarr/docker"; import { dockerContainersRequestHandler } from "@homarr/request-handler/docker";
import { logger } from "@homarr/log";
import { createCacheChannel } from "@homarr/redis";
import { dockerMiddleware } from "../../middlewares/docker"; import { dockerMiddleware } from "../../middlewares/docker";
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc"; import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
const dockerCache = createCacheChannel<{
containers: (ContainerInfo & { instance: string; iconUrl: string | null })[];
}>("docker-containers", 5 * 60 * 1000);
export const dockerRouter = createTRPCRouter({ export const dockerRouter = createTRPCRouter({
getContainers: permissionRequiredProcedure getContainers: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.unstable_concat(dockerMiddleware()) .concat(dockerMiddleware())
.query(async () => { .query(async () => {
const result = await dockerCache const innerHandler = dockerContainersRequestHandler.handler({});
.consumeAsync(async () => { const result = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
const dockerInstances = DockerSingleton.getInstances();
const containers = await Promise.all(
// Return all the containers of all the instances into only one item
dockerInstances.map(({ instance, host: key }) =>
instance.listContainers({ all: true }).then((containers) =>
containers.map((container) => ({
...container,
instance: key,
})),
),
),
).then((containers) => containers.flat());
const extractImage = (container: ContainerInfo) => container.Image.split("/").at(-1)?.split(":").at(0) ?? "";
const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`));
const dbIcons =
likeQueries.length >= 1
? await db.query.icons.findMany({
where: or(...likeQueries),
})
: [];
return {
containers: containers.map((container) => ({
...container,
iconUrl:
dbIcons.find((icon) => {
const extractedImage = extractImage(container);
if (!extractedImage) return false;
return icon.name.toLowerCase().includes(extractedImage.toLowerCase());
})?.url ?? null,
})),
};
})
.catch((error) => {
logger.error(error);
return {
isError: true,
error: error as unknown,
};
});
if ("isError" in result) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "An error occurred while fetching the containers",
cause: result.error,
});
}
const { data, timestamp } = result; const { data, timestamp } = result;
return { return {
containers: sanitizeContainers(data.containers), containers: data satisfies DockerContainer[],
timestamp, timestamp,
}; };
}), }),
subscribeContainers: permissionRequiredProcedure
.requiresPermission("admin")
.concat(dockerMiddleware())
.subscription(() => {
return observable<DockerContainer[]>((emit) => {
const innerHandler = dockerContainersRequestHandler.handler({});
const unsubscribe = innerHandler.subscribe((data) => {
emit.next(data);
});
return unsubscribe;
});
}),
invalidate: permissionRequiredProcedure invalidate: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.unstable_concat(dockerMiddleware()) .concat(dockerMiddleware())
.mutation(async () => { .mutation(async () => {
await dockerCache.invalidateAsync(); const innerHandler = dockerContainersRequestHandler.handler({});
return; await innerHandler.invalidateAsync();
}), }),
startAll: permissionRequiredProcedure startAll: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.unstable_concat(dockerMiddleware()) .concat(dockerMiddleware())
.input(z.object({ ids: z.array(z.string()) })) .input(z.object({ ids: z.array(z.string()) }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
await Promise.allSettled( await Promise.allSettled(
@@ -98,11 +56,12 @@ export const dockerRouter = createTRPCRouter({
}), }),
); );
await dockerCache.invalidateAsync(); const innerHandler = dockerContainersRequestHandler.handler({});
await innerHandler.invalidateAsync();
}), }),
stopAll: permissionRequiredProcedure stopAll: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.unstable_concat(dockerMiddleware()) .concat(dockerMiddleware())
.input(z.object({ ids: z.array(z.string()) })) .input(z.object({ ids: z.array(z.string()) }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
await Promise.allSettled( await Promise.allSettled(
@@ -112,11 +71,12 @@ export const dockerRouter = createTRPCRouter({
}), }),
); );
await dockerCache.invalidateAsync(); const innerHandler = dockerContainersRequestHandler.handler({});
await innerHandler.invalidateAsync();
}), }),
restartAll: permissionRequiredProcedure restartAll: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.unstable_concat(dockerMiddleware()) .concat(dockerMiddleware())
.input(z.object({ ids: z.array(z.string()) })) .input(z.object({ ids: z.array(z.string()) }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
await Promise.allSettled( await Promise.allSettled(
@@ -126,11 +86,12 @@ export const dockerRouter = createTRPCRouter({
}), }),
); );
await dockerCache.invalidateAsync(); const innerHandler = dockerContainersRequestHandler.handler({});
await innerHandler.invalidateAsync();
}), }),
removeAll: permissionRequiredProcedure removeAll: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.unstable_concat(dockerMiddleware()) .concat(dockerMiddleware())
.input(z.object({ ids: z.array(z.string()) })) .input(z.object({ ids: z.array(z.string()) }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
await Promise.allSettled( await Promise.allSettled(
@@ -140,7 +101,8 @@ export const dockerRouter = createTRPCRouter({
}), }),
); );
await dockerCache.invalidateAsync(); const innerHandler = dockerContainersRequestHandler.handler({});
await innerHandler.invalidateAsync();
}), }),
}); });
@@ -180,20 +142,6 @@ interface DockerContainer {
image: string; image: string;
ports: Port[]; ports: Port[];
iconUrl: string | null; iconUrl: string | null;
} cpuUsage: number;
memoryUsage: number;
function sanitizeContainers(
containers: (ContainerInfo & { instance: string; iconUrl: string | null })[],
): DockerContainer[] {
return containers.map((container) => {
return {
name: container.Names[0]?.split("/")[1] ?? "Unknown",
id: container.Id,
instance: container.instance,
state: container.State as ContainerState,
image: container.Image,
ports: container.Ports,
iconUrl: container.iconUrl,
};
});
} }
+21 -13
View File
@@ -6,7 +6,15 @@ import { and, createId, eq, handleTransactionsAsync, like, not } from "@homarr/d
import { getMaxGroupPositionAsync } from "@homarr/db/queries"; import { getMaxGroupPositionAsync } from "@homarr/db/queries";
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema"; import { groupMembers, groupPermissions, groups } from "@homarr/db/schema";
import { everyoneGroup } from "@homarr/definitions"; import { everyoneGroup } from "@homarr/definitions";
import { validation } from "@homarr/validation"; import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
import {
groupCreateSchema,
groupSavePartialSettingsSchema,
groupSavePermissionsSchema,
groupSavePositionsSchema,
groupUpdateSchema,
groupUserSchema,
} from "@homarr/validation/group";
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, protectedProcedure } from "../trpc"; import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, protectedProcedure } from "../trpc";
import { throwIfCredentialsDisabled } from "./invite/checks"; import { throwIfCredentialsDisabled } from "./invite/checks";
@@ -39,7 +47,7 @@ export const groupRouter = createTRPCRouter({
getPaginated: permissionRequiredProcedure getPaginated: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.common.paginated) .input(paginatedSchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined; const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined;
const groupCount = await ctx.db.$count(groups, whereQuery); const groupCount = await ctx.db.$count(groups, whereQuery);
@@ -74,7 +82,7 @@ export const groupRouter = createTRPCRouter({
}), }),
getById: permissionRequiredProcedure getById: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.common.byId) .input(byIdSchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const group = await ctx.db.query.groups.findFirst({ const group = await ctx.db.query.groups.findFirst({
where: eq(groups.id, input.id), where: eq(groups.id, input.id),
@@ -169,7 +177,7 @@ export const groupRouter = createTRPCRouter({
}), }),
createInitialExternalGroup: onboardingProcedure createInitialExternalGroup: onboardingProcedure
.requiresStep("group") .requiresStep("group")
.input(validation.group.create) .input(groupCreateSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkSimilarNameAndThrowAsync(ctx.db, input.name); await checkSimilarNameAndThrowAsync(ctx.db, input.name);
@@ -191,7 +199,7 @@ export const groupRouter = createTRPCRouter({
}), }),
createGroup: permissionRequiredProcedure createGroup: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.create) .input(groupCreateSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await checkSimilarNameAndThrowAsync(ctx.db, input.name); await checkSimilarNameAndThrowAsync(ctx.db, input.name);
@@ -209,7 +217,7 @@ export const groupRouter = createTRPCRouter({
}), }),
updateGroup: permissionRequiredProcedure updateGroup: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.update) .input(groupUpdateSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.id); await throwIfGroupNotFoundAsync(ctx.db, input.id);
await throwIfGroupNameIsReservedAsync(ctx.db, input.id); await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
@@ -225,7 +233,7 @@ export const groupRouter = createTRPCRouter({
}), }),
savePartialSettings: permissionRequiredProcedure savePartialSettings: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.savePartialSettings) .input(groupSavePartialSettingsSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.id); await throwIfGroupNotFoundAsync(ctx.db, input.id);
@@ -239,7 +247,7 @@ export const groupRouter = createTRPCRouter({
}), }),
savePositions: permissionRequiredProcedure savePositions: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.savePositions) .input(groupSavePositionsSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const positions = input.positions.map((id, index) => ({ id, position: index + 1 })); const positions = input.positions.map((id, index) => ({ id, position: index + 1 }));
@@ -262,7 +270,7 @@ export const groupRouter = createTRPCRouter({
}), }),
savePermissions: permissionRequiredProcedure savePermissions: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.savePermissions) .input(groupSavePermissionsSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.groupId); await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
@@ -277,7 +285,7 @@ export const groupRouter = createTRPCRouter({
}), }),
transferOwnership: permissionRequiredProcedure transferOwnership: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.groupUser) .input(groupUserSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.groupId); await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId); await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
@@ -291,7 +299,7 @@ export const groupRouter = createTRPCRouter({
}), }),
deleteGroup: permissionRequiredProcedure deleteGroup: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.common.byId) .input(byIdSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.id); await throwIfGroupNotFoundAsync(ctx.db, input.id);
await throwIfGroupNameIsReservedAsync(ctx.db, input.id); await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
@@ -300,7 +308,7 @@ export const groupRouter = createTRPCRouter({
}), }),
addMember: permissionRequiredProcedure addMember: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.groupUser) .input(groupUserSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.groupId); await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId); await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
@@ -324,7 +332,7 @@ export const groupRouter = createTRPCRouter({
}), }),
removeMember: permissionRequiredProcedure removeMember: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.input(validation.group.groupUser) .input(groupUserSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.groupId); await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId); await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
+2 -2
View File
@@ -1,11 +1,11 @@
import { and, like } from "@homarr/db"; import { and, like } from "@homarr/db";
import { icons } from "@homarr/db/schema"; import { icons } from "@homarr/db/schema";
import { validation } from "@homarr/validation"; import { iconsFindSchema } from "@homarr/validation/icons";
import { createTRPCRouter, publicProcedure } from "../trpc"; import { createTRPCRouter, publicProcedure } from "../trpc";
export const iconsRouter = createTRPCRouter({ export const iconsRouter = createTRPCRouter({
findIcons: publicProcedure.input(validation.icons.findIcons).query(async ({ ctx, input }) => { findIcons: publicProcedure.input(iconsFindSchema).query(async ({ ctx, input }) => {
return { return {
icons: await ctx.db.query.iconRepositories.findMany({ icons: await ctx.db.query.iconRepositories.findMany({
with: { with: {
@@ -37,7 +37,7 @@ export const importRouter = createTRPCRouter({
.requiresStep("import") .requiresStep("import")
.input(importInitialOldmarrInputSchema) .input(importInitialOldmarrInputSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await importInitialOldmarrAsync(ctx.db, input, ctx.session); await importInitialOldmarrAsync(ctx.db, input);
await nextOnboardingStepAsync(ctx.db, undefined); await nextOnboardingStepAsync(ctx.db, undefined);
}), }),
}); });
@@ -24,12 +24,19 @@ import {
integrationSecretKindObject, integrationSecretKindObject,
} from "@homarr/definitions"; } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations";
import { validation } from "@homarr/validation"; import { logger } from "@homarr/log";
import { byIdSchema } from "@homarr/validation/common";
import {
integrationCreateSchema,
integrationSavePermissionsSchema,
integrationUpdateSchema,
} from "@homarr/validation/integration";
import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
import { throwIfActionForbiddenAsync } from "./integration-access"; import { throwIfActionForbiddenAsync } from "./integration-access";
import { testConnectionAsync } from "./integration-test-connection"; import { MissingSecretError, testConnectionAsync } from "./integration-test-connection";
import { mapTestConnectionError } from "./map-test-connection-error";
export const integrationRouter = createTRPCRouter({ export const integrationRouter = createTRPCRouter({
all: publicProcedure.query(async ({ ctx }) => { all: publicProcedure.query(async ({ ctx }) => {
@@ -141,7 +148,7 @@ export const integrationRouter = createTRPCRouter({
}, },
}); });
}), }),
byId: protectedProcedure.input(validation.integration.byId).query(async ({ ctx, input }) => { byId: protectedProcedure.input(byIdSchema).query(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full"); await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
const integration = await ctx.db.query.integrations.findFirst({ const integration = await ctx.db.query.integrations.findFirst({
where: eq(integrations.id, input.id), where: eq(integrations.id, input.id),
@@ -178,16 +185,36 @@ export const integrationRouter = createTRPCRouter({
}), }),
create: permissionRequiredProcedure create: permissionRequiredProcedure
.requiresPermission("integration-create") .requiresPermission("integration-create")
.input(validation.integration.create) .input(integrationCreateSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await testConnectionAsync({ logger.info("Creating integration", {
name: input.name,
kind: input.kind,
url: input.url,
});
const result = await testConnectionAsync({
id: "new", id: "new",
name: input.name, name: input.name,
url: input.url, url: input.url,
kind: input.kind, kind: input.kind,
secrets: input.secrets, secrets: input.secrets,
}).catch((error) => {
if (!(error instanceof MissingSecretError)) throw error;
throw new TRPCError({
code: "BAD_REQUEST",
message: error.message,
});
}); });
if (!result.success) {
logger.error(result.error);
return {
error: mapTestConnectionError(result.error),
};
}
const integrationId = createId(); const integrationId = createId();
await ctx.db.insert(integrations).values({ await ctx.db.insert(integrations).values({
id: integrationId, id: integrationId,
@@ -206,6 +233,13 @@ export const integrationRouter = createTRPCRouter({
); );
} }
logger.info("Created integration", {
id: integrationId,
name: input.name,
kind: input.kind,
url: input.url,
});
if ( if (
input.attemptSearchEngineCreation && input.attemptSearchEngineCreation &&
integrationDefs[input.kind].category.flatMap((category) => category).includes("search") integrationDefs[input.kind].category.flatMap((category) => category).includes("search")
@@ -221,9 +255,13 @@ export const integrationRouter = createTRPCRouter({
}); });
} }
}), }),
update: protectedProcedure.input(validation.integration.update).mutation(async ({ ctx, input }) => { update: protectedProcedure.input(integrationUpdateSchema).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full"); await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
logger.info("Updating integration", {
id: input.id,
});
const integration = await ctx.db.query.integrations.findFirst({ const integration = await ctx.db.query.integrations.findFirst({
where: eq(integrations.id, input.id), where: eq(integrations.id, input.id),
with: { with: {
@@ -238,7 +276,7 @@ export const integrationRouter = createTRPCRouter({
}); });
} }
await testConnectionAsync( const testResult = await testConnectionAsync(
{ {
id: input.id, id: input.id,
name: input.name, name: input.name,
@@ -247,7 +285,21 @@ export const integrationRouter = createTRPCRouter({
secrets: input.secrets, secrets: input.secrets,
}, },
integration.secrets, integration.secrets,
); ).catch((error) => {
if (!(error instanceof MissingSecretError)) throw error;
throw new TRPCError({
code: "BAD_REQUEST",
message: error.message,
});
});
if (!testResult.success) {
logger.error(testResult.error);
return {
error: mapTestConnectionError(testResult.error),
};
}
await ctx.db await ctx.db
.update(integrations) .update(integrations)
@@ -281,8 +333,15 @@ export const integrationRouter = createTRPCRouter({
} }
} }
} }
logger.info("Updated integration", {
id: input.id,
name: input.name,
kind: integration.kind,
url: input.url,
});
}), }),
delete: protectedProcedure.input(validation.integration.delete).mutation(async ({ ctx, input }) => { delete: protectedProcedure.input(byIdSchema).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full"); await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
const integration = await ctx.db.query.integrations.findFirst({ const integration = await ctx.db.query.integrations.findFirst({
@@ -298,7 +357,7 @@ export const integrationRouter = createTRPCRouter({
await ctx.db.delete(integrations).where(eq(integrations.id, input.id)); await ctx.db.delete(integrations).where(eq(integrations.id, input.id));
}), }),
getIntegrationPermissions: protectedProcedure.input(validation.board.permissions).query(async ({ input, ctx }) => { getIntegrationPermissions: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full"); await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({ const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({
@@ -370,7 +429,7 @@ export const integrationRouter = createTRPCRouter({
}; };
}), }),
saveUserIntegrationPermissions: protectedProcedure saveUserIntegrationPermissions: protectedProcedure
.input(validation.integration.savePermissions) .input(integrationSavePermissionsSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full"); await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full");
@@ -416,7 +475,7 @@ export const integrationRouter = createTRPCRouter({
}); });
}), }),
saveGroupIntegrationPermissions: protectedProcedure saveGroupIntegrationPermissions: protectedProcedure
.input(validation.integration.savePermissions) .input(integrationSavePermissionsSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full"); await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full");
@@ -462,7 +521,7 @@ export const integrationRouter = createTRPCRouter({
}); });
}), }),
searchInIntegration: protectedProcedure searchInIntegration: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("search"))) .concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("search")))
.input(z.object({ integrationId: z.string(), query: z.string() })) .input(z.object({ integrationId: z.string(), query: z.string() }))
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const integrationInstance = await createIntegrationAsync(ctx.integration); const integrationInstance = await createIntegrationAsync(ctx.integration);
@@ -2,7 +2,7 @@ import { decryptSecret } from "@homarr/common/server";
import type { Integration } from "@homarr/db/schema"; import type { Integration } from "@homarr/db/schema";
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions"; import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
import { getAllSecretKindOptions } from "@homarr/definitions"; import { getAllSecretKindOptions } from "@homarr/definitions";
import { createIntegrationAsync, IntegrationTestConnectionError } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
type FormIntegration = Integration & { type FormIntegration = Integration & {
@@ -19,6 +19,12 @@ export const testConnectionAsync = async (
value: `${string}.${string}`; value: `${string}.${string}`;
}[] = [], }[] = [],
) => { ) => {
logger.info("Testing connection", {
integrationName: integration.name,
integrationKind: integration.kind,
integrationUrl: integration.url,
});
const formSecrets = integration.secrets const formSecrets = integration.secrets
.filter((secret) => secret.value !== null) .filter((secret) => secret.value !== null)
.map((secret) => ({ .map((secret) => ({
@@ -72,7 +78,15 @@ export const testConnectionAsync = async (
decryptedSecrets, decryptedSecrets,
}); });
await integrationInstance.testConnectionAsync(); const result = await integrationInstance.testConnectionAsync();
if (result.success) {
logger.info("Tested connection successfully", {
integrationName: integration.name,
integrationKind: integration.kind,
integrationUrl: integration.url,
});
}
return result;
}; };
interface SourcedIntegrationSecret { interface SourcedIntegrationSecret {
@@ -87,7 +101,7 @@ const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedInteg
); );
if (matchingSecretKindOptions.length === 0) { if (matchingSecretKindOptions.length === 0) {
throw new IntegrationTestConnectionError("secretNotDefined"); throw new MissingSecretError();
} }
if (matchingSecretKindOptions.length === 1) { if (matchingSecretKindOptions.length === 1) {
@@ -101,6 +115,18 @@ const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedInteg
); );
if (onlyFormSecretsKindOptions.length >= 1) { if (onlyFormSecretsKindOptions.length >= 1) {
// If the first option is no secret it would always be selected even if we want to have a secret
if (
onlyFormSecretsKindOptions.length >= 2 &&
onlyFormSecretsKindOptions.some((secretKinds) => secretKinds.length === 0)
) {
return (
// Will never be undefined because of the check above
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
onlyFormSecretsKindOptions.find((secretKinds) => secretKinds.length >= 1) ?? onlyFormSecretsKindOptions[0]!
);
}
// Will never be undefined because of the check above // Will never be undefined because of the check above
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return onlyFormSecretsKindOptions[0]!; return onlyFormSecretsKindOptions[0]!;
@@ -110,3 +136,9 @@ const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedInteg
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return matchingSecretKindOptions[0]!; return matchingSecretKindOptions[0]!;
}; };
export class MissingSecretError extends Error {
constructor() {
super("No secret defined for this integration");
}
}
@@ -0,0 +1,141 @@
import type { X509Certificate } from "node:crypto";
import type { RequestErrorCode } from "@homarr/common/server";
import type {
AnyTestConnectionError,
TestConnectionErrorDataOfType,
TestConnectionErrorType,
} from "@homarr/integrations/test-connection";
export interface MappedError {
name: string;
message: string;
metadata: { key: string; value: string | number | boolean }[];
cause?: MappedError;
}
const ignoredErrorProperties = ["name", "message", "cause", "stack"];
const mapError = (error: Error): MappedError => {
const metadata = Object.entries(error)
.filter(([key]) => !ignoredErrorProperties.includes(key))
.map(([key, value]) => {
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return { key, value };
}
return null;
})
.filter((value) => value !== null);
return {
name: error.name,
message: error.message,
metadata,
cause: error.cause && error.cause instanceof Error ? mapError(error.cause) : undefined,
};
};
export interface MappedCertificate {
isSelfSigned: boolean;
issuer: string;
subject: string;
serialNumber: string;
validFrom: Date;
validTo: Date;
fingerprint: string;
pem: string;
}
const mapCertificate = (certificate: X509Certificate, code: RequestErrorCode): MappedCertificate => ({
isSelfSigned: certificate.ca || code === "DEPTH_ZERO_SELF_SIGNED_CERT",
issuer: certificate.issuer,
subject: certificate.subject,
serialNumber: certificate.serialNumber,
validFrom: certificate.validFromDate,
validTo: certificate.validToDate,
fingerprint: certificate.fingerprint256,
pem: certificate.toString(),
});
type MappedData<TType extends TestConnectionErrorType> = TType extends "unknown" | "parse"
? undefined
: TType extends "certificate"
? {
type: TestConnectionErrorDataOfType<TType>["requestError"]["type"];
reason: TestConnectionErrorDataOfType<TType>["requestError"]["reason"];
certificate: MappedCertificate;
}
: TType extends "request"
? {
type: TestConnectionErrorDataOfType<TType>["requestError"]["type"];
reason: TestConnectionErrorDataOfType<TType>["requestError"]["reason"];
}
: TType extends "authorization"
? {
statusCode: TestConnectionErrorDataOfType<TType>["statusCode"];
reason: TestConnectionErrorDataOfType<TType>["reason"];
}
: TType extends "statusCode"
? {
statusCode: TestConnectionErrorDataOfType<TType>["statusCode"];
reason: TestConnectionErrorDataOfType<TType>["reason"];
url: TestConnectionErrorDataOfType<TType>["url"];
}
: never;
type AnyMappedData = {
[TType in TestConnectionErrorType]: MappedData<TType>;
}[TestConnectionErrorType];
const mapData = (error: AnyTestConnectionError): AnyMappedData => {
if (error.type === "unknown") return undefined;
if (error.type === "parse") return undefined;
if (error.type === "certificate") {
return {
type: error.data.requestError.type,
reason: error.data.requestError.reason,
certificate: mapCertificate(error.data.certificate, error.data.requestError.code),
};
}
if (error.type === "request") {
return {
type: error.data.requestError.type,
reason: error.data.requestError.reason,
};
}
if (error.type === "authorization") {
return {
statusCode: error.data.statusCode,
reason: error.data.reason,
};
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (error.type === "statusCode") {
return {
statusCode: error.data.statusCode,
reason: error.data.reason,
url: error.data.url,
};
}
throw new Error(`Unsupported error type: ${(error as AnyTestConnectionError).type}`);
};
interface MappedTestConnectionError<TType extends TestConnectionErrorType> {
type: TType;
name: string;
message: string;
data: MappedData<TType>;
cause?: MappedError;
}
export type AnyMappedTestConnectionError = {
[TType in TestConnectionErrorType]: MappedTestConnectionError<TType>;
}[TestConnectionErrorType];
export const mapTestConnectionError = (error: AnyTestConnectionError) => {
return {
type: error.type,
name: error.name,
message: error.message,
data: mapData(error),
cause: error.cause ? mapError(error.cause) : undefined,
} as AnyMappedTestConnectionError;
};
@@ -13,7 +13,7 @@ import { MemoryResourceParser } from "../resource-parser/memory-resource-parser"
export const clusterRouter = createTRPCRouter({ export const clusterRouter = createTRPCRouter({
getCluster: permissionRequiredProcedure getCluster: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
.unstable_concat(kubernetesMiddleware()) .concat(kubernetesMiddleware())
.query(async (): Promise<KubernetesCluster> => { .query(async (): Promise<KubernetesCluster> => {
const { coreApi, metricsApi, versionApi, kubeConfig } = KubernetesClient.getInstance(); const { coreApi, metricsApi, versionApi, kubeConfig } = KubernetesClient.getInstance();
Loaded 100 of 440 files, more files were not shown because too many files have changed in this diff. Show more