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
262 changed files with 19810 additions and 5334 deletions

No files matched your search

+6
View File
@@ -31,6 +31,12 @@ 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.18.0
- 1.17.0 - 1.17.0
- 1.16.0 - 1.16.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.15.0 22.16.0
+200
View File
@@ -1,3 +1,203 @@
## [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) ## [1.19.0](https://github.com/homarr-labs/homarr/compare/v1.18.1...v1.19.0) (2025-05-02)
### Features ### Features
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:22.15.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
+25 -25
View File
@@ -48,21 +48,21 @@
"@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.7", "@mantine/colors-generator": "^8.0.2",
"@mantine/core": "^7.17.7", "@mantine/core": "^8.0.2",
"@mantine/dropzone": "^7.17.7", "@mantine/dropzone": "^8.0.2",
"@mantine/hooks": "^7.17.7", "@mantine/hooks": "^8.0.2",
"@mantine/modals": "^7.17.7", "@mantine/modals": "^8.0.2",
"@mantine/tiptap": "^7.17.7", "@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.75.1", "@tanstack/react-query": "^5.80.6",
"@tanstack/react-query-devtools": "^5.75.1", "@tanstack/react-query-devtools": "^5.80.6",
"@tanstack/react-query-next-experimental": "^5.75.1", "@tanstack/react-query-next-experimental": "^5.80.6",
"@trpc/client": "^11.1.2", "@trpc/client": "^11.3.1",
"@trpc/next": "^11.1.2", "@trpc/next": "^11.3.1",
"@trpc/react-query": "^11.1.2", "@trpc/react-query": "^11.3.1",
"@trpc/server": "^11.1.2", "@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",
@@ -70,35 +70,35 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"flag-icons": "^7.3.2", "flag-icons": "^7.5.0",
"glob": "^11.0.2", "glob": "^11.0.2",
"jotai": "^2.12.3", "jotai": "^2.12.5",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "15.3.1", "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.87.0", "sass": "^1.89.1",
"superjson": "2.2.2", "superjson": "2.2.2",
"swagger-ui-react": "^5.21.0", "swagger-ui-react": "^5.24.0",
"use-deep-compare-effect": "^1.8.1", "use-deep-compare-effect": "^1.8.1",
"zod": "^3.24.3" "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.15.3", "@types/node": "^22.15.30",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@types/react": "19.1.2", "@types/react": "19.1.6",
"@types/react-dom": "19.1.3", "@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.25.1", "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.3" "typescript": "^5.8.3"
@@ -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";
@@ -204,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();
@@ -231,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
@@ -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,7 +11,6 @@ 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";
@@ -18,6 +18,8 @@ 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"];
@@ -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")}
@@ -24,13 +24,14 @@ 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 { appHrefSchema } from "@homarr/validation/app"; import { appHrefSchema } from "@homarr/validation/app";
import { integrationCreateSchema } from "@homarr/validation/integration"; 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 integrationCreateSchema>> & { searchParams: Partial<z.infer<typeof integrationCreateSchema>> & {
@@ -73,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(
@@ -81,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"),
@@ -114,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"),
@@ -164,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")}
@@ -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/certificates";
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,6 +1,7 @@
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 { IconAlertTriangle, IconCertificate, IconCertificateOff } from "@tabler/icons-react"; import { IconAlertTriangle, IconCertificate, IconCertificateOff } from "@tabler/icons-react";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -64,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 && (
@@ -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>
); );
@@ -125,8 +125,8 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
setOptions={(partialNewOptions) => setOptions={(partialNewOptions) =>
updateOptions({ updateOptions({
newOptions: { newOptions: {
...partialNewOptions.newOptions,
...options, ...options,
...partialNewOptions.newOptions,
}, },
}) })
} }
@@ -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 = {
+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": [
{ {
+5 -4
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 --external:deasync --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",
@@ -38,15 +38,16 @@
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"undici": "7.8.0" "undici": "7.10.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.15.3", "@types/node": "^22.15.30",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"eslint": "^9.25.1", "esbuild": "^0.25.5",
"eslint": "^9.28.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"tsx": "4.19.4", "tsx": "4.19.4",
"typescript": "^5.8.3" "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 -2
View File
@@ -27,14 +27,15 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"tsx": "4.19.4", "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.1", "@types/ws": "^8.18.1",
"eslint": "^9.25.1", "esbuild": "^0.25.5",
"eslint": "^9.28.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.3" "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)
+18 -15
View File
@@ -1,6 +1,6 @@
{ {
"name": "homarr", "name": "homarr",
"version": "1.19.0", "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.2", "@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.5.2", "@turbo/gen": "^2.5.4",
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.5.1",
"@vitest/coverage-v8": "^3.1.2", "@vitest/coverage-v8": "^3.2.2",
"@vitest/ui": "^3.1.2", "@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.1.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.25.0", "testcontainers": "^11.0.1",
"turbo": "^2.5.2", "turbo": "^2.5.4",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.2" "vitest": "^3.2.2"
}, },
"packageManager": "pnpm@10.10.0", "packageManager": "pnpm@10.11.1",
"engines": { "engines": {
"node": ">=22.15.0" "node": ">=22.16.0"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
@@ -70,13 +70,16 @@
"tree-sitter-json" "tree-sitter-json"
], ],
"overrides": { "overrides": {
"proxmox-api>undici": "7.8.0" "proxmox-api>undici": "7.10.0"
}, },
"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"
}
} }
} }
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+10 -10
View File
@@ -40,25 +40,25 @@
"@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.2", "@kubernetes/client-node": "^1.3.0",
"@tanstack/react-query": "^5.75.1", "@tanstack/react-query": "^5.80.6",
"@trpc/client": "^11.1.2", "@trpc/client": "^11.3.1",
"@trpc/react-query": "^11.1.2", "@trpc/react-query": "^11.3.1",
"@trpc/server": "^11.1.2", "@trpc/server": "^11.3.1",
"@trpc/tanstack-react-query": "^11.1.2", "@trpc/tanstack-react-query": "^11.3.1",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"next": "15.3.1", "next": "15.3.3",
"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.2.0", "trpc-to-openapi": "^2.3.1",
"zod": "^3.24.3" "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.25.1", "eslint": "^9.28.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
@@ -4,6 +4,9 @@ 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 { and, eq } from "@homarr/db";
import { trustedCertificateHostnames } from "@homarr/db/schema";
import { logger } from "@homarr/log";
import { certificateValidFileNameSchema, superRefineCertificateFile } from "@homarr/validation/certificates"; import { certificateValidFileNameSchema, superRefineCertificateFile } from "@homarr/validation/certificates";
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc"; import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
@@ -20,8 +23,13 @@ export const certificateRouter = createTRPCRouter({
const content = await input.file.text(); const content = await input.file.text();
// Validate the certificate // Validate the certificate
let x509Certificate: X509Certificate;
try { try {
new X509Certificate(content); x509Certificate = new X509Certificate(content);
logger.info("Adding trusted certificate", {
subject: x509Certificate.subject,
issuer: x509Certificate.issuer,
});
} catch { } catch {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
@@ -30,11 +38,89 @@ export const certificateRouter = createTRPCRouter({
} }
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: certificateValidFileNameSchema })) .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,
});
}), }),
}); });
+31 -83
View File
@@ -1,90 +1,48 @@
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")
.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")
.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")
@@ -98,7 +56,8 @@ export const dockerRouter = createTRPCRouter({
}), }),
); );
await dockerCache.invalidateAsync(); const innerHandler = dockerContainersRequestHandler.handler({});
await innerHandler.invalidateAsync();
}), }),
stopAll: permissionRequiredProcedure stopAll: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
@@ -112,7 +71,8 @@ export const dockerRouter = createTRPCRouter({
}), }),
); );
await dockerCache.invalidateAsync(); const innerHandler = dockerContainersRequestHandler.handler({});
await innerHandler.invalidateAsync();
}), }),
restartAll: permissionRequiredProcedure restartAll: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
@@ -126,7 +86,8 @@ export const dockerRouter = createTRPCRouter({
}), }),
); );
await dockerCache.invalidateAsync(); const innerHandler = dockerContainersRequestHandler.handler({});
await innerHandler.invalidateAsync();
}), }),
removeAll: permissionRequiredProcedure removeAll: permissionRequiredProcedure
.requiresPermission("admin") .requiresPermission("admin")
@@ -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,
};
});
} }
@@ -24,6 +24,7 @@ import {
integrationSecretKindObject, integrationSecretKindObject,
} from "@homarr/definitions"; } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations";
import { logger } from "@homarr/log";
import { byIdSchema } from "@homarr/validation/common"; import { byIdSchema } from "@homarr/validation/common";
import { import {
integrationCreateSchema, integrationCreateSchema,
@@ -34,7 +35,8 @@ import {
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 }) => {
@@ -185,14 +187,34 @@ export const integrationRouter = createTRPCRouter({
.requiresPermission("integration-create") .requiresPermission("integration-create")
.input(integrationCreateSchema) .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,
@@ -211,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")
@@ -229,6 +258,10 @@ export const integrationRouter = createTRPCRouter({
update: protectedProcedure.input(integrationUpdateSchema).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: {
@@ -243,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,
@@ -252,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)
@@ -286,6 +333,13 @@ export const integrationRouter = createTRPCRouter({
} }
} }
} }
logger.info("Updated integration", {
id: input.id,
name: input.name,
kind: integration.kind,
url: input.url,
});
}), }),
delete: protectedProcedure.input(byIdSchema).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");
@@ -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) {
@@ -122,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;
};
@@ -2,9 +2,10 @@ import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { asc, createId, eq, like } from "@homarr/db"; import { asc, createId, eq, like } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries"; import { getServerSettingByKeyAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
import { searchEngines, users } from "@homarr/db/schema"; import { searchEngines, users } from "@homarr/db/schema";
import { createIntegrationAsync } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations";
import { logger } from "@homarr/log";
import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common"; import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common";
import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine"; import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine";
import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request"; import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
@@ -96,23 +97,35 @@ export const searchEngineRouter = createTRPCRouter({
}); });
} }
const serverDefaultId = await getServerSettingByKeyAsync(ctx.db, "search").then( const searchSettings = await getServerSettingByKeyAsync(ctx.db, "search");
(setting) => setting.defaultSearchEngineId,
);
if (serverDefaultId) { if (!searchSettings.defaultSearchEngineId) return null;
return await ctx.db.query.searchEngines.findFirst({
where: eq(searchEngines.id, serverDefaultId), const serverDefault = await ctx.db.query.searchEngines.findFirst({
with: { where: eq(searchEngines.id, searchSettings.defaultSearchEngineId),
integration: { with: {
columns: { integration: {
kind: true, columns: {
url: true, kind: true,
id: true, url: true,
}, id: true,
}, },
}, },
},
});
if (serverDefault) return serverDefault;
// Remove the default search engine ID from settings if it does not longer exist
try {
await updateServerSettingByKeyAsync(ctx.db, "search", {
...searchSettings,
defaultSearchEngineId: null,
}); });
} catch (error) {
logger.warn(
new Error("Failed to update search settings after default search engine not found", { cause: error }),
);
} }
return null; return null;
@@ -4,14 +4,26 @@ import { describe, expect, test, vi } from "vitest";
import type { Session } from "@homarr/auth"; import type { Session } from "@homarr/auth";
import { objectKeys } from "@homarr/common"; import { objectKeys } from "@homarr/common";
import type { Database } from "@homarr/db"; import type { Database } from "@homarr/db";
import { getPermissionsWithChildren } from "@homarr/definitions";
import type { GroupPermissionKey } from "@homarr/definitions"; import type { GroupPermissionKey } from "@homarr/definitions";
import { getPermissionsWithChildren } from "@homarr/definitions";
import type { RouterInputs } from "../../.."; import type { RouterInputs } from "../../..";
import { dockerRouter } from "../../docker/docker-router"; import { dockerRouter } from "../../docker/docker-router";
// Mock the auth module to return an empty session // Mock the auth module to return an empty session
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session })); vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
vi.mock("@homarr/request-handler/docker", () => ({
dockerContainersRequestHandler: {
handler: () => ({
getCachedOrUpdatedDataAsync: async () => {
return await Promise.resolve({ containers: [] });
},
invalidateAsync: async () => {
return await Promise.resolve();
},
}),
},
}));
vi.mock("@homarr/redis", () => ({ vi.mock("@homarr/redis", () => ({
createCacheChannel: () => ({ createCacheChannel: () => ({
// eslint-disable-next-line @typescript-eslint/require-await // eslint-disable-next-line @typescript-eslint/require-await
@@ -22,6 +34,7 @@ vi.mock("@homarr/redis", () => ({
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
invalidateAsync: async () => {}, invalidateAsync: async () => {},
}), }),
createWidgetOptionsChannel: () => ({}),
})); }));
vi.mock("@homarr/docker/env", () => ({ vi.mock("@homarr/docker/env", () => ({
@@ -46,6 +59,7 @@ const validInputs: {
[key in (typeof procedureKeys)[number]]: RouterInputs["docker"][key]; [key in (typeof procedureKeys)[number]]: RouterInputs["docker"][key];
} = { } = {
getContainers: undefined, getContainers: undefined,
subscribeContainers: undefined,
startAll: { ids: ["1"] }, startAll: { ids: ["1"] },
stopAll: { ids: ["1"] }, stopAll: { ids: ["1"] },
restartAll: { ids: ["1"] }, restartAll: { ids: ["1"] },
@@ -25,7 +25,7 @@ const defaultSessionWithPermissions = (permissions: GroupPermissionKey[] = []) =
// Mock the auth module to return an empty session // Mock the auth module to return an empty session
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session })); vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
vi.mock("../../integration/integration-test-connection", () => ({ vi.mock("../../integration/integration-test-connection", () => ({
testConnectionAsync: async () => await Promise.resolve(undefined), testConnectionAsync: async () => await Promise.resolve({ success: true }),
})); }));
describe("all should return all integrations", () => { describe("all should return all integrations", () => {
@@ -22,7 +22,7 @@ describe("testConnectionAsync should run test connection of integration", () =>
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
factorySpy.mockReturnValue( factorySpy.mockReturnValue(
Promise.resolve({ Promise.resolve({
testConnectionAsync: async () => await Promise.resolve(), testConnectionAsync: async () => await Promise.resolve({ success: true }),
} as homarrIntegrations.PiHoleIntegrationV6), } as homarrIntegrations.PiHoleIntegrationV6),
); );
optionsSpy.mockReturnValue([["apiKey"]]); optionsSpy.mockReturnValue([["apiKey"]]);
@@ -64,7 +64,7 @@ describe("testConnectionAsync should run test connection of integration", () =>
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
factorySpy.mockReturnValue( factorySpy.mockReturnValue(
Promise.resolve({ Promise.resolve({
testConnectionAsync: async () => await Promise.resolve(), testConnectionAsync: async () => await Promise.resolve({ success: true }),
} as homarrIntegrations.PiHoleIntegrationV6), } as homarrIntegrations.PiHoleIntegrationV6),
); );
optionsSpy.mockReturnValue([["apiKey"]]); optionsSpy.mockReturnValue([["apiKey"]]);
@@ -113,7 +113,7 @@ describe("testConnectionAsync should run test connection of integration", () =>
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
factorySpy.mockReturnValue( factorySpy.mockReturnValue(
Promise.resolve({ Promise.resolve({
testConnectionAsync: async () => await Promise.resolve(), testConnectionAsync: async () => await Promise.resolve({ success: true }),
} as homarrIntegrations.PiHoleIntegrationV6), } as homarrIntegrations.PiHoleIntegrationV6),
); );
optionsSpy.mockReturnValue([["apiKey"]]); optionsSpy.mockReturnValue([["apiKey"]]);
@@ -162,7 +162,7 @@ describe("testConnectionAsync should run test connection of integration", () =>
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
factorySpy.mockReturnValue( factorySpy.mockReturnValue(
Promise.resolve({ Promise.resolve({
testConnectionAsync: async () => await Promise.resolve(), testConnectionAsync: async () => await Promise.resolve({ success: true }),
} as homarrIntegrations.PiHoleIntegrationV6), } as homarrIntegrations.PiHoleIntegrationV6),
); );
optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]); optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]);
@@ -215,7 +215,7 @@ describe("testConnectionAsync should run test connection of integration", () =>
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
factorySpy.mockReturnValue( factorySpy.mockReturnValue(
Promise.resolve({ Promise.resolve({
testConnectionAsync: async () => await Promise.resolve(), testConnectionAsync: async () => await Promise.resolve({ success: true }),
} as homarrIntegrations.PiHoleIntegrationV6), } as homarrIntegrations.PiHoleIntegrationV6),
); );
optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]); optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]);
+8 -4
View File
@@ -19,10 +19,11 @@ const createDownloadClientIntegrationMiddleware = (action: IntegrationAction) =>
export const downloadsRouter = createTRPCRouter({ export const downloadsRouter = createTRPCRouter({
getJobsAndStatuses: publicProcedure getJobsAndStatuses: publicProcedure
.concat(createDownloadClientIntegrationMiddleware("query")) .concat(createDownloadClientIntegrationMiddleware("query"))
.query(async ({ ctx }) => { .input(z.object({ limitPerIntegration: z.number().default(50) }))
.query(async ({ ctx, input }) => {
return await Promise.all( return await Promise.all(
ctx.integrations.map(async (integration) => { ctx.integrations.map(async (integration) => {
const innerHandler = downloadClientRequestHandler.handler(integration, {}); const innerHandler = downloadClientRequestHandler.handler(integration, { limit: input.limitPerIntegration });
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
@@ -40,7 +41,8 @@ export const downloadsRouter = createTRPCRouter({
}), }),
subscribeToJobsAndStatuses: publicProcedure subscribeToJobsAndStatuses: publicProcedure
.concat(createDownloadClientIntegrationMiddleware("query")) .concat(createDownloadClientIntegrationMiddleware("query"))
.subscription(({ ctx }) => { .input(z.object({ limitPerIntegration: z.number().default(50) }))
.subscription(({ ctx, input }) => {
return observable<{ return observable<{
integration: Modify<Integration, { kind: IntegrationKindByCategory<"downloadClient"> }>; integration: Modify<Integration, { kind: IntegrationKindByCategory<"downloadClient"> }>;
data: DownloadClientJobsAndStatus; data: DownloadClientJobsAndStatus;
@@ -48,7 +50,9 @@ export const downloadsRouter = createTRPCRouter({
const unsubscribes: (() => void)[] = []; const unsubscribes: (() => void)[] = [];
for (const integrationWithSecrets of ctx.integrations) { for (const integrationWithSecrets of ctx.integrations) {
const { decryptedSecrets: _, ...integration } = integrationWithSecrets; const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
const innerHandler = downloadClientRequestHandler.handler(integrationWithSecrets, {}); const innerHandler = downloadClientRequestHandler.handler(integrationWithSecrets, {
limit: input.limitPerIntegration,
});
const unsubscribe = innerHandler.subscribe((data) => { const unsubscribe = innerHandler.subscribe((data) => {
emit.next({ emit.next({
integration, integration,
+3 -3
View File
@@ -5,7 +5,7 @@ import { releasesRequestHandler } from "@homarr/request-handler/releases";
import { createTRPCRouter, publicProcedure } from "../../trpc"; import { createTRPCRouter, publicProcedure } from "../../trpc";
const formatVersionFilterRegex = (versionFilter: z.infer<typeof _releaseVersionFilterSchema> | undefined) => { const formatVersionFilterRegex = (versionFilter: z.infer<typeof releaseVersionFilterSchema> | undefined) => {
if (!versionFilter) return undefined; if (!versionFilter) return undefined;
const escapedPrefix = versionFilter.prefix ? escapeForRegEx(versionFilter.prefix) : ""; const escapedPrefix = versionFilter.prefix ? escapeForRegEx(versionFilter.prefix) : "";
@@ -15,7 +15,7 @@ const formatVersionFilterRegex = (versionFilter: z.infer<typeof _releaseVersionF
return `^${escapedPrefix}${precision}${escapedSuffix}$`; return `^${escapedPrefix}${precision}${escapedSuffix}$`;
}; };
const _releaseVersionFilterSchema = z.object({ const releaseVersionFilterSchema = z.object({
prefix: z.string().optional(), prefix: z.string().optional(),
precision: z.number(), precision: z.number(),
suffix: z.string().optional(), suffix: z.string().optional(),
@@ -29,7 +29,7 @@ export const releasesRouter = createTRPCRouter({
z.object({ z.object({
providerKey: z.string(), providerKey: z.string(),
identifier: z.string(), identifier: z.string(),
versionFilter: _releaseVersionFilterSchema.optional(), versionFilter: releaseVersionFilterSchema.optional(),
}), }),
), ),
}), }),
+1 -1
View File
@@ -35,7 +35,7 @@ async function importCookiesAsync() {
return (await cookies()) return (await cookies())
.getAll() .getAll()
.map(({ name, value }) => `${name}=${value}`) .map(({ name, value }) => `${name}=${encodeURIComponent(value)}`)
.join(";"); .join(";");
} }
+1
View File
@@ -40,6 +40,7 @@ export const env = createEnv({
AUTH_OIDC_GROUPS_ATTRIBUTE: z.string().default("groups"), // Is used in the signIn event to assign the correct groups, key is from object of decoded id_token AUTH_OIDC_GROUPS_ATTRIBUTE: z.string().default("groups"), // Is used in the signIn event to assign the correct groups, key is from object of decoded id_token
AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE: z.string().optional(), AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE: z.string().optional(),
AUTH_OIDC_FORCE_USERINFO: createBooleanSchema(false), AUTH_OIDC_FORCE_USERINFO: createBooleanSchema(false),
AUTH_OIDC_ENABLE_DANGEROUS_ACCOUNT_LINKING: createBooleanSchema(false),
} }
: {}), : {}),
...(authProviders.includes("ldap") ...(authProviders.includes("ldap")
+3
View File
@@ -13,6 +13,7 @@ import { extractProfileName } from "./providers/oidc/oidc-provider";
export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["events"], undefined>["signIn"] => { export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["events"], undefined>["signIn"] => {
return async ({ user, profile }) => { return async ({ user, profile }) => {
logger.debug(`SignIn EventHandler for user: ${JSON.stringify(user)} . profile: ${JSON.stringify(profile)}`);
if (!user.id) throw new Error("User ID is missing"); if (!user.id) throw new Error("User ID is missing");
const dbUser = await db.query.users.findFirst({ const dbUser = await db.query.users.findFirst({
@@ -28,11 +29,13 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
const groupsKey = env.AUTH_OIDC_GROUPS_ATTRIBUTE; const groupsKey = env.AUTH_OIDC_GROUPS_ATTRIBUTE;
// Groups from oidc provider are provided from the profile, it's not typed. // Groups from oidc provider are provided from the profile, it's not typed.
if (profile && groupsKey in profile && Array.isArray(profile[groupsKey])) { if (profile && groupsKey in profile && Array.isArray(profile[groupsKey])) {
logger.debug(`Using profile groups (${groupsKey}): ${JSON.stringify(profile[groupsKey])}`);
await synchronizeGroupsWithExternalForUserAsync(db, user.id, profile[groupsKey] as string[]); await synchronizeGroupsWithExternalForUserAsync(db, user.id, profile[groupsKey] as string[]);
} }
// In ldap-authroization we return the groups from ldap, it's not typed. // In ldap-authroization we return the groups from ldap, it's not typed.
if ("groups" in user && Array.isArray(user.groups)) { if ("groups" in user && Array.isArray(user.groups)) {
logger.debug(`Using profile groups: ${JSON.stringify(user.groups)}`);
await synchronizeGroupsWithExternalForUserAsync(db, user.id, user.groups as string[]); await synchronizeGroupsWithExternalForUserAsync(db, user.id, user.groups as string[]);
} }
await addUserToEveryoneGroupIfNotMemberAsync(db, user.id); await addUserToEveryoneGroupIfNotMemberAsync(db, user.id);
+9 -9
View File
@@ -23,8 +23,8 @@
}, },
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@auth/core": "^0.39.0", "@auth/core": "^0.39.1",
"@auth/drizzle-adapter": "^1.9.0", "@auth/drizzle-adapter": "^1.9.1",
"@homarr/certificates": "workspace:^0.1.0", "@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
@@ -32,22 +32,22 @@
"@homarr/env": "workspace:^0.1.0", "@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"bcrypt": "^5.1.1", "bcrypt": "^6.0.0",
"cookies": "^0.9.1", "cookies": "^0.9.1",
"ldapts": "7.4.0", "ldapts": "8.0.1",
"next": "15.3.1", "next": "15.3.3",
"next-auth": "5.0.0-beta.27", "next-auth": "5.0.0-beta.28",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"zod": "^3.24.3" "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/bcrypt": "5.0.2", "@types/bcrypt": "5.0.2",
"@types/cookies": "0.9.0", "@types/cookies": "0.9.1",
"eslint": "^9.25.1", "eslint": "^9.28.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
@@ -23,21 +23,22 @@ export type BoardPermissionsProps = (
export const constructBoardPermissions = (board: BoardPermissionsProps, session: Session | null) => { export const constructBoardPermissions = (board: BoardPermissionsProps, session: Session | null) => {
const creatorId = "creator" in board ? board.creator?.id : board.creatorId; const creatorId = "creator" in board ? board.creator?.id : board.creatorId;
const isCreator = session !== null && session.user.id === creatorId;
return { return {
hasFullAccess: hasFullAccess:
session?.user.id === creatorId || isCreator ||
board.userPermissions.some(({ permission }) => permission === "full") || board.userPermissions.some(({ permission }) => permission === "full") ||
board.groupPermissions.some(({ permission }) => permission === "full") || board.groupPermissions.some(({ permission }) => permission === "full") ||
(session?.user.permissions.includes("board-full-all") ?? false), (session?.user.permissions.includes("board-full-all") ?? false),
hasChangeAccess: hasChangeAccess:
session?.user.id === creatorId || isCreator ||
board.userPermissions.some(({ permission }) => permission === "modify" || permission === "full") || board.userPermissions.some(({ permission }) => permission === "modify" || permission === "full") ||
board.groupPermissions.some(({ permission }) => permission === "modify" || permission === "full") || board.groupPermissions.some(({ permission }) => permission === "modify" || permission === "full") ||
(session?.user.permissions.includes("board-modify-all") ?? false) || (session?.user.permissions.includes("board-modify-all") ?? false) ||
(session?.user.permissions.includes("board-full-all") ?? false), (session?.user.permissions.includes("board-full-all") ?? false),
hasViewAccess: hasViewAccess:
session?.user.id === creatorId || isCreator ||
board.userPermissions.length >= 1 || board.userPermissions.length >= 1 ||
board.groupPermissions.length >= 1 || board.groupPermissions.length >= 1 ||
board.isPublic || board.isPublic ||
@@ -286,4 +286,22 @@ describe("constructBoardPermissions", () => {
expect(result.hasChangeAccess).toBe(false); expect(result.hasChangeAccess).toBe(false);
expect(result.hasViewAccess).toBe(true); expect(result.hasViewAccess).toBe(true);
}); });
test("should return all false when creator is null and session is null", () => {
// Arrange
const board = {
creator: null,
userPermissions: [],
groupPermissions: [],
isPublic: false,
};
const session = null;
// Act
const result = constructBoardPermissions(board, session);
// Assert
expect(result.hasFullAccess).toBe(false);
expect(result.hasChangeAccess).toBe(false);
expect(result.hasViewAccess).toBe(false);
});
}); });
@@ -15,6 +15,7 @@ export const OidcProvider = (headers: ReadonlyHeaders | null): OIDCConfig<Profil
clientId: env.AUTH_OIDC_CLIENT_ID, clientId: env.AUTH_OIDC_CLIENT_ID,
clientSecret: env.AUTH_OIDC_CLIENT_SECRET, clientSecret: env.AUTH_OIDC_CLIENT_SECRET,
issuer: env.AUTH_OIDC_ISSUER, issuer: env.AUTH_OIDC_ISSUER,
allowDangerousEmailAccountLinking: env.AUTH_OIDC_ENABLE_DANGEROUS_ACCOUNT_LINKING,
authorization: { authorization: {
params: { params: {
scope: env.AUTH_OIDC_SCOPE_OVERWRITE, scope: env.AUTH_OIDC_SCOPE_OVERWRITE,
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+3 -2
View File
@@ -23,13 +23,14 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"undici": "7.8.0" "@homarr/db": "workspace:^0.1.0",
"undici": "7.10.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+62 -13
View File
@@ -1,13 +1,18 @@
import { X509Certificate } from "node:crypto";
import fsSync from "node:fs"; import fsSync from "node:fs";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import type { AgentOptions } from "node:https";
import { Agent as HttpsAgent } from "node:https"; import { Agent as HttpsAgent } from "node:https";
import path from "node:path"; import path from "node:path";
import { rootCertificates } from "node:tls"; import { checkServerIdentity, rootCertificates } from "node:tls";
import axios from "axios"; import axios from "axios";
import { fetch } from "undici"; import { fetch } from "undici";
import { env } from "@homarr/common/env"; import { env } from "@homarr/common/env";
import { LoggingAgent } from "@homarr/common/server"; import { LoggingAgent } from "@homarr/common/server";
import type { InferSelectModel } from "@homarr/db";
import { db } from "@homarr/db";
import type { trustedCertificateHostnames } from "@homarr/db/schema";
const getCertificateFolder = () => { const getCertificateFolder = () => {
return env.NODE_ENV === "production" return env.NODE_ENV === "production"
@@ -40,10 +45,23 @@ export const loadCustomRootCertificatesAsync = async () => {
export const removeCustomRootCertificateAsync = async (fileName: string) => { export const removeCustomRootCertificateAsync = async (fileName: string) => {
const folder = getCertificateFolder(); const folder = getCertificateFolder();
if (!folder) { if (!folder) {
return; return null;
} }
await fs.rm(path.join(folder, fileName)); const existingFiles = await fs.readdir(folder, { withFileTypes: true });
if (!existingFiles.some((file) => file.isFile() && file.name === fileName)) {
throw new Error(`File ${fileName} does not exist`);
}
const fullPath = path.join(folder, fileName);
const content = await fs.readFile(fullPath, "utf8");
await fs.rm(fullPath);
try {
return new X509Certificate(content);
} catch {
return null;
}
}; };
export const addCustomRootCertificateAsync = async (fileName: string, content: string) => { export const addCustomRootCertificateAsync = async (fileName: string, content: string) => {
@@ -61,25 +79,56 @@ export const addCustomRootCertificateAsync = async (fileName: string, content: s
await fs.writeFile(path.join(folder, fileName), content); await fs.writeFile(path.join(folder, fileName), content);
}; };
export const createCertificateAgentAsync = async () => { export const getTrustedCertificateHostnamesAsync = async () => {
return await db.query.trustedCertificateHostnames.findMany();
};
export const getAllTrustedCertificatesAsync = async () => {
const customCertificates = await loadCustomRootCertificatesAsync(); const customCertificates = await loadCustomRootCertificatesAsync();
return rootCertificates.concat(customCertificates.map((cert) => cert.content));
};
export const createCustomCheckServerIdentity = (
trustedHostnames: InferSelectModel<typeof trustedCertificateHostnames>[],
): typeof checkServerIdentity => {
return (hostname, peerCertificate) => {
const matchingTrustedHostnames = trustedHostnames.filter(
(cert) => cert.thumbprint === peerCertificate.fingerprint256,
);
// We trust the certificate if we have a matching hostname
if (matchingTrustedHostnames.some((cert) => cert.hostname === hostname)) return undefined;
return checkServerIdentity(hostname, peerCertificate);
};
};
export const createCertificateAgentAsync = async (override?: {
ca: string | string[];
checkServerIdentity: typeof checkServerIdentity;
}) => {
return new LoggingAgent({ return new LoggingAgent({
connect: { connect: override ?? {
ca: rootCertificates.concat(customCertificates.map((cert) => cert.content)), ca: await getAllTrustedCertificatesAsync(),
checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()),
}, },
}); });
}; };
export const createHttpsAgentAsync = async () => { export const createHttpsAgentAsync = async (override?: Pick<AgentOptions, "ca" | "checkServerIdentity">) => {
const customCertificates = await loadCustomRootCertificatesAsync(); return new HttpsAgent(
return new HttpsAgent({ override ?? {
ca: rootCertificates.concat(customCertificates.map((cert) => cert.content)), ca: await getAllTrustedCertificatesAsync(),
}); checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()),
},
);
}; };
export const createAxiosCertificateInstanceAsync = async () => { export const createAxiosCertificateInstanceAsync = async (
override?: Pick<AgentOptions, "ca" | "checkServerIdentity">,
) => {
return axios.create({ return axios.create({
httpsAgent: await createHttpsAgentAsync(), httpsAgent: await createHttpsAgentAsync(override),
}); });
}; };
+2 -1
View File
@@ -34,7 +34,8 @@
"@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.25.1", "esbuild": "^0.25.5",
"eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+5 -4
View File
@@ -30,17 +30,18 @@
"@homarr/env": "workspace:^0.1.0", "@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"next": "15.3.1", "next": "15.3.3",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"undici": "7.8.0", "undici": "7.10.0",
"zod": "^3.24.3" "zod": "^3.25.56",
"zod-validation-error": "^3.4.1"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+8
View File
@@ -5,3 +5,11 @@ export const splitToNChunks = <T>(array: T[], chunks: number): T[][] => {
} }
return result; return result;
}; };
export const splitToChunksWithNItems = <T>(array: T[], itemCount: number): T[][] => {
const result: T[][] = [];
for (let i = 0; i < array.length; i += itemCount) {
result.push(array.slice(i, i + itemCount));
}
return result;
};
+26
View File
@@ -0,0 +1,26 @@
import dayjs from "dayjs";
import type { UnitTypeShort } from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
dayjs.extend(isBetween);
const validUnits = ["h", "d", "w", "M", "y"] as UnitTypeShort[];
export const isDateWithin = (date: Date, relativeDate: string): boolean => {
if (relativeDate.length < 2) {
throw new Error("Relative date must be at least 2 characters long");
}
const amount = parseInt(relativeDate.slice(0, -1), 10);
if (isNaN(amount) || amount <= 0) {
throw new Error("Relative date must be a number greater than 0");
}
const unit = relativeDate.slice(-1) as dayjs.UnitTypeShort;
if (!validUnits.includes(unit)) {
throw new Error("Invalid relative time unit");
}
const startDate = dayjs().subtract(amount, unit);
return dayjs(date).isBetween(startDate, dayjs(), null, "[]");
};
@@ -0,0 +1,43 @@
import { AxiosError } from "axios";
import { logger } from "@homarr/log";
import type { AnyRequestError } from "../request-error";
import { RequestError } from "../request-error";
import { ResponseError } from "../response-error";
import { matchErrorCode } from "./fetch-http-error-handler";
import { HttpErrorHandler } from "./http-error-handler";
export class AxiosHttpErrorHandler extends HttpErrorHandler {
handleRequestError(error: unknown): AnyRequestError | undefined {
if (!(error instanceof AxiosError)) return undefined;
if (error.code === undefined) return undefined;
logger.debug("Received Axios request error", {
code: error.code,
message: error.message,
});
const requestErrorInput = matchErrorCode(error.code);
if (!requestErrorInput) return undefined;
return new RequestError(requestErrorInput, {
cause: error,
});
}
handleResponseError(error: unknown): ResponseError | undefined {
if (!(error instanceof AxiosError)) return undefined;
if (error.response === undefined) return undefined;
logger.debug("Received Axios response error", {
status: error.response.status,
url: error.response.config.url,
message: error.message,
});
return new ResponseError({
status: error.response.status,
url: error.response.config.url ?? "?",
});
}
}
@@ -0,0 +1,68 @@
import { logger } from "@homarr/log";
import { objectEntries } from "../../../object";
import type { Modify } from "../../../types";
import type { AnyRequestError, AnyRequestErrorInput, RequestErrorCode, RequestErrorReason } from "../request-error";
import { RequestError, requestErrorMap } from "../request-error";
import type { ResponseError } from "../response-error";
import { HttpErrorHandler } from "./http-error-handler";
export class FetchHttpErrorHandler extends HttpErrorHandler {
constructor(private type = "undici") {
super();
}
handleRequestError(error: unknown): AnyRequestError | undefined {
if (!isTypeErrorWithCode(error)) return undefined;
logger.debug(`Received ${this.type} request error`, {
code: error.cause.code,
});
const result = matchErrorCode(error.cause.code);
if (!result) return undefined;
return new RequestError(result, { cause: error });
}
/**
* Response errors do not exist for fetch as it does not throw errors for non successful responses.
*/
handleResponseError(_: unknown): ResponseError | undefined {
return undefined;
}
}
type TypeErrorWithCode = Modify<
TypeError,
{
cause: Error & { code: string };
}
>;
const isTypeErrorWithCode = (error: unknown): error is TypeErrorWithCode => {
return (
error instanceof TypeError &&
error.cause instanceof Error &&
"code" in error.cause &&
typeof error.cause.code === "string"
);
};
export const matchErrorCode = (code: string): AnyRequestErrorInput | undefined => {
for (const [key, value] of objectEntries(requestErrorMap)) {
const entries = Object.entries(value) as [string, string | string[]][];
const found = entries.find(([_, entryCode]) =>
typeof entryCode === "string" ? entryCode === code : entryCode.includes(code),
);
if (!found) continue;
return {
type: key,
reason: found[0] as RequestErrorReason<typeof key>,
code: code as RequestErrorCode,
};
}
return undefined;
};
@@ -0,0 +1,7 @@
import type { AnyRequestError } from "../request-error";
import type { ResponseError } from "../response-error";
export abstract class HttpErrorHandler {
abstract handleRequestError(error: unknown): AnyRequestError | undefined;
abstract handleResponseError(error: unknown): ResponseError | undefined;
}
@@ -0,0 +1,5 @@
export * from "./http-error-handler";
export * from "./fetch-http-error-handler";
export * from "./ofetch-http-error-handler";
export * from "./axios-http-error-handler";
export * from "./tsdav-http-error-handler";
@@ -0,0 +1,38 @@
import { FetchError } from "ofetch";
import { logger } from "@homarr/log";
import type { AnyRequestError } from "../request-error";
import { ResponseError } from "../response-error";
import { FetchHttpErrorHandler } from "./fetch-http-error-handler";
import { HttpErrorHandler } from "./http-error-handler";
/**
* Ofetch is a wrapper around the native fetch API
* which will always throw the FetchError (also for non successful responses).
*
* It is for example used within the ctrl packages like qbittorrent, deluge, transmission, etc.
*/
export class OFetchHttpErrorHandler extends HttpErrorHandler {
handleRequestError(error: unknown): AnyRequestError | undefined {
if (!(error instanceof FetchError)) return undefined;
if (!(error.cause instanceof TypeError)) return undefined;
const result = new FetchHttpErrorHandler("ofetch").handleRequestError(error.cause);
if (!result) return undefined;
return result;
}
handleResponseError(error: unknown): ResponseError | undefined {
if (!(error instanceof FetchError)) return undefined;
if (error.response === undefined) return undefined;
logger.debug("Received ofetch response error", {
status: error.response.status,
url: error.response.url,
});
return new ResponseError(error.response);
}
}
@@ -0,0 +1,25 @@
import { logger } from "@homarr/log";
import type { AnyRequestError } from "../request-error";
import { ResponseError } from "../response-error";
import { FetchHttpErrorHandler } from "./fetch-http-error-handler";
import { HttpErrorHandler } from "./http-error-handler";
export class TsdavHttpErrorHandler extends HttpErrorHandler {
handleRequestError(error: unknown): AnyRequestError | undefined {
return new FetchHttpErrorHandler("tsdav").handleRequestError(error);
}
handleResponseError(error: unknown): ResponseError | undefined {
if (!(error instanceof Error)) return undefined;
// Tsdav sadly does not throw a custom error and rather just uses "Error"
// https://github.com/natelindev/tsdav/blob/bf33f04b1884694d685ee6f2b43fe9354b12d167/src/account.ts#L86
if (error.message !== "Invalid credentials") return undefined;
logger.debug("Received Tsdav response error", {
status: 401,
});
return new ResponseError({ status: 401, url: "?" });
}
}
+3
View File
@@ -0,0 +1,3 @@
export * from "./handlers";
export * from "./request-error";
export * from "./response-error";
@@ -0,0 +1,73 @@
export type AnyRequestError = {
[key in keyof RequestErrorMap]: RequestError<key>;
}[keyof RequestErrorMap];
export type AnyRequestErrorInput = {
[key in RequestErrorType]: RequestErrorInput<key>;
}[RequestErrorType];
export interface RequestErrorInput<TType extends RequestErrorType> {
type: TType;
reason: RequestErrorReason<TType>;
code: RequestErrorCode;
}
export class RequestError<TType extends RequestErrorType> extends Error {
public readonly type: TType;
public readonly reason: RequestErrorReason<TType>;
public readonly code: RequestErrorCode;
constructor(input: AnyRequestErrorInput, options: { cause?: Error }) {
super("Request failed", options);
this.name = RequestError.name;
this.type = input.type as TType;
this.reason = input.reason as RequestErrorReason<TType>;
this.code = input.code;
}
get cause(): Error | undefined {
return super.cause as Error | undefined;
}
}
export const requestErrorMap = {
certificate: {
expired: ["CERT_HAS_EXPIRED"],
hostnameMismatch: ["ERR_TLS_CERT_ALTNAME_INVALID", "CERT_COMMON_NAME_INVALID"],
notYetValid: ["CERT_NOT_YET_VALID"],
untrusted: ["DEPTH_ZERO_SELF_SIGNED_CERT", "UNABLE_TO_VERIFY_LEAF_SIGNATURE", "UNABLE_TO_GET_ISSUER_CERT_LOCALLY"],
},
connection: {
hostUnreachable: "EHOSTUNREACH",
networkUnreachable: "ENETUNREACH",
refused: "ECONNREFUSED",
reset: "ECONNRESET",
},
dns: {
notFound: "ENOTFOUND",
timeout: "EAI_AGAIN",
noAnswer: "ENODATA",
},
timeout: {
aborted: "ECONNABORTED",
timeout: "ETIMEDOUT",
},
} as const satisfies Record<string, Record<string, string | string[]>>;
type RequestErrorMap = typeof requestErrorMap;
export type RequestErrorType = keyof RequestErrorMap;
export type RequestErrorReason<TType extends RequestErrorType> = keyof RequestErrorMap[TType];
export type AnyRequestErrorReason = {
[key in keyof RequestErrorMap]: RequestErrorReason<key>;
}[keyof RequestErrorMap];
type ExtractInnerValues<T> = {
[K in keyof T]: T[K][keyof T[K]];
}[keyof T];
type FlattenStringOrStringArray<T> = T extends (infer U)[] ? U : T;
export type RequestErrorCode = FlattenStringOrStringArray<ExtractInnerValues<typeof requestErrorMap>>;
@@ -0,0 +1,12 @@
export class ResponseError extends Error {
public readonly statusCode: number;
public readonly url?: string;
constructor(response: { status: number; url?: string }, options?: ErrorOptions) {
super("Response did not indicate success", options);
this.name = ResponseError.name;
this.statusCode = response.status;
this.url = response.url;
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from "./parse";
export * from "./http";
@@ -0,0 +1,3 @@
export * from "./parse-error-handler";
export * from "./zod-parse-error-handler";
export * from "./json-parse-error-handler";
@@ -0,0 +1,16 @@
import { logger } from "@homarr/log";
import { ParseError } from "../parse-error";
import { ParseErrorHandler } from "./parse-error-handler";
export class JsonParseErrorHandler extends ParseErrorHandler {
handleParseError(error: unknown): ParseError | undefined {
if (!(error instanceof SyntaxError)) return undefined;
logger.debug("Received JSON parse error", {
message: error.message,
});
return new ParseError("Failed to parse json", { cause: error });
}
}
@@ -0,0 +1,5 @@
import type { ParseError } from "../parse-error";
export abstract class ParseErrorHandler {
abstract handleParseError(error: unknown): ParseError | undefined;
}
@@ -0,0 +1,24 @@
import { ZodError } from "zod";
import { fromError } from "zod-validation-error";
import { logger } from "@homarr/log";
import { ParseError } from "../parse-error";
import { ParseErrorHandler } from "./parse-error-handler";
export class ZodParseErrorHandler extends ParseErrorHandler {
handleParseError(error: unknown): ParseError | undefined {
if (!(error instanceof ZodError)) return undefined;
// TODO: migrate to zod v4 prettfyError once it's released
// https://v4.zod.dev/v4#error-pretty-printing
const message = fromError(error, {
issueSeparator: "\n",
prefix: null,
}).toString();
logger.debug("Received Zod parse error");
return new ParseError(message, { cause: error });
}
}
@@ -0,0 +1,2 @@
export * from "./handlers";
export * from "./parse-error";
@@ -0,0 +1,10 @@
export class ParseError extends Error {
constructor(message: string, options?: { cause: Error }) {
super(`Failed to parse data:\n${message}`, options);
this.name = ParseError.name;
}
get cause(): Error {
return super.cause as Error;
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ export class LoggingAgent extends Agent {
url.searchParams.set(key, "REDACTED"); url.searchParams.set(key, "REDACTED");
}); });
logger.info( logger.debug(
`Dispatching request ${url.toString().replaceAll("=&", "&")} (${Object.keys(options.headers ?? {}).length} headers)`, `Dispatching request ${url.toString().replaceAll("=&", "&")} (${Object.keys(options.headers ?? {}).length} headers)`,
); );
return super.dispatch(options, handler); return super.dispatch(options, handler);
+3
View File
@@ -0,0 +1,3 @@
export const isFunction = (value: unknown): value is (...args: unknown[]) => unknown => {
return typeof value === "function";
};
+2
View File
@@ -2,6 +2,7 @@ export * from "./object";
export * from "./string"; export * from "./string";
export * from "./cookie"; export * from "./cookie";
export * from "./array"; export * from "./array";
export * from "./date";
export * from "./stopwatch"; export * from "./stopwatch";
export * from "./hooks"; export * from "./hooks";
export * from "./url"; export * from "./url";
@@ -9,3 +10,4 @@ export * from "./number";
export * from "./error"; export * from "./error";
export * from "./fetch-with-timeout"; export * from "./fetch-with-timeout";
export * from "./theme"; export * from "./theme";
export * from "./function";
+1
View File
@@ -2,3 +2,4 @@ export * from "./security";
export * from "./encryption"; export * from "./encryption";
export * from "./user-agent"; export * from "./user-agent";
export * from "./fetch-agent"; export * from "./fetch-agent";
export * from "./errors";
+49
View File
@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import { splitToChunksWithNItems, splitToNChunks } from "../array";
describe("splitToNChunks", () => {
it("should split an array into the specified number of chunks", () => {
const array = [1, 2, 3, 4, 5];
const chunks = 3;
const result = splitToNChunks(array, chunks);
expect(result).toEqual([[1, 2], [3, 4], [5]]);
});
it("should handle an empty array", () => {
const array: number[] = [];
const chunks = 3;
const result = splitToNChunks(array, chunks);
expect(result).toEqual([[], [], []]);
});
it("should handle more chunks than elements", () => {
const array = [1, 2];
const chunks = 5;
const result = splitToNChunks(array, chunks);
expect(result).toEqual([[1], [2], [], [], []]);
});
});
describe("splitToChunksWithNItems", () => {
it("should split an array into chunks with the specified number of items", () => {
const array = [1, 2, 3, 4, 5];
const items = 2;
const result = splitToChunksWithNItems(array, items);
expect(result).toEqual([[1, 2], [3, 4], [5]]);
});
it("should handle an empty array", () => {
const array: number[] = [];
const items = 2;
const result = splitToChunksWithNItems(array, items);
expect(result).toEqual([]);
});
it("should handle more items per chunk than elements", () => {
const array = [1, 2];
const items = 5;
const result = splitToChunksWithNItems(array, items);
expect(result).toEqual([[1, 2]]);
});
});
+91
View File
@@ -0,0 +1,91 @@
import { describe, expect, it } from "vitest";
import { isDateWithin } from "../date";
describe("isDateWithin", () => {
it("should return true for a date within the specified hours", () => {
const date = new Date();
date.setHours(date.getHours() - 20);
expect(isDateWithin(date, "100h")).toBe(true);
});
it("should return false for a date outside the specified hours", () => {
const date = new Date();
date.setHours(date.getHours() - 101);
expect(isDateWithin(date, "100h")).toBe(false);
});
it("should return true for a date within the specified days", () => {
const date = new Date();
date.setDate(date.getDate() - 5);
expect(isDateWithin(date, "10d")).toBe(true);
});
it("should return false for a date outside the specified days", () => {
const date = new Date();
date.setDate(date.getDate() - 11);
expect(isDateWithin(date, "10d")).toBe(false);
});
it("should return true for a date within the specified weeks", () => {
const date = new Date();
date.setDate(date.getDate() - 10);
expect(isDateWithin(date, "7w")).toBe(true);
});
it("should return false for a date outside the specified weeks", () => {
const date = new Date();
date.setDate(date.getDate() - 50);
expect(isDateWithin(date, "7w")).toBe(false);
});
it("should return true for a date within the specified months", () => {
const date = new Date();
date.setMonth(date.getMonth() - 1);
expect(isDateWithin(date, "2M")).toBe(true);
});
it("should return false for a date outside the specified months", () => {
const date = new Date();
date.setMonth(date.getMonth() - 3);
expect(isDateWithin(date, "2M")).toBe(false);
});
it("should return true for a date within the specified years", () => {
const date = new Date();
date.setFullYear(date.getFullYear() - 1);
expect(isDateWithin(date, "2y")).toBe(true);
});
it("should return false for a date outside the specified years", () => {
const date = new Date();
date.setFullYear(date.getFullYear() - 3);
expect(isDateWithin(date, "2y")).toBe(false);
});
it("should return false for a date after the specified relative time", () => {
const date = new Date();
date.setDate(date.getDate() + 2);
expect(isDateWithin(date, "1d")).toBe(false);
});
it("should throw an error for an invalid unit", () => {
const date = new Date();
expect(() => isDateWithin(date, "2x")).toThrow("Invalid relative time unit");
});
it("should throw an error if relativeDate is less than 2 characters long", () => {
const date = new Date();
expect(() => isDateWithin(date, "h")).toThrow("Relative date must be at least 2 characters long");
});
it("should throw an error if relativeDate has an invalid number", () => {
const date = new Date();
expect(() => isDateWithin(date, "hh")).toThrow("Relative date must be a number greater than 0");
});
it("should throw an error if relativeDate is set to 0", () => {
const date = new Date();
expect(() => isDateWithin(date, "0y")).toThrow("Relative date must be a number greater than 0");
});
});
+4 -4
View File
@@ -21,7 +21,7 @@ const REDACTED = "REDACTED";
describe("LoggingAgent should log all requests", () => { describe("LoggingAgent should log all requests", () => {
test("should log all requests", () => { test("should log all requests", () => {
// Arrange // Arrange
const infoLogSpy = vi.spyOn(logger, "info"); const infoLogSpy = vi.spyOn(logger, "debug");
const agent = new LoggingAgent(); const agent = new LoggingAgent();
// Act // Act
@@ -33,7 +33,7 @@ describe("LoggingAgent should log all requests", () => {
test("should show amount of headers", () => { test("should show amount of headers", () => {
// Arrange // Arrange
const infoLogSpy = vi.spyOn(logger, "info"); const infoLogSpy = vi.spyOn(logger, "debug");
const agent = new LoggingAgent(); const agent = new LoggingAgent();
// Act // Act
@@ -68,7 +68,7 @@ describe("LoggingAgent should log all requests", () => {
[`/?stringWith13Chars=${"a".repeat(13)}`, `/?stringWith13Chars=${REDACTED}`], [`/?stringWith13Chars=${"a".repeat(13)}`, `/?stringWith13Chars=${REDACTED}`],
])("should redact sensitive data in url https://homarr.dev%s", (path, expected) => { ])("should redact sensitive data in url https://homarr.dev%s", (path, expected) => {
// Arrange // Arrange
const infoLogSpy = vi.spyOn(logger, "info"); const infoLogSpy = vi.spyOn(logger, "debug");
const agent = new LoggingAgent(); const agent = new LoggingAgent();
// Act // Act
@@ -87,7 +87,7 @@ describe("LoggingAgent should log all requests", () => {
["date times", "/?datetime=2022-01-01T00:00:00.000Z"], ["date times", "/?datetime=2022-01-01T00:00:00.000Z"],
])("should not redact values that are %s", (_reason, path) => { ])("should not redact values that are %s", (_reason, path) => {
// Arrange // Arrange
const infoLogSpy = vi.spyOn(logger, "info"); const infoLogSpy = vi.spyOn(logger, "debug");
const agent = new LoggingAgent(); const agent = new LoggingAgent();
// Act // Act
+1 -1
View File
@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+1
View File
@@ -24,6 +24,7 @@ export const cronJobs = {
mediaTranscoding: { preventManualExecution: false }, mediaTranscoding: { preventManualExecution: false },
minecraftServerStatus: { preventManualExecution: false }, minecraftServerStatus: { preventManualExecution: false },
networkController: { preventManualExecution: false }, networkController: { preventManualExecution: false },
dockerContainers: { preventManualExecution: false },
} satisfies Record<JobGroupKeys, { preventManualExecution?: boolean }>; } satisfies Record<JobGroupKeys, { preventManualExecution?: boolean }>;
/** /**
+1 -1
View File
@@ -7,6 +7,6 @@ import { cronJobRunnerChannel } from ".";
*/ */
export const registerCronJobRunner = () => { export const registerCronJobRunner = () => {
cronJobRunnerChannel.subscribe((jobName) => { cronJobRunnerChannel.subscribe((jobName) => {
jobGroup.runManually(jobName); void jobGroup.runManuallyAsync(jobName);
}); });
}; };
+1 -1
View File
@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+2 -2
View File
@@ -25,14 +25,14 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"node-cron": "^3.0.3" "node-cron": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"eslint": "^9.25.1", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+7 -7
View File
@@ -1,5 +1,6 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import cron from "node-cron"; import type { ScheduledTask } from "node-cron";
import { schedule, validate } from "node-cron";
import { Stopwatch } from "@homarr/common"; import { Stopwatch } from "@homarr/common";
import type { MaybePromise } from "@homarr/common/types"; import type { MaybePromise } from "@homarr/common/types";
@@ -27,7 +28,7 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
options: CreateCronJobOptions, options: CreateCronJobOptions,
creatorOptions: CreateCronJobCreatorOptions<TAllowedNames>, creatorOptions: CreateCronJobCreatorOptions<TAllowedNames>,
) => { ) => {
const expectedMaximumDurationInMillis = options.expectedMaximumDurationInMillis ?? 1000; const expectedMaximumDurationInMillis = options.expectedMaximumDurationInMillis ?? 2500;
return (callback: () => MaybePromise<void>) => { return (callback: () => MaybePromise<void>) => {
const catchingCallbackAsync = async () => { const catchingCallbackAsync = async () => {
try { try {
@@ -64,12 +65,11 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
/** /**
* We are not using the runOnInit method as we want to run the job only once we start the cron job schedule manually. * We are not using the runOnInit method as we want to run the job only once we start the cron job schedule manually.
* This allows us to always run it once we start it. Additionally it will not run the callback if only the cron job file is imported. * This allows us to always run it once we start it. Additionally, it will not run the callback if only the cron job file is imported.
*/ */
let scheduledTask: cron.ScheduledTask | null = null; let scheduledTask: ScheduledTask | null = null;
if (cronExpression !== "never") { if (cronExpression !== "never") {
scheduledTask = cron.schedule(cronExpression, () => void catchingCallbackAsync(), { scheduledTask = schedule(cronExpression, () => void catchingCallbackAsync(), {
scheduled: false,
name, name,
timezone: creatorOptions.timezone, timezone: creatorOptions.timezone,
}); });
@@ -110,7 +110,7 @@ export const createCronJobCreator = <TAllowedNames extends string = string>(
options: CreateCronJobOptions = { runOnStart: false }, options: CreateCronJobOptions = { runOnStart: false },
) => { ) => {
creatorOptions.logger.logDebug(`Validating cron expression '${cronExpression}' for job: ${name}`); creatorOptions.logger.logDebug(`Validating cron expression '${cronExpression}' for job: ${name}`);
if (cronExpression !== "never" && !cron.validate(cronExpression)) { if (cronExpression !== "never" && !validate(cronExpression)) {
throw new Error(`Invalid cron expression '${cronExpression}' for job '${name}'`); throw new Error(`Invalid cron expression '${cronExpression}' for job '${name}'`);
} }
creatorOptions.logger.logDebug(`Cron job expression '${cronExpression}' for job ${name} is valid`); creatorOptions.logger.logDebug(`Cron job expression '${cronExpression}' for job ${name} is valid`);
+8 -8
View File
@@ -34,33 +34,33 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
options.logger.logInfo(`Starting schedule cron job ${job.name}.`); options.logger.logInfo(`Starting schedule cron job ${job.name}.`);
await job.onStartAsync(); await job.onStartAsync();
job.scheduledTask?.start(); await job.scheduledTask?.start();
}, },
startAllAsync: async () => { startAllAsync: async () => {
for (const job of jobRegistry.values()) { for (const job of jobRegistry.values()) {
options.logger.logInfo(`Starting schedule of cron job ${job.name}.`); options.logger.logInfo(`Starting schedule of cron job ${job.name}.`);
await job.onStartAsync(); await job.onStartAsync();
job.scheduledTask?.start(); await job.scheduledTask?.start();
} }
}, },
runManually: (name: keyof TJobs) => { runManuallyAsync: async (name: keyof TJobs) => {
const job = jobRegistry.get(name as string); const job = jobRegistry.get(name as string);
if (!job) return; if (!job) return;
options.logger.logInfo(`Running schedule cron job ${job.name} manually.`); options.logger.logInfo(`Running schedule cron job ${job.name} manually.`);
job.scheduledTask?.now(); await job.scheduledTask?.execute();
}, },
stop: (name: keyof TJobs) => { stopAsync: async (name: keyof TJobs) => {
const job = jobRegistry.get(name as string); const job = jobRegistry.get(name as string);
if (!job) return; if (!job) return;
options.logger.logInfo(`Stopping schedule cron job ${job.name}.`); options.logger.logInfo(`Stopping schedule cron job ${job.name}.`);
job.scheduledTask?.stop(); await job.scheduledTask?.stop();
}, },
stopAll: () => { stopAllAsync: async () => {
for (const job of jobRegistry.values()) { for (const job of jobRegistry.values()) {
options.logger.logInfo(`Stopping schedule cron job ${job.name}.`); options.logger.logInfo(`Stopping schedule cron job ${job.name}.`);
job.scheduledTask?.stop(); await job.scheduledTask?.stop();
} }
}, },
getJobRegistry() { getJobRegistry() {
+1 -1
View File
@@ -44,7 +44,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.28.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }
+2
View File
@@ -1,4 +1,5 @@
import { analyticsJob } from "./jobs/analytics"; import { analyticsJob } from "./jobs/analytics";
import { dockerContainersJob } from "./jobs/docker";
import { iconsUpdaterJob } from "./jobs/icons-updater"; import { iconsUpdaterJob } from "./jobs/icons-updater";
import { dnsHoleJob } from "./jobs/integrations/dns-hole"; import { dnsHoleJob } from "./jobs/integrations/dns-hole";
import { downloadsJob } from "./jobs/integrations/downloads"; import { downloadsJob } from "./jobs/integrations/downloads";
@@ -35,6 +36,7 @@ export const jobGroup = createCronJobGroup({
updateChecker: updateCheckerJob, updateChecker: updateCheckerJob,
mediaTranscoding: mediaTranscodingJob, mediaTranscoding: mediaTranscodingJob,
minecraftServerStatus: minecraftServerStatusJob, minecraftServerStatus: minecraftServerStatusJob,
dockerContainers: dockerContainersJob,
networkController: networkControllerJob, networkController: networkControllerJob,
}); });
+28
View File
@@ -0,0 +1,28 @@
import SuperJSON from "superjson";
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
import { db, eq } from "@homarr/db";
import { items } from "@homarr/db/schema";
import { logger } from "@homarr/log";
import { dockerContainersRequestHandler } from "@homarr/request-handler/docker";
import type { WidgetComponentProps } from "../../../widgets";
import { createCronJob } from "../lib";
export const dockerContainersJob = createCronJob("dockerContainers", EVERY_MINUTE).withCallback(async () => {
const dockerItems = await db.query.items.findMany({
where: eq(items.kind, "dockerContainers"),
});
await Promise.allSettled(
dockerItems.map(async (item) => {
try {
const options = SuperJSON.parse<WidgetComponentProps<"dockerContainers">["options"]>(item.options);
const innerHandler = dockerContainersRequestHandler.handler(options);
await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
} catch (error) {
logger.error("Failed to update Docker container status", { item, error });
}
}),
);
});
@@ -8,7 +8,9 @@ export const downloadsJob = createCronJob("downloads", EVERY_5_SECONDS).withCall
createRequestIntegrationJobHandler(downloadClientRequestHandler.handler, { createRequestIntegrationJobHandler(downloadClientRequestHandler.handler, {
widgetKinds: ["downloads"], widgetKinds: ["downloads"],
getInput: { getInput: {
downloads: () => ({}), downloads: (options) => ({
limit: options.limitPerIntegration,
}),
}, },
}), }),
); );
@@ -0,0 +1,6 @@
CREATE TABLE `trusted_certificate_hostname` (
`hostname` varchar(256) NOT NULL,
`thumbprint` varchar(128) NOT NULL,
`certificate` text NOT NULL,
CONSTRAINT `trusted_certificate_hostname_hostname_thumbprint_pk` PRIMARY KEY(`hostname`,`thumbprint`)
);
File diff suppressed because it is too large. Load diff
@@ -225,6 +225,13 @@
"when": 1740784837957, "when": 1740784837957,
"tag": "0031_add_dynamic_section_options", "tag": "0031_add_dynamic_section_options",
"breakpoints": true "breakpoints": true
},
{
"idx": 32,
"version": "5",
"when": 1746821770071,
"tag": "0032_add_trusted_certificate_hostnames",
"breakpoints": true
} }
] ]
} }
@@ -0,0 +1,6 @@
CREATE TABLE `trusted_certificate_hostname` (
`hostname` text NOT NULL,
`thumbprint` text NOT NULL,
`certificate` text NOT NULL,
PRIMARY KEY(`hostname`, `thumbprint`)
);
File diff suppressed because it is too large. Load diff
@@ -225,6 +225,13 @@
"when": 1740784849045, "when": 1740784849045,
"tag": "0031_add_dynamic_section_options", "tag": "0031_add_dynamic_section_options",
"breakpoints": true "breakpoints": true
},
{
"idx": 32,
"version": "6",
"when": 1746821779051,
"tag": "0032_add_trusted_certificate_hostnames",
"breakpoints": true
} }
] ]
} }
+8 -7
View File
@@ -38,19 +38,19 @@
}, },
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@auth/core": "^0.39.0", "@auth/core": "^0.39.1",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0", "@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^7.17.7", "@mantine/core": "^8.0.2",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^10.25.0", "@testcontainers/mysql": "^11.0.1",
"better-sqlite3": "^11.9.1", "better-sqlite3": "^11.10.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"drizzle-kit": "^0.31.0", "drizzle-kit": "^0.31.1",
"drizzle-orm": "^0.43.1", "drizzle-orm": "^0.44.2",
"drizzle-zod": "^0.7.1", "drizzle-zod": "^0.7.1",
"mysql2": "3.14.1" "mysql2": "3.14.1"
}, },
@@ -60,7 +60,8 @@
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"eslint": "^9.25.1", "esbuild": "^0.25.5",
"eslint": "^9.28.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"tsx": "4.19.4", "tsx": "4.19.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"
+1
View File
@@ -39,6 +39,7 @@ export const {
layouts, layouts,
itemLayouts, itemLayouts,
sectionLayouts, sectionLayouts,
trustedCertificateHostnames,
} = schema; } = schema;
export type User = InferSelectModel<typeof schema.users>; export type User = InferSelectModel<typeof schema.users>;
Loaded 100 of 262 files, more files were not shown because too many files have changed in this diff. Show more