Compare commits

...
115 Commits
Author SHA1 Message Date
Releases Homarr 3506f875e3 chore(release): 1.4.0 [skip ci] 2025-01-31 19:13:04 +00:00
homarr-releases[bot] e30e4b8cb7 chore(release): automatic release v1.4.0 2025-01-31 19:12:20 +00:00
homarr-renovate[bot] 9662e1abba fix(deps): update tiptap monorepo to v2.11.5 (#2203)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-31 09:33:25 +00:00
homarr-renovate[bot] e44c3a7bc8 fix(deps): update tanstack-query monorepo to ^5.66.0 (#2202)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-31 09:16:43 +00:00
homarr-renovate[bot] c287e9965f fix(deps): update dependency jotai to ^2.11.2 (#2201)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-31 02:14:23 +00:00
homarr-crowdin[bot] bea0124e3f chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-31 00:44:11 +00:00
Manuel d4154d259f fix: respect certificates for app ping (#2198) 2025-01-30 23:12:45 +01:00
Meier Lukas 77746278a3 fix(docker): missing production env variable (#2188) 2025-01-30 22:14:52 +01:00
Meier Lukas c75d4f0f58 fix: negative curve radius system health (#2197) 2025-01-30 22:14:26 +01:00
Meier Lukas 96fd5cc35e fix: use hostname for suspense query url (#2187) 2025-01-30 22:14:14 +01:00
Meier Lukas bd0b7c9518 fix: board crashes with empty string favicon (#2192) 2025-01-30 20:15:05 +01:00
homarr-renovate[bot] c05c51856b fix(deps): update tiptap monorepo to v2.11.4 (#2195)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-30 16:18:52 +00:00
homarr-crowdin[bot] dac1646ecc chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-30 00:43:11 +00:00
Yossi Hillali c19a4009f3 feat: weather widget - add wind speed, option to disable decimals for temperature & redesign dropdown forecast (#2099) 2025-01-29 23:02:13 +02:00
homarr-renovate[bot] 5657384d2a chore(deps): update dependency @types/swagger-ui-react to v5 (#2181)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-29 21:30:45 +01:00
homarr-renovate[bot] 4c72a34daf fix(deps): update dependency drizzle-orm to ^0.39.1 (#2186)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-29 16:39:45 +00:00
homarr-renovate[bot] de48ef21e9 chore(deps): update dependency @types/chroma-js to v3.1.1 (#2185)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-29 16:18:59 +00:00
homarr-renovate[bot] 37ccfab89a fix(deps): update dependency drizzle-kit to ^0.30.4 (#2183)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-29 15:43:18 +00:00
homarr-crowdin[bot] 07d60fe13e chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-29 00:44:09 +00:00
Meier Lukas bbd1b0c5d0 chore: remove node-options from example env 2025-01-28 22:26:44 +01:00
Meier Lukas c130e92661 chore(issue-template): add missing homarr version 1.3.1 2025-01-28 22:24:58 +01:00
Releases Homarr 0de853ff37 chore(release): 1.3.1 [skip ci] 2025-01-28 21:20:02 +00:00
homarr-releases[bot] a438a23f4e chore(release): automatic release v1.3.1 2025-01-28 21:19:19 +00:00
Manuel 8dd7fe297c fix: automatic search engine creation (#2172) 2025-01-28 22:18:31 +01:00
Manuel 780efab80c fix: app url line clamp (#2170) 2025-01-28 20:33:04 +01:00
homarr-renovate[bot] a4e9abcda1 fix(deps): update dependency swagger-ui-react to ^5.18.3 (#2164)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-28 13:00:40 +00:00
homarr-renovate[bot] 55b1afbb21 fix(deps): update tanstack-query monorepo to ^5.65.1 (#2161)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-28 09:17:32 +00:00
homarr-renovate[bot] b447fc9fe4 chore(deps): update dependency @types/node to ^22.12.0 (#2159)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-28 02:15:44 +00:00
homarr-crowdin[bot] 51ebaba399 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-28 00:44:31 +00:00
homarr-renovate[bot]andMeier Lukas 9d4aea2c41 fix(deps): update dependency typescript-eslint to ^8.22.0 (#2149)
* fix(deps): update dependency typescript-eslint to ^8.22.0

* fix: lint issues

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-01-27 22:32:23 +01:00
Meier Lukas 4dcdac6621 chore(issue-template): add missing homarr version 1.3.0 2025-01-27 21:22:18 +01:00
Releases Homarr 84bc242834 chore(release): 1.3.0 [skip ci] 2025-01-27 20:09:27 +00:00
homarr-releases[bot] f840f4c600 chore(release): automatic release v1.3.0 2025-01-27 20:08:24 +00:00
Manuel 5c8d7fcc15 feat: log body integration test failure (#2151) 2025-01-27 21:06:27 +01:00
Meier Lukas 93dc3ae97e fix: unable to update integration when encryption key changed (#2133) 2025-01-27 20:36:03 +01:00
Meier Lukas 7cb0aa70f1 feat(category): save collapse state for signed in users (#2134) 2025-01-27 20:34:50 +01:00
homarr-renovate[bot] 5c219a8b59 fix(deps): update testcontainers-node monorepo to ^10.17.2 (#2148)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-27 17:54:15 +00:00
homarr-renovate[bot] 50970e431d fix(deps): update tanstack-query monorepo to ^5.65.0 (#2146)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-27 17:33:14 +00:00
homarr-renovate[bot] af67bf26d6 fix(deps): update dependency drizzle-zod to ^0.7.0 (#2143)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-27 17:29:42 +00:00
homarr-renovate[bot] 0d5487366c fix(deps): update dependency drizzle-kit to ^0.30.3 (#2142)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-27 17:28:44 +00:00
homarr-renovate[bot] f0daef237c fix(deps): update dependency drizzle-orm to ^0.39.0 (#2144)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-27 11:53:35 +00:00
homarr-crowdin[bot] 54a62c49cf chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-27 00:45:54 +00:00
Meier Lukas 92f70f5a03 feat(user): add search in new tab preference (#2125) 2025-01-26 22:37:48 +01:00
Meier Lukas c43a2f0488 refactor: move zod import from validation package to zod (#2111)
* refactor: move zod import from validation package to zod

* refactor: move missing zod imports
2025-01-26 22:16:27 +01:00
Meier Lukas 1146c59a41 feat: add opacity for categories (#2132) 2025-01-26 21:26:42 +01:00
Meier Lukas af1f8eaf76 fix: websocket is not keep alive (#2131) 2025-01-26 21:26:21 +01:00
Meier Lukas dd2ca13004 fix: multi-text-input not saving on blur (#2128) 2025-01-26 21:26:08 +01:00
Meier Lukas b38de29a88 fix: app url not required (#2130) 2025-01-26 21:25:57 +01:00
Meier Lukas 2bb44e1666 fix: bookmarks show unauthorized when not logged in (#2123) 2025-01-26 21:25:04 +01:00
Meier Lukas 57b523fa80 fix: split up proxmox secrets are incorrectly migrated (#2124) 2025-01-26 21:24:50 +01:00
homarr-renovate[bot] ecbd2d14da chore(deps): update dependency @types/swagger-ui-react to ^4.19.0 (#2127)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-26 13:08:59 +00:00
homarr-renovate[bot] bbac4128eb fix(deps): update mantine monorepo to ^7.16.2 (#2129)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-26 12:56:37 +00:00
homarr-crowdin[bot]andMeier Lukas 5aaca9261f chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-01-26 10:58:49 +00:00
homarr-renovate[bot]andMeier Lukas 963d77463a fix(deps): update dependency @t3-oss/env-nextjs to ^0.12.0 (#2117)
* fix(deps): update dependency @t3-oss/env-nextjs to ^0.12.0

* fix: inconsistent tanstack versions

* fix: broken lock file

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-01-26 11:50:53 +01:00
Spillebulle b28274fa85 chore(lang): fix typo in english translation (#2112) 2025-01-25 16:34:56 +01:00
homarr-crowdin[bot] 3e85f4e44d chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-25 00:42:53 +00:00
Meier Lukas 614f8077e1 chore(issue-template): add missing homarr version 1.2.0 2025-01-24 20:22:11 +01:00
homarr-renovate[bot] b36b5c1eb1 chore(deps): update dependency eslint to ^9.19.0 (#2104)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-24 20:19:35 +01:00
Releases Homarr b4b19f486e chore(release): 1.2.0 [skip ci] 2025-01-24 19:12:53 +00:00
homarr-releases[bot] 32cf09a62e chore(release): automatic release v1.2.0 2025-01-24 19:11:55 +00:00
Meier Lukas a1ba4372da fix: system health monitoring gets proxmox id passed (#2094) 2025-01-24 17:36:57 +01:00
Meier Lukas 4e153b8046 fix: url-encode nzbget credentials (#2093) 2025-01-24 17:36:37 +01:00
Meier Lukas ac788650fb fix: issue with foreign key constraint do to empty string for home board (#2088) 2025-01-24 17:36:27 +01:00
homarr-crowdin[bot] a9b0e26c8d chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-24 00:44:37 +00:00
Yossi Hillali 132db15424 feat: add bookmarks options (apps) (#2048) 2025-01-23 22:10:16 +02:00
homarr-renovate[bot] 860b73fa3f chore(deps): update dependency @types/node to ^22.10.10 (#2091)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-23 18:20:56 +00:00
Yossi Hillali e4dd19b574 fix: date & time widget padding cut off (#2089) 2025-01-23 19:25:44 +02:00
homarr-renovate[bot] 1b4ed02010 chore(deps): update dependency @types/ws to ^8.5.14 (#2090)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-23 16:55:45 +00:00
homarr-renovate[bot] 47b7cbb086 chore(deps): update node.js to v22.13.1 (#2068)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-23 17:47:14 +01:00
homarr-renovate[bot] 7a6477a272 chore(deps): update vitest monorepo to ^3.0.4 (#2082)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-23 16:19:41 +00:00
homarr-renovate[bot] bff536008c chore(deps): update dependency @types/react to v19.0.8 (#2081)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-23 15:54:12 +00:00
homarr-renovate[bot] 8b040eddef fix(deps): update turbo monorepo to ^2.3.4 (#2085)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-23 15:45:42 +00:00
homarr-crowdin[bot] ce87d62b35 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-23 00:43:53 +00:00
Meier Lukas f88cc7381b chore(issue-template): add missing homarr versions 2025-01-22 21:00:59 +01:00
Releases Homarr cb54de524b chore(release): 1.1.0 [skip ci] 2025-01-22 19:46:18 +00:00
homarr-releases[bot] 7d8c7b9c2d chore(release): automatic release v1.1.0 2025-01-22 19:45:14 +00:00
Meier Lukas 73f7d885cd fix: docker hosts and ports env variable wrongly used (#2050)
* fix: docker hosts and ports env variable wrongly used

* fix: ci issues
2025-01-22 20:43:54 +01:00
Meier Lukas 470d27e091 fix: hide current language switch in other users preferences (#2069) 2025-01-22 20:06:40 +01:00
Meier Lukas 99c8e5b609 fix: add missing request handler for rss feeds (#2006)
* fix: add missing request handler for rss feeds

* fix: ci issues
2025-01-22 18:23:49 +01:00
Meier Lukas b413e2ec7d fix: change error when section for item not found to logged warning and remove item (#2044) 2025-01-22 18:18:14 +01:00
Meier Lukas 29d4f9e003 fix: entity state and execute automation cards aren't movable (#2043)
* fix: entity state and execute automation cards aren't movable

* fix: disable eslint issue
2025-01-22 18:17:15 +01:00
homarr-renovate[bot] 1f54bf5b9e fix(deps): update tiptap monorepo to v2.11.3 (#2067)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-22 16:39:48 +00:00
homarr-renovate[bot] 4d8c22a4e6 fix(deps): update nextjs monorepo to ^15.1.6 (#2063)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-22 11:16:22 +00:00
homarr-renovate[bot] 3e9e1e5e4f fix(deps): update dependency undici to v7.3.0 (#2061)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-22 10:35:59 +00:00
homarr-renovate[bot] 582b981e69 chore(deps): update dependency proxmox-api&gt;undici to v7.3.0 (#2060)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-22 10:15:15 +00:00
homarr-crowdin[bot] f8ec4a43cf chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-22 00:44:52 +00:00
homarr-renovate[bot] 2a8809551c fix(deps): update dependency @tabler/icons-react to ^3.29.0 (#2057)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-21 21:40:00 +00:00
homarr-renovate[bot] 9df59296ff chore(deps): update node.js to v22.13.1 (#2053)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-21 17:16:47 +00:00
homarr-renovate[bot] 701538fcb5 fix(deps): update testcontainers-node monorepo to ^10.17.1 (#2047)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-21 14:47:05 +00:00
homarr-renovate[bot] 0e82f2d199 chore(deps): update dependency testcontainers to ^10.17.0 (#2046)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-21 14:36:28 +00:00
homarr-renovate[bot] a2b5d60aa8 chore(deps): update vitest monorepo to ^3.0.3 (#2045)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-21 14:16:33 +00:00
Yossi Hillali 02dabfcd01 feat: add dropdown list for switching boards (#2042) 2025-01-21 14:34:41 +01:00
Meier Lukas 931f312291 fix: unable to move items with touch devices (#2038) 2025-01-21 13:54:44 +01:00
Meier Lukas 175927970b fix: empty env variables should be ignored (#2040)
* fix: empty env variables should be ignored

* fix: e2e test fails because of empty env variable values
2025-01-21 13:09:49 +01:00
Meier Lukas c922c5b083 fix: certificate card delete button not reachable caused by overflow (#2037) 2025-01-21 11:03:48 +01:00
Meier Lukas 73bbcfeee8 fix: url of batch link can get to long with 414 http code (#2036) 2025-01-21 11:03:16 +01:00
Meier Lukas 7a2d97ba91 fix: copy only working for https and localhost, adding open in new tab (#2035) 2025-01-21 11:03:03 +01:00
Meier Lukas e47d497560 chore: add stack and cause to trpc error logging (#2033) 2025-01-21 11:02:33 +01:00
Meier Lukas b9271ba14a chore: make error messages for auth errors more detailed (#2041) 2025-01-21 11:02:21 +01:00
Meier Lukas 9d85677046 feat: add support for calendar without integrations, add today mark (#2016)
* feat: add support for calendar without integrations, add today mark

* fix: ci issues
2025-01-21 11:01:52 +01:00
Meier Lukas b30c192b8f feat: add initial focus on new integration search (#2015) 2025-01-21 11:00:56 +01:00
Meier Lukas d4f04da709 fix: add item does not respect dynamic sections (#2010)
* bug: add item does not respect dynamic sections

* fix: deepsource issue
2025-01-21 11:00:29 +01:00
Meier Lukas 7960a4cea1 fix: unable to change home board in user preferences (#2011) 2025-01-21 10:59:04 +01:00
homarr-crowdin[bot] 243d7e9dd4 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-21 09:23:53 +00:00
Yossi Hillali bdefff636b fix: health monitoring cpu temp ring when no data, disks health when no data (#2029) 2025-01-20 21:43:18 +01:00
homarr-renovate[bot] 55feb9dff8 fix(deps): update dependency typescript-eslint to ^8.21.0 (#2032)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-20 17:33:27 +00:00
homarr-renovate[bot] 45af6442a1 fix(deps): update dependency jotai to ^2.11.1 (#2026)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-20 14:15:54 +00:00
homarr-crowdin[bot] 72debef4b4 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-20 05:32:12 +00:00
Meier Lukas 25635db17e chore: update homarr-docs-sitemap 2025-01-19 10:56:20 +01:00
homarr-renovate[bot] 1897752e38 fix(deps): update mantine monorepo to ^7.16.1 (#2014)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-19 09:53:43 +00:00
homarr-renovate[bot] 554853ffbd fix(deps): update tanstack-query monorepo to ^5.64.2 (#2013)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-19 08:53:10 +00:00
homarr-crowdin[bot] e40237e4a5 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-19 00:47:05 +00:00
SeDemalandMeier Lukas fa6262cf5e fix: User options switch on load value always false (#1995)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-01-18 08:48:07 +00:00
homarr-renovate[bot] 49bd02e869 fix(deps): update dependency better-sqlite3 to ^11.8.1 (#2005)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-18 06:52:55 +00:00
homarr-crowdin[bot] 3c8fbe371b chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-18 00:42:34 +00:00
255 changed files with 19551 additions and 4914 deletions

No files matched your search

+1 -4
View File
@@ -32,7 +32,4 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
# If it is used, please use the full path to the directory where the certificates are stored.
# LOCAL_CERTIFICATE_PATH='FULL_PATH_TO_CERTIFICATES'
TURBO_TELEMETRY_DISABLED=1
# Configure logging to use winston logger
NODE_OPTIONS='-r @homarr/log/override'
TURBO_TELEMETRY_DISABLED=1
+6 -1
View File
@@ -31,8 +31,13 @@ body:
label: Version
description: What version of Homarr are you running?
options:
- 1.0.0-beta
- 1.3.1
- 1.3.0
- 1.2.0
- 1.1.0
- 1.0.1
- 1.0.0
- 1.0.0-beta
- Other (describe in "additional information")
default: 0
validations:
+1 -1
View File
@@ -1 +1 @@
22.13.0
22.13.1
+104
View File
@@ -1,3 +1,107 @@
## [1.4.0](https://github.com/homarr-labs/homarr/compare/v1.3.1...v1.4.0) (2025-01-31)
### Features
* weather widget - add wind speed, option to disable decimals for temperature & redesign dropdown forecast ([#2099](https://github.com/homarr-labs/homarr/issues/2099)) ([c19a400](https://github.com/homarr-labs/homarr/commit/c19a4009f37ec2aba8d4edd2d19819605ecb9575))
### Bug Fixes
* board crashes with empty string favicon ([#2192](https://github.com/homarr-labs/homarr/issues/2192)) ([bd0b7c9](https://github.com/homarr-labs/homarr/commit/bd0b7c9518af88e0a329ea6286cb5ef97098782a))
* **deps:** update dependency drizzle-kit to ^0.30.4 ([#2183](https://github.com/homarr-labs/homarr/issues/2183)) ([37ccfab](https://github.com/homarr-labs/homarr/commit/37ccfab89a7843c65d6f354bef2b0537b20ea757))
* **deps:** update dependency drizzle-orm to ^0.39.1 ([#2186](https://github.com/homarr-labs/homarr/issues/2186)) ([4c72a34](https://github.com/homarr-labs/homarr/commit/4c72a34daf06c5e00fc4ece6d990148c1cd6d87f))
* **deps:** update dependency jotai to ^2.11.2 ([#2201](https://github.com/homarr-labs/homarr/issues/2201)) ([c287e99](https://github.com/homarr-labs/homarr/commit/c287e9965f008b6ac6f3bac56057b94c412a3779))
* **deps:** update tanstack-query monorepo to ^5.66.0 ([#2202](https://github.com/homarr-labs/homarr/issues/2202)) ([e44c3a7](https://github.com/homarr-labs/homarr/commit/e44c3a7bc8be5b67d78096d5daa53ad23ccdea5d))
* **deps:** update tiptap monorepo to v2.11.4 ([#2195](https://github.com/homarr-labs/homarr/issues/2195)) ([c05c518](https://github.com/homarr-labs/homarr/commit/c05c51856baff5222b0abda56e1c11d9bc9bd6f9))
* **deps:** update tiptap monorepo to v2.11.5 ([#2203](https://github.com/homarr-labs/homarr/issues/2203)) ([9662e1a](https://github.com/homarr-labs/homarr/commit/9662e1abba3e105fd549d25ee0a0efc48cf293a1))
* **docker:** missing production env variable ([#2188](https://github.com/homarr-labs/homarr/issues/2188)) ([7774627](https://github.com/homarr-labs/homarr/commit/77746278a36f2b9d39c780eada25195121cc0803))
* negative curve radius system health ([#2197](https://github.com/homarr-labs/homarr/issues/2197)) ([c75d4f0](https://github.com/homarr-labs/homarr/commit/c75d4f0f58136a0499a51494eb54c78c745f0b3f))
* respect certificates for app ping ([#2198](https://github.com/homarr-labs/homarr/issues/2198)) ([d4154d2](https://github.com/homarr-labs/homarr/commit/d4154d259fa50bfd754e7ea98b872df78864e014))
* use hostname for suspense query url ([#2187](https://github.com/homarr-labs/homarr/issues/2187)) ([96fd5cc](https://github.com/homarr-labs/homarr/commit/96fd5cc35e9ca85aba48670860e6bf2a4a2caaa9))
## [1.3.1](https://github.com/homarr-labs/homarr/compare/v1.3.0...v1.3.1) (2025-01-28)
### Bug Fixes
* app url line clamp ([#2170](https://github.com/homarr-labs/homarr/issues/2170)) ([780efab](https://github.com/homarr-labs/homarr/commit/780efab80cdc5446a6468d60afe2c1d9ecca0f1e))
* automatic search engine creation ([#2172](https://github.com/homarr-labs/homarr/issues/2172)) ([8dd7fe2](https://github.com/homarr-labs/homarr/commit/8dd7fe297c431bbe448bc47b381370ade7c4d3d9))
* **deps:** update dependency swagger-ui-react to ^5.18.3 ([#2164](https://github.com/homarr-labs/homarr/issues/2164)) ([a4e9abc](https://github.com/homarr-labs/homarr/commit/a4e9abcda192a96754078eece4343c2c1c9fa032))
* **deps:** update dependency typescript-eslint to ^8.22.0 ([#2149](https://github.com/homarr-labs/homarr/issues/2149)) ([9d4aea2](https://github.com/homarr-labs/homarr/commit/9d4aea2c4187ddda6a0484d0f6a0403c04961dde))
* **deps:** update tanstack-query monorepo to ^5.65.1 ([#2161](https://github.com/homarr-labs/homarr/issues/2161)) ([55b1afb](https://github.com/homarr-labs/homarr/commit/55b1afbb2167ccb6f5115cbb1291dfe561a5aa05))
## [1.3.0](https://github.com/homarr-labs/homarr/compare/v1.2.0...v1.3.0) (2025-01-27)
### Features
* add opacity for categories ([#2132](https://github.com/homarr-labs/homarr/issues/2132)) ([1146c59](https://github.com/homarr-labs/homarr/commit/1146c59a41ddd4b4cda14f6601437cd9bceb305a))
* **category:** save collapse state for signed in users ([#2134](https://github.com/homarr-labs/homarr/issues/2134)) ([7cb0aa7](https://github.com/homarr-labs/homarr/commit/7cb0aa70f11253aa4016d928f326eb71183626f6))
* log body integration test failure ([#2151](https://github.com/homarr-labs/homarr/issues/2151)) ([5c8d7fc](https://github.com/homarr-labs/homarr/commit/5c8d7fcc153ba4b94cd88c6ca8b22962d812b2b4))
* **user:** add search in new tab preference ([#2125](https://github.com/homarr-labs/homarr/issues/2125)) ([92f70f5](https://github.com/homarr-labs/homarr/commit/92f70f5a03048c7b269604f1ce33313ecf23a64b))
### Bug Fixes
* app url not required ([#2130](https://github.com/homarr-labs/homarr/issues/2130)) ([b38de29](https://github.com/homarr-labs/homarr/commit/b38de29a882ce26a9326f7f65403ea3916a58cac))
* bookmarks show unauthorized when not logged in ([#2123](https://github.com/homarr-labs/homarr/issues/2123)) ([2bb44e1](https://github.com/homarr-labs/homarr/commit/2bb44e16669f94d3d7500e828b1ab66b98c54bc7))
* **deps:** update dependency @t3-oss/env-nextjs to ^0.12.0 ([#2117](https://github.com/homarr-labs/homarr/issues/2117)) ([963d774](https://github.com/homarr-labs/homarr/commit/963d77463a5206ac1cb5d035b52271c6bbbd5e73))
* **deps:** update dependency drizzle-kit to ^0.30.3 ([#2142](https://github.com/homarr-labs/homarr/issues/2142)) ([0d54873](https://github.com/homarr-labs/homarr/commit/0d5487366c2616738b2542838f5cbd9c6d628973))
* **deps:** update dependency drizzle-orm to ^0.39.0 ([#2144](https://github.com/homarr-labs/homarr/issues/2144)) ([f0daef2](https://github.com/homarr-labs/homarr/commit/f0daef237c08c866f1f2aaefc113e70838b76be4))
* **deps:** update dependency drizzle-zod to ^0.7.0 ([#2143](https://github.com/homarr-labs/homarr/issues/2143)) ([af67bf2](https://github.com/homarr-labs/homarr/commit/af67bf26d674539abd7a0fdc54d7764d90f7007c))
* **deps:** update mantine monorepo to ^7.16.2 ([#2129](https://github.com/homarr-labs/homarr/issues/2129)) ([bbac412](https://github.com/homarr-labs/homarr/commit/bbac4128eb36d934310a83087c733338ae2b9b9c))
* **deps:** update tanstack-query monorepo to ^5.65.0 ([#2146](https://github.com/homarr-labs/homarr/issues/2146)) ([50970e4](https://github.com/homarr-labs/homarr/commit/50970e431d7654d2db43a4b1d55cc4f7d15a4ac2))
* **deps:** update testcontainers-node monorepo to ^10.17.2 ([#2148](https://github.com/homarr-labs/homarr/issues/2148)) ([5c219a8](https://github.com/homarr-labs/homarr/commit/5c219a8b595f90c53189569bd63fb85db8eab882))
* multi-text-input not saving on blur ([#2128](https://github.com/homarr-labs/homarr/issues/2128)) ([dd2ca13](https://github.com/homarr-labs/homarr/commit/dd2ca1300422a6ad477089efe905901515eebaf6))
* split up proxmox secrets are incorrectly migrated ([#2124](https://github.com/homarr-labs/homarr/issues/2124)) ([57b523f](https://github.com/homarr-labs/homarr/commit/57b523fa805de56d8795169a535e5e45d7c22ad7))
* unable to update integration when encryption key changed ([#2133](https://github.com/homarr-labs/homarr/issues/2133)) ([93dc3ae](https://github.com/homarr-labs/homarr/commit/93dc3ae97ed97691c304c9b6de9245930d5461ae))
* websocket is not keep alive ([#2131](https://github.com/homarr-labs/homarr/issues/2131)) ([af1f8ea](https://github.com/homarr-labs/homarr/commit/af1f8eaf76062a9fde5570097fc6a75742174d97))
## [1.2.0](https://github.com/homarr-labs/homarr/compare/v1.1.0...v1.2.0) (2025-01-24)
### Features
* add bookmarks options (apps) ([#2048](https://github.com/homarr-labs/homarr/issues/2048)) ([132db15](https://github.com/homarr-labs/homarr/commit/132db154242959dc2f16add4e58b0bdb7b96fb8c))
### Bug Fixes
* date & time widget padding cut off ([#2089](https://github.com/homarr-labs/homarr/issues/2089)) ([e4dd19b](https://github.com/homarr-labs/homarr/commit/e4dd19b574bfe0dcbcd92a3c5993aef550683fc3))
* **deps:** update turbo monorepo to ^2.3.4 ([#2085](https://github.com/homarr-labs/homarr/issues/2085)) ([8b040ed](https://github.com/homarr-labs/homarr/commit/8b040eddef8dbc1269489038bc503dcf23ba9580))
* issue with foreign key constraint do to empty string for home board ([#2088](https://github.com/homarr-labs/homarr/issues/2088)) ([ac78865](https://github.com/homarr-labs/homarr/commit/ac788650fb60d7aca889ec270dc29edb7ee53123))
* system health monitoring gets proxmox id passed ([#2094](https://github.com/homarr-labs/homarr/issues/2094)) ([a1ba437](https://github.com/homarr-labs/homarr/commit/a1ba4372da3430777a85dc83bbfc59f4b03d65fb))
* url-encode nzbget credentials ([#2093](https://github.com/homarr-labs/homarr/issues/2093)) ([4e153b8](https://github.com/homarr-labs/homarr/commit/4e153b804664e4fbc63ee5eb945c8096c809c6d5))
## [1.1.0](https://github.com/homarr-labs/homarr/compare/v1.0.1...v1.1.0) (2025-01-22)
### Features
* add dropdown list for switching boards ([#2042](https://github.com/homarr-labs/homarr/issues/2042)) ([02dabfc](https://github.com/homarr-labs/homarr/commit/02dabfcd01e372ada7d80a0501b708b171c8fbc6))
* add initial focus on new integration search ([#2015](https://github.com/homarr-labs/homarr/issues/2015)) ([b30c192](https://github.com/homarr-labs/homarr/commit/b30c192b8fbbbfcff8eb09b8f1654eb8dd90b56d))
* add support for calendar without integrations, add today mark ([#2016](https://github.com/homarr-labs/homarr/issues/2016)) ([9d85677](https://github.com/homarr-labs/homarr/commit/9d8567704670a9ab471ac3e246c5ba85f908cfd9))
### Bug Fixes
* add item does not respect dynamic sections ([#2010](https://github.com/homarr-labs/homarr/issues/2010)) ([d4f04da](https://github.com/homarr-labs/homarr/commit/d4f04da70910b6adbf3764de1499dbd46fe9b6d5))
* add missing request handler for rss feeds ([#2006](https://github.com/homarr-labs/homarr/issues/2006)) ([99c8e5b](https://github.com/homarr-labs/homarr/commit/99c8e5b609b17dfe443b35bfff2b883845dc62fe))
* certificate card delete button not reachable caused by overflow ([#2037](https://github.com/homarr-labs/homarr/issues/2037)) ([c922c5b](https://github.com/homarr-labs/homarr/commit/c922c5b08387b01136332876782fed1444203096))
* change error when section for item not found to logged warning and remove item ([#2044](https://github.com/homarr-labs/homarr/issues/2044)) ([b413e2e](https://github.com/homarr-labs/homarr/commit/b413e2ec7dd37c43665373be914ffa414599a206))
* copy only working for https and localhost, adding open in new tab ([#2035](https://github.com/homarr-labs/homarr/issues/2035)) ([7a2d97b](https://github.com/homarr-labs/homarr/commit/7a2d97ba9119e046c06365146fc4efd080f6f6df))
* **deps:** update dependency @tabler/icons-react to ^3.29.0 ([#2057](https://github.com/homarr-labs/homarr/issues/2057)) ([2a88095](https://github.com/homarr-labs/homarr/commit/2a8809551cfc4f4c09aa5c01cac05914fa913769))
* **deps:** update dependency better-sqlite3 to ^11.8.1 ([#2005](https://github.com/homarr-labs/homarr/issues/2005)) ([49bd02e](https://github.com/homarr-labs/homarr/commit/49bd02e869081ec325bd8b4ba2909603a82cd7df))
* **deps:** update dependency jotai to ^2.11.1 ([#2026](https://github.com/homarr-labs/homarr/issues/2026)) ([45af644](https://github.com/homarr-labs/homarr/commit/45af6442a1b84be399ddd281a17227a4184490be))
* **deps:** update dependency typescript-eslint to ^8.21.0 ([#2032](https://github.com/homarr-labs/homarr/issues/2032)) ([55feb9d](https://github.com/homarr-labs/homarr/commit/55feb9dff87088d7f6d006bcecc61b0435edbb6f))
* **deps:** update dependency undici to v7.3.0 ([#2061](https://github.com/homarr-labs/homarr/issues/2061)) ([3e9e1e5](https://github.com/homarr-labs/homarr/commit/3e9e1e5e4f737f8bdb1046585463c987fe43c3d5))
* **deps:** update mantine monorepo to ^7.16.1 ([#2014](https://github.com/homarr-labs/homarr/issues/2014)) ([1897752](https://github.com/homarr-labs/homarr/commit/1897752e3893804a5082f34b20929ef007825506))
* **deps:** update nextjs monorepo to ^15.1.6 ([#2063](https://github.com/homarr-labs/homarr/issues/2063)) ([4d8c22a](https://github.com/homarr-labs/homarr/commit/4d8c22a4e684b4f921967992d04d2461eb2e52db))
* **deps:** update tanstack-query monorepo to ^5.64.2 ([#2013](https://github.com/homarr-labs/homarr/issues/2013)) ([554853f](https://github.com/homarr-labs/homarr/commit/554853ffbda202eff559b5dafa330a15e2649f17))
* **deps:** update testcontainers-node monorepo to ^10.17.1 ([#2047](https://github.com/homarr-labs/homarr/issues/2047)) ([701538f](https://github.com/homarr-labs/homarr/commit/701538fcb5c546478f5d31b2869b933e7544cc3e))
* **deps:** update tiptap monorepo to v2.11.3 ([#2067](https://github.com/homarr-labs/homarr/issues/2067)) ([1f54bf5](https://github.com/homarr-labs/homarr/commit/1f54bf5b9e24b6d063f5ce9165c87d2d036fb8c6))
* docker hosts and ports env variable wrongly used ([#2050](https://github.com/homarr-labs/homarr/issues/2050)) ([73f7d88](https://github.com/homarr-labs/homarr/commit/73f7d885cddbbaa2ac879953fe5ee24a6abe582c))
* empty env variables should be ignored ([#2040](https://github.com/homarr-labs/homarr/issues/2040)) ([1759279](https://github.com/homarr-labs/homarr/commit/175927970b25648104fac5a31649471b3ebeaa9e))
* entity state and execute automation cards aren't movable ([#2043](https://github.com/homarr-labs/homarr/issues/2043)) ([29d4f9e](https://github.com/homarr-labs/homarr/commit/29d4f9e00313fe3e7ae207547cbd7a143b314535))
* health monitoring cpu temp ring when no data, disks health when no data ([#2029](https://github.com/homarr-labs/homarr/issues/2029)) ([bdefff6](https://github.com/homarr-labs/homarr/commit/bdefff636b06cb33020765e606bfe8e41c931722))
* hide current language switch in other users preferences ([#2069](https://github.com/homarr-labs/homarr/issues/2069)) ([470d27e](https://github.com/homarr-labs/homarr/commit/470d27e091a006af8a8c3ceccfc439902ebc5f18))
* unable to change home board in user preferences ([#2011](https://github.com/homarr-labs/homarr/issues/2011)) ([7960a4c](https://github.com/homarr-labs/homarr/commit/7960a4cea11d839f5a0d1889e98bb949c11b68fa))
* unable to move items with touch devices ([#2038](https://github.com/homarr-labs/homarr/issues/2038)) ([931f312](https://github.com/homarr-labs/homarr/commit/931f312291c0a6f1f22a26adf89dfbf912e4fe5e))
* url of batch link can get to long with 414 http code ([#2036](https://github.com/homarr-labs/homarr/issues/2036)) ([73bbcfe](https://github.com/homarr-labs/homarr/commit/73bbcfeee86bf0950145070a6a2ce528b708fe7c))
* User options switch on load value always false ([#1995](https://github.com/homarr-labs/homarr/issues/1995)) ([fa6262c](https://github.com/homarr-labs/homarr/commit/fa6262cf5e780fc3c5b445f988de9d9eb5b86ab5))
## [1.0.1](https://github.com/homarr-labs/homarr/compare/v1.0.0...v1.0.1) (2025-01-17)
### Bug Fixes
+2 -1
View File
@@ -1,4 +1,4 @@
FROM node:22.13.0-alpine AS base
FROM node:22.13.1-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
@@ -63,6 +63,7 @@ ENV DB_URL='/appdata/db/db.sqlite'
ENV DB_DIALECT='sqlite'
ENV DB_DRIVER='better-sqlite3'
ENV AUTH_PROVIDERS='credentials'
ENV NODE_ENV='production'
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD ["sh", "run.sh"]
+1
View File
@@ -2,6 +2,7 @@
import "@homarr/auth/env";
import "@homarr/db/env";
import "@homarr/common/env";
import "@homarr/docker/env";
import type { NextConfig } from "next";
import MillionLint from "@million/lint";
+25 -21
View File
@@ -23,8 +23,10 @@
"@homarr/cron-job-status": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/docker": "workspace:^0.1.0",
"@homarr/form": "workspace:^0.1.0",
"@homarr/gridstack": "^1.11.3",
"@homarr/gridstack": "^1.12.0",
"@homarr/icons": "workspace:^0.1.0",
"@homarr/integrations": "workspace:^0.1.0",
"@homarr/log": "workspace:^",
"@homarr/modals": "workspace:^0.1.0",
@@ -34,23 +36,24 @@
"@homarr/old-schema": "workspace:^0.1.0",
"@homarr/redis": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/settings": "workspace:^0.1.0",
"@homarr/spotlight": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "^7.16.0",
"@mantine/core": "^7.16.0",
"@mantine/dropzone": "^7.16.0",
"@mantine/hooks": "^7.16.0",
"@mantine/modals": "^7.16.0",
"@mantine/tiptap": "^7.16.0",
"@mantine/colors-generator": "^7.16.2",
"@mantine/core": "^7.16.2",
"@mantine/dropzone": "^7.16.2",
"@mantine/hooks": "^7.16.2",
"@mantine/modals": "^7.16.2",
"@mantine/tiptap": "^7.16.2",
"@million/lint": "1.0.14",
"@t3-oss/env-nextjs": "^0.11.1",
"@tabler/icons-react": "^3.28.1",
"@tanstack/react-query": "^5.64.1",
"@tanstack/react-query-devtools": "^5.64.1",
"@tanstack/react-query-next-experimental": "5.64.1",
"@t3-oss/env-nextjs": "^0.12.0",
"@tabler/icons-react": "^3.29.0",
"@tanstack/react-query": "^5.66.0",
"@tanstack/react-query-devtools": "^5.66.0",
"@tanstack/react-query-next-experimental": "^5.66.0",
"@trpc/client": "next",
"@trpc/next": "next",
"@trpc/react-query": "next",
@@ -64,9 +67,9 @@
"dotenv": "^16.4.7",
"flag-icons": "^7.3.2",
"glob": "^11.0.1",
"jotai": "^2.11.0",
"jotai": "^2.11.2",
"mantine-react-table": "2.0.0-beta.8",
"next": "15.1.5",
"next": "15.1.6",
"postcss-preset-mantine": "^1.17.0",
"prismjs": "^1.29.0",
"react": "19.0.0",
@@ -75,21 +78,22 @@
"react-simple-code-editor": "^0.14.1",
"sass": "^1.83.4",
"superjson": "2.2.2",
"swagger-ui-react": "^5.18.2",
"use-deep-compare-effect": "^1.8.1"
"swagger-ui-react": "^5.18.3",
"use-deep-compare-effect": "^1.8.1",
"zod": "^3.24.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.0",
"@types/node": "^22.10.7",
"@types/chroma-js": "3.1.1",
"@types/node": "^22.12.0",
"@types/prismjs": "^1.26.5",
"@types/react": "19.0.7",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"@types/swagger-ui-react": "^4.18.3",
"@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.1.2",
"eslint": "^9.18.0",
"eslint": "^9.19.0",
"node-loader": "^2.1.0",
"prettier": "^3.4.2",
"typescript": "^5.7.3"
@@ -18,8 +18,8 @@ import superjson from "superjson";
import type { SuperJSONResult } from "superjson";
import type { AppRouter } from "@homarr/api";
import { clientApi, getTrpcUrl } from "@homarr/api/client";
import { createHeadersCallbackForSource } from "@homarr/api/shared";
import { clientApi } from "@homarr/api/client";
import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
import { env } from "~/env";
@@ -94,6 +94,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
false: unstable_httpBatchStreamLink({
transformer: superjson,
url: getTrpcUrl(),
maxURLLength: 2083, // Suggested by tRPC: https://trpc.io/docs/client/links/httpBatchLink#setting-a-maximum-url-length
headers: createHeadersCallbackForSource("nextjs-react (json)"),
}),
}),
@@ -2,13 +2,13 @@
import { useRouter } from "next/navigation";
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
import type { z } from "zod";
import { clientApi } from "@homarr/api/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
interface RegistrationFormProps {
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Anchor, Button, Card, Code, Collapse, Divider, PasswordInput, Stack, Text, TextInput } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { z } from "zod";
import { signIn } from "@homarr/auth/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
@@ -12,7 +13,7 @@ import type { useForm } from "@homarr/form";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { validation, z } from "@homarr/validation";
import { validation } from "@homarr/validation";
interface LoginFormProps {
providers: string[];
@@ -7,6 +7,7 @@ import "~/styles/gridstack.scss";
import { IntegrationProvider } from "@homarr/auth/client";
import { auth } from "@homarr/auth/next";
import { getIntegrationsWithPermissionsAsync } from "@homarr/auth/server";
import { isNullOrWhitespace } from "@homarr/common";
import { getI18n } from "@homarr/translation/server";
import { createMetaTitle } from "~/metadata";
@@ -48,11 +49,13 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
return {
title: board.metaTitle ?? createMetaTitle(t("board.content.metaTitle", { boardName: board.name })),
icons: {
icon: board.faviconImageUrl ? board.faviconImageUrl : undefined,
apple: board.faviconImageUrl ? board.faviconImageUrl : undefined,
icon: !isNullOrWhitespace(board.faviconImageUrl) ? board.faviconImageUrl : undefined,
apple: !isNullOrWhitespace(board.faviconImageUrl) ? board.faviconImageUrl : undefined,
},
appleWebApp: {
startupImage: { url: board.faviconImageUrl ? board.faviconImageUrl : "/logo/logo.png" },
startupImage: {
url: !isNullOrWhitespace(board.faviconImageUrl) ? board.faviconImageUrl : "/logo/logo.png",
},
},
};
} catch (error) {
@@ -2,6 +2,7 @@
import type { MouseEvent } from "react";
import { useCallback, useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Group, Menu } from "@mantine/core";
import { useHotkeys } from "@mantine/hooks";
@@ -9,9 +10,11 @@ import {
IconBox,
IconBoxAlignTop,
IconChevronDown,
IconLayoutBoard,
IconPencil,
IconPencilOff,
IconPlus,
IconReplace,
IconResize,
IconSettings,
} from "@tabler/icons-react";
@@ -49,6 +52,8 @@ export const BoardContentHeaderActions = () => {
<HeaderButton href={`/boards/${board.name}/settings`}>
<IconSettings stroke={1.5} />
</HeaderButton>
<SelectBoardsMenu />
</>
);
};
@@ -151,6 +156,32 @@ const EditModeMenu = () => {
);
};
const SelectBoardsMenu = () => {
const { data: boards = [] } = clientApi.board.getAllBoards.useQuery();
return (
<Menu position="bottom-end" withArrow>
<Menu.Target>
<HeaderButton w="auto" px={4}>
<IconReplace stroke={1.5} />
</HeaderButton>
</Menu.Target>
<Menu.Dropdown style={{ transform: "translate(-7px, 0)" }}>
{boards.map((board) => (
<Menu.Item
key={board.id}
component={Link}
href={`/boards/${board.name}`}
leftSection={<IconLayoutBoard size={20} />}
>
{board.name}
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
);
};
const usePreventLeaveWithDirty = (isDirty: boolean) => {
const t = useI18n();
const { openConfirmModal } = useConfirmModal();
@@ -2,12 +2,12 @@
import { Button, Card, Stack, TextInput } from "@mantine/core";
import { IconArrowRight } from "@tabler/icons-react";
import type { z } from "zod";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
export const InitGroup = () => {
@@ -3,6 +3,7 @@
import { startTransition } from "react";
import { Button, Card, Group, Stack, Switch, Text } from "@mantine/core";
import { IconArrowRight } from "@tabler/icons-react";
import type { z } from "zod";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
@@ -11,7 +12,6 @@ import type { CheckboxProps } from "@homarr/form/types";
import { defaultServerSettings } from "@homarr/server-settings";
import type { TranslationObject } from "@homarr/translation";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
export const InitSettings = () => {
@@ -1,6 +1,7 @@
"use client";
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
import type { z } from "zod";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
@@ -8,7 +9,6 @@ import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
export const InitUserForm = () => {
+19
View File
@@ -9,10 +9,14 @@ import "~/styles/scroll-area.scss";
import { notFound } from "next/navigation";
import { NextIntlClientProvider } from "next-intl";
import { api } from "@homarr/api/server";
import { env } from "@homarr/auth/env";
import { auth } from "@homarr/auth/next";
import { db } from "@homarr/db";
import { getServerSettingsAsync } from "@homarr/db/queries";
import { ModalProvider } from "@homarr/modals";
import { Notifications } from "@homarr/notifications";
import { SettingsProvider } from "@homarr/settings";
import { SpotlightProvider } from "@homarr/spotlight";
import type { SupportedLanguage } from "@homarr/translation";
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
@@ -73,6 +77,8 @@ export default async function Layout(props: {
}
const session = await auth();
const user = session ? await api.user.getById({ userId: session.user.id }).catch(() => null) : null;
const serverSettings = await getServerSettingsAsync(db);
const colorScheme = await getCurrentColorSchemeAsync();
const direction = isLocaleRTL((await props.params).locale) ? "rtl" : "ltr";
const i18nMessages = await getI18nMessages();
@@ -81,6 +87,19 @@ export default async function Layout(props: {
(innerProps) => {
return <AuthProvider session={session} logoutUrl={env.AUTH_LOGOUT_REDIRECT_URL} {...innerProps} />;
},
(innerProps) => (
<SettingsProvider
user={user}
serverSettings={{
board: {
homeBoardId: serverSettings.board.homeBoardId,
mobileHomeBoardId: serverSettings.board.mobileHomeBoardId,
},
search: { defaultSearchEngineId: serverSettings.search.defaultSearchEngineId },
}}
{...innerProps}
/>
),
(innerProps) => <JotaiProvider {...innerProps} />,
(innerProps) => <TRPCReactProvider {...innerProps} />,
(innerProps) => <DayJsLoader {...innerProps} />,
@@ -2,11 +2,11 @@
import Link from "next/link";
import { Button, Group, Stack, Textarea, TextInput } from "@mantine/core";
import type { z } from "zod";
import { useZodForm } from "@homarr/form";
import type { TranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
import { IconPicker } from "~/components/icons/picker/icon-picker";
@@ -25,11 +25,11 @@ export const AppForm = (props: AppFormProps) => {
const t = useI18n();
const form = useZodForm(validation.app.manage, {
initialValues: initialValues ?? {
name: "",
description: "",
iconUrl: "",
href: "",
initialValues: {
name: initialValues?.name ?? "",
description: initialValues?.description ?? "",
iconUrl: initialValues?.iconUrl ?? "",
href: initialValues?.href ?? "",
},
});
@@ -2,6 +2,7 @@
import { useCallback } from "react";
import { useRouter } from "next/navigation";
import type { z } from "zod";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
@@ -9,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { TranslationFunction } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client";
import type { validation, z } from "@homarr/validation";
import type { validation } from "@homarr/validation";
import { AppForm } from "../../_form";
@@ -2,13 +2,14 @@
import { useCallback } from "react";
import { useRouter } from "next/navigation";
import type { z } from "zod";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { TranslationFunction } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client";
import type { validation, z } from "@homarr/validation";
import type { validation } from "@homarr/validation";
import { AppForm } from "../_form";
@@ -2,6 +2,7 @@ import Link from "next/link";
import { redirect } from "next/navigation";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
import { IconBox, IconPencil } from "@tabler/icons-react";
import { z } from "zod";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
@@ -9,7 +10,6 @@ import { auth } from "@homarr/auth/next";
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { SearchInput, TablePagination } from "@homarr/ui";
import { z } from "@homarr/validation";
import { ManageContainer } from "~/components/manage/manage-container";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
@@ -80,7 +80,7 @@ const AppCard = async ({ app }: AppCardProps) => {
return (
<Card withBorder>
<Group justify="space-between" wrap="nowrap">
<Group align="top" justify="start" wrap="nowrap">
<Group align="top" justify="start" wrap="nowrap" style={{ flex: "1" }}>
<Avatar
size="sm"
src={app.iconUrl}
@@ -91,7 +91,7 @@ const AppCard = async ({ app }: AppCardProps) => {
},
}}
/>
<Stack gap={0}>
<Stack gap={0} style={{ flex: "1" }}>
<Text fw={500} lineClamp={1}>
{app.name}
</Text>
@@ -101,7 +101,7 @@ const AppCard = async ({ app }: AppCardProps) => {
</Text>
)}
{app.href && (
<Anchor href={app.href} lineClamp={1} size="sm" w="min-content">
<Anchor href={app.href} lineClamp={1} size="sm" style={{ wordBreak: "break-all" }}>
{app.href}
</Anchor>
)}
@@ -3,6 +3,7 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Button, Fieldset, Group, Stack, TextInput } from "@mantine/core";
import type { z } from "zod";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
@@ -13,7 +14,6 @@ import { convertIntegrationTestConnectionError } from "@homarr/integrations/clie
import { useConfirmModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
import { SecretCard } from "../../_components/secrets/integration-secret-card";
@@ -70,9 +70,9 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
if (testConnectionError) {
showErrorNotification({
title: t(`integration.testConnection.notification.${testConnectionError.key}.title`),
message: testConnectionError.message
? testConnectionError.message
: t(`integration.testConnection.notification.${testConnectionError.key}.message`),
message:
testConnectionError.message ??
t(`integration.testConnection.notification.${testConnectionError.key}.message`),
});
return;
}
@@ -15,7 +15,9 @@ export const IntegrationCreateDropdownContent = () => {
const [search, setSearch] = useState("");
const filteredKinds = useMemo(() => {
return integrationKinds.filter((kind) => kind.includes(search.toLowerCase()));
return integrationKinds.filter((kind) =>
getIntegrationName(kind).toLowerCase().includes(search.toLowerCase().trim()),
);
}, [search]);
const handleSearch = React.useCallback(
@@ -29,6 +31,7 @@ export const IntegrationCreateDropdownContent = () => {
leftSection={<IconSearch stroke={1.5} size={20} />}
placeholder={t("integration.page.list.search")}
value={search}
data-autofocus
onChange={handleSearch}
/>
@@ -5,6 +5,7 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import { Alert, Button, Checkbox, Fieldset, Group, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";
import type { z } from "zod";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
@@ -15,7 +16,6 @@ import { useZodForm } from "@homarr/form";
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
import { IntegrationSecretInput } from "../_components/secrets/integration-secret-inputs";
@@ -63,9 +63,9 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
if (testConnectionError) {
showErrorNotification({
title: t(`integration.testConnection.notification.${testConnectionError.key}.title`),
message: testConnectionError.message
? testConnectionError.message
: t(`integration.testConnection.notification.${testConnectionError.key}.message`),
message:
testConnectionError.message ??
t(`integration.testConnection.notification.${testConnectionError.key}.message`),
});
return;
}
@@ -1,5 +1,6 @@
import { notFound } from "next/navigation";
import { Container, Group, Stack, Title } from "@mantine/core";
import { z } from "zod";
import { auth } from "@homarr/auth/next";
import type { IntegrationKind } from "@homarr/definitions";
@@ -7,7 +8,6 @@ import { getIntegrationName, integrationKinds } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { IntegrationAvatar } from "@homarr/ui";
import type { validation } from "@homarr/validation";
import { z } from "@homarr/validation";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NewIntegrationForm } from "./_integration-new-form";
@@ -102,7 +102,15 @@ export default async function IntegrationsPage(props: IntegrationsPageProps) {
const IntegrationSelectMenu = ({ children }: PropsWithChildren) => {
return (
<Menu width={256} trapFocus position="bottom-end" withinPortal shadow="md" keepMounted={false}>
<Menu
width={256}
trapFocus
position="bottom-end"
withinPortal
shadow="md"
keepMounted={false}
withInitialFocusPlaceholder={false}
>
{children}
<MenuDropdown>
<IntegrationCreateDropdownContent />
@@ -15,6 +15,11 @@ export const CopyMedia = ({ media }: CopyMediaProps) => {
const url = typeof window !== "undefined" ? `${window.location.origin}/api/user-medias/${media.id}` : "";
// Clipboard only works on localhost or secure connections (https)
if (url.startsWith("http://") && !url.startsWith("http://localhost")) {
return null;
}
return (
<CopyButton value={url}>
{({ copy, copied }) => (
@@ -1,16 +1,31 @@
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Anchor, Group, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from "@mantine/core";
import {
ActionIcon,
Anchor,
Group,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Title,
Tooltip,
} from "@mantine/core";
import { IconExternalLink } from "@tabler/icons-react";
import { z } from "zod";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { humanFileSize } from "@homarr/common";
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { createLocalImageUrl } from "@homarr/icons/local";
import { getI18n } from "@homarr/translation/server";
import { SearchInput, TablePagination, UserAvatar } from "@homarr/ui";
import { z } from "@homarr/validation";
import { ManageContainer } from "~/components/manage/manage-container";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
@@ -91,13 +106,14 @@ interface RowProps {
const Row = async ({ media }: RowProps) => {
const session = await auth();
const t = await getI18n();
const canDelete = media.creatorId === session?.user.id || session?.user.permissions.includes("media-full-all");
return (
<TableTr>
<TableTd w={64}>
<Image
src={`/api/user-medias/${media.id}`}
src={createLocalImageUrl(media.id)}
alt={media.name}
width={64}
height={64}
@@ -121,6 +137,17 @@ const Row = async ({ media }: RowProps) => {
<TableTd w={64}>
<Group wrap="nowrap" gap="xs">
<CopyMedia media={media} />
<Tooltip label={t("media.action.open.label")} openDelay={500}>
<ActionIcon
component="a"
href={createLocalImageUrl(media.id)}
target="_blank"
color="gray"
variant="subtle"
>
<IconExternalLink size={16} stroke={1.5} />
</ActionIcon>
</Tooltip>
{canDelete && <DeleteMedia media={media} />}
</Group>
</TableTd>
@@ -4,13 +4,13 @@ import Link from "next/link";
import type { SegmentedControlItem } from "@mantine/core";
import { Button, Fieldset, Grid, Group, SegmentedControl, Stack, Textarea, TextInput } from "@mantine/core";
import { WidgetIntegrationSelect } from "node_modules/@homarr/widgets/src/widget-integration-select";
import type { z } from "zod";
import { clientApi } from "@homarr/api/client";
import { searchEngineTypes } from "@homarr/definitions";
import { useZodForm } from "@homarr/form";
import type { TranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
import { IconPicker } from "~/components/icons/picker/icon-picker";
@@ -2,6 +2,7 @@
import { useCallback } from "react";
import { useRouter } from "next/navigation";
import type { z } from "zod";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
@@ -9,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { TranslationFunction } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client";
import type { validation, z } from "@homarr/validation";
import type { validation } from "@homarr/validation";
import { SearchEngineForm } from "../../_form";
@@ -2,13 +2,14 @@
import { useCallback } from "react";
import { useRouter } from "next/navigation";
import type { z } from "zod";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { TranslationFunction } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client";
import type { validation, z } from "@homarr/validation";
import type { validation } from "@homarr/validation";
import { SearchEngineForm } from "../_form";
@@ -2,6 +2,7 @@ import Link from "next/link";
import { redirect } from "next/navigation";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
import { IconPencil, IconSearch } from "@tabler/icons-react";
import { z } from "zod";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
@@ -9,7 +10,6 @@ import { auth } from "@homarr/auth/next";
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { SearchInput, TablePagination } from "@homarr/ui";
import { z } from "@homarr/validation";
import { ManageContainer } from "~/components/manage/manage-container";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
@@ -2,6 +2,7 @@
import { Button, FileInput, Group, Stack } 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";
@@ -9,7 +10,7 @@ import { useZodForm } from "@homarr/form";
import { createModal, useModalAction } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { superRefineCertificateFile, z } from "@homarr/validation";
import { superRefineCertificateFile } from "@homarr/validation";
export const AddCertificateButton = () => {
const { openModal } = useModalAction(AddCertificateModal);
@@ -59,10 +59,17 @@ export default async function CertificatesPage({ params }: CertificatesPageProps
{x509Certificates.map((cert) => (
<Card key={cert.x509.fingerprint} withBorder>
<Group wrap="nowrap">
<IconCertificate color={getMantineColor(iconColor(cert.x509.validToDate), 6)} size={32} stroke={1.5} />
<Stack flex={1} gap="xs">
<Group justify="space-between">
<Text fw={500}>{cert.x509.subject}</Text>
<IconCertificate
color={getMantineColor(iconColor(cert.x509.validToDate), 6)}
style={{ minWidth: 32 }}
size={32}
stroke={1.5}
/>
<Stack flex={1} gap="xs" maw="calc(100% - 48px)">
<Group justify="space-between" wrap="nowrap">
<Text fw={500} lineClamp={1} style={{ wordBreak: "break-all" }}>
{cert.x509.subject}
</Text>
<Text c="gray.6" ta="end" size="sm">
{cert.fileName}
</Text>
@@ -16,7 +16,7 @@ import { MantineReactTable } from "mantine-react-table";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useTimeAgo } from "@homarr/common";
import type { DockerContainerState } from "@homarr/definitions";
import type { ContainerState } from "@homarr/docker";
import { useModalAction } from "@homarr/modals";
import { AddDockerAppToHomarr } from "@homarr/modals-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
@@ -246,9 +246,9 @@ const containerStates = {
exited: "red",
removing: "pink",
dead: "dark",
} satisfies Record<DockerContainerState, MantineColor>;
} satisfies Record<ContainerState, MantineColor>;
const ContainerStateBadge = ({ state }: { state: DockerContainerState }) => {
const ContainerStateBadge = ({ state }: { state: ContainerState }) => {
const t = useScopedI18n("docker.field.state.option");
return (
@@ -1,6 +1,7 @@
"use client";
import { Button, Group, Select, Stack } from "@mantine/core";
import type { z } from "zod";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
@@ -8,7 +9,6 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
interface ChangeHomeBoardFormProps {
@@ -39,8 +39,8 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
});
const form = useZodForm(validation.user.changeHomeBoards, {
initialValues: {
homeBoardId: user.homeBoardId ?? "",
mobileHomeBoardId: user.mobileHomeBoardId ?? "",
homeBoardId: user.homeBoardId,
mobileHomeBoardId: user.mobileHomeBoardId,
},
});
@@ -1,6 +1,7 @@
"use client";
import { Button, Group, Select, Stack } from "@mantine/core";
import { Button, Group, Select, Stack, Switch } from "@mantine/core";
import type { z } from "zod";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
@@ -8,37 +9,38 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
interface ChangeDefaultSearchEngineFormProps {
interface ChangeSearchPreferencesFormProps {
user: RouterOutputs["user"]["getById"];
searchEnginesData: { value: string; label: string }[];
}
export const ChangeDefaultSearchEngineForm = ({ user, searchEnginesData }: ChangeDefaultSearchEngineFormProps) => {
export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeSearchPreferencesFormProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changeDefaultSearchEngine.useMutation({
const { mutate, isPending } = clientApi.user.changeSearchPreferences.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
defaultSearchEngineId: variables.defaultSearchEngineId,
openInNewTab: variables.openInNewTab,
});
showSuccessNotification({
message: t("user.action.changeDefaultSearchEngine.notification.success.message"),
message: t("user.action.changeSearchPreferences.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("user.action.changeDefaultSearchEngine.notification.error.message"),
message: t("user.action.changeSearchPreferences.notification.error.message"),
});
},
});
const form = useZodForm(validation.user.changeDefaultSearchEngine, {
const form = useZodForm(validation.user.changeSearchPreferences, {
initialValues: {
defaultSearchEngineId: user.defaultSearchEngineId ?? "",
defaultSearchEngineId: user.defaultSearchEngineId,
openInNewTab: user.openSearchInNewTab,
},
});
@@ -52,7 +54,16 @@ export const ChangeDefaultSearchEngineForm = ({ user, searchEnginesData }: Chang
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Select w="100%" data={searchEnginesData} {...form.getInputProps("defaultSearchEngineId")} />
<Select
label={t("user.field.defaultSearchEngine.label")}
w="100%"
data={searchEnginesData}
{...form.getInputProps("defaultSearchEngineId")}
/>
<Switch
label={t("user.field.openSearchInNewTab.label")}
{...form.getInputProps("openInNewTab", { type: "checkbox" })}
/>
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
@@ -64,4 +75,4 @@ export const ChangeDefaultSearchEngineForm = ({ user, searchEnginesData }: Chang
);
};
type FormType = z.infer<typeof validation.user.changeDefaultSearchEngine>;
type FormType = z.infer<typeof validation.user.changeSearchPreferences>;
@@ -4,6 +4,7 @@ import { Button, Group, Radio, Stack } from "@mantine/core";
import type { DayOfWeek } from "@mantine/dates";
import dayjs from "dayjs";
import localeData from "dayjs/plugin/localeData";
import type { z } from "zod";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
@@ -11,7 +12,6 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
dayjs.extend(localeData);
@@ -1,6 +1,7 @@
"use client";
import { Button, Group, Stack, Switch } from "@mantine/core";
import type { z } from "zod";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
@@ -8,7 +9,6 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
interface PingIconsEnabledProps {
@@ -51,7 +51,10 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Switch {...form.getInputProps("pingIconsEnabled")} label={t("user.field.pingIconsEnabled.label")} />
<Switch
{...form.getInputProps("pingIconsEnabled", { type: "checkbox" })}
label={t("user.field.pingIconsEnabled.label")}
/>
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
@@ -11,8 +11,8 @@ import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone"
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
import { createMetaTitle } from "~/metadata";
import { canAccessUserEditPage } from "../access";
import { ChangeDefaultSearchEngineForm } from "./_components/_change-default-search-engine";
import { ChangeHomeBoardForm } from "./_components/_change-home-board";
import { ChangeSearchPreferencesForm } from "./_components/_change-search-preferences";
import { DeleteUserButton } from "./_components/_delete-user-button";
import { FirstDayOfWeek } from "./_components/_first-day-of-week";
import { PingIconsEnabled } from "./_components/_ping-icons-enabled";
@@ -83,10 +83,12 @@ export default async function EditUserPage(props: Props) {
</Box>
</Group>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.language")}</Title>
<CurrentLanguageCombobox />
</Stack>
{session?.user.id === user.id && (
<Stack mb="lg">
<Title order={2}>{tGeneral("item.language")}</Title>
<CurrentLanguageCombobox />
</Stack>
)}
<Stack mb="lg">
<Title order={2}>{tGeneral("item.board.title")}</Title>
@@ -100,8 +102,8 @@ export default async function EditUserPage(props: Props) {
</Stack>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.defaultSearchEngine")}</Title>
<ChangeDefaultSearchEngineForm user={user} searchEnginesData={searchEngines} />
<Title order={2}>{tGeneral("item.search")}</Title>
<ChangeSearchPreferencesForm user={user} searchEnginesData={searchEngines} />
</Stack>
<Stack mb="lg">
@@ -17,6 +17,7 @@ import {
} from "@mantine/core";
import { useListState } from "@mantine/hooks";
import { IconPlus, IconUserCheck } from "@tabler/icons-react";
import { z } from "zod";
import { clientApi } from "@homarr/api/client";
import { everyoneGroup, groupPermissions } from "@homarr/definitions";
@@ -26,7 +27,7 @@ import { useModalAction } from "@homarr/modals";
import { showErrorNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput, UserAvatar } from "@homarr/ui";
import { validation, z } from "@homarr/validation";
import { validation } from "@homarr/validation";
import { createCustomErrorParams } from "@homarr/validation/form";
import { GroupSelectModal } from "~/components/access/group-select-modal";
@@ -1,6 +1,7 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { Anchor, Group, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from "@mantine/core";
import { z } from "zod";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
@@ -8,7 +9,6 @@ import { auth } from "@homarr/auth/next";
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { getI18n } from "@homarr/translation/server";
import { SearchInput, TablePagination, UserAvatarGroup } from "@homarr/ui";
import { z } from "@homarr/validation";
import { ManageContainer } from "~/components/manage/manage-container";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
+5 -2
View File
@@ -1,6 +1,7 @@
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter, createTRPCContext } from "@homarr/api";
import { trpcPath } from "@homarr/api/shared";
import { auth } from "@homarr/auth/next";
import { logger } from "@homarr/log";
@@ -25,12 +26,14 @@ export function OPTIONS() {
const handler = auth(async (req) => {
const response = await fetchRequestHandler({
endpoint: "/api/trpc",
endpoint: trpcPath,
router: appRouter,
req,
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
onError({ error, path, type }) {
logger.error(`tRPC Error with ${type} on '${path}': (${error.code}) - ${error.message}`);
logger.error(
`tRPC Error with ${type} on '${path}': (${error.code}) - ${error.message}\n${error.stack}\n${error.cause}`,
);
},
});
@@ -0,0 +1,62 @@
import type { Modify } from "@homarr/common/types";
import { createId } from "@homarr/db/client";
import type { WidgetKind } from "@homarr/definitions";
import type { Board, DynamicSection, EmptySection, Item } from "~/app/[locale]/boards/_types";
import { getFirstEmptyPosition } from "./empty-position";
export interface CreateItemInput {
kind: WidgetKind;
}
export const createItemCallback =
({ kind }: CreateItemInput) =>
(previous: Board): Board => {
const firstSection = previous.sections
.filter((section): section is EmptySection => section.kind === "empty")
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset)
.at(0);
if (!firstSection) return previous;
const dynamicSectionsOfFirstSection = previous.sections.filter(
(section): section is DynamicSection => section.kind === "dynamic" && section.parentSectionId === firstSection.id,
);
const elements = [...firstSection.items, ...dynamicSectionsOfFirstSection];
const emptyPosition = getFirstEmptyPosition(elements, previous.columnCount);
if (!emptyPosition) {
console.error("Your board is full");
return previous;
}
const widget = {
id: createId(),
kind,
options: {},
width: 1,
height: 1,
...emptyPosition,
integrationIds: [],
advancedOptions: {
customCssClasses: [],
},
} satisfies Modify<
Item,
{
kind: WidgetKind;
}
>;
return {
...previous,
sections: previous.sections.map((section) => {
// Return same section if item is not in it
if (section.id !== firstSection.id) return section;
return {
...section,
items: section.items.concat(widget),
};
}),
};
};
@@ -0,0 +1,81 @@
import { createId } from "@homarr/db/client";
import type { Board, DynamicSection, EmptySection } from "~/app/[locale]/boards/_types";
import { getFirstEmptyPosition } from "./empty-position";
export interface DuplicateItemInput {
itemId: string;
}
export const duplicateItemCallback =
({ itemId }: DuplicateItemInput) =>
(previous: Board): Board => {
const itemToDuplicate = previous.sections
.flatMap((section) => section.items.map((item) => ({ ...item, sectionId: section.id })))
.find((item) => item.id === itemId);
if (!itemToDuplicate) return previous;
const currentSection = previous.sections.find((section) => section.id === itemToDuplicate.sectionId);
if (!currentSection) return previous;
const dynamicSectionsOfCurrentSection = previous.sections.filter(
(section): section is DynamicSection =>
section.kind === "dynamic" && section.parentSectionId === currentSection.id,
);
const elements = [...currentSection.items, ...dynamicSectionsOfCurrentSection];
let sectionId = currentSection.id;
let emptyPosition = getFirstEmptyPosition(
elements,
currentSection.kind === "dynamic" ? currentSection.width : previous.columnCount,
currentSection.kind === "dynamic" ? currentSection.height : undefined,
{
width: itemToDuplicate.width,
height: itemToDuplicate.height,
},
);
if (!emptyPosition) {
const firstSection = previous.sections
.filter((section): section is EmptySection => section.kind === "empty")
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset)
.at(0);
if (!firstSection) return previous;
const dynamicSectionsOfFirstSection = previous.sections.filter(
(section): section is DynamicSection =>
section.kind === "dynamic" && section.parentSectionId === firstSection.id,
);
const elements = [...firstSection.items, ...dynamicSectionsOfFirstSection];
emptyPosition = getFirstEmptyPosition(elements, previous.columnCount, undefined, {
width: itemToDuplicate.width,
height: itemToDuplicate.height,
});
if (!emptyPosition) {
console.error("Your board is full");
return previous;
}
sectionId = firstSection.id;
}
const widget = structuredClone(itemToDuplicate);
widget.id = createId();
widget.xOffset = emptyPosition.xOffset;
widget.yOffset = emptyPosition.yOffset;
widget.sectionId = sectionId;
const result = {
...previous,
sections: previous.sections.map((section) => {
// Return same section if item is not in it
if (section.id !== sectionId) return section;
return {
...section,
items: section.items.concat(widget),
};
}),
};
return result;
};
@@ -0,0 +1,25 @@
import type { Item } from "~/app/[locale]/boards/_types";
export const getFirstEmptyPosition = (
elements: Pick<Item, "yOffset" | "xOffset" | "width" | "height">[],
columnCount: number,
rowCount = 9999,
size: { width: number; height: number } = { width: 1, height: 1 },
) => {
for (let yOffset = 0; yOffset < rowCount + 1 - size.height; yOffset++) {
for (let xOffset = 0; xOffset < columnCount; xOffset++) {
const isOccupied = elements.some(
(element) =>
element.yOffset < yOffset + size.height &&
element.yOffset + element.height > yOffset &&
element.xOffset < xOffset + size.width &&
element.xOffset + element.width > xOffset,
);
if (!isOccupied) {
return { xOffset, yOffset };
}
}
}
return undefined;
};
@@ -0,0 +1,61 @@
import { describe, expect, test, vi } from "vitest";
import type { Board } from "~/app/[locale]/boards/_types";
import { createItemCallback } from "../create-item";
import * as emptyPosition from "../empty-position";
import { createDynamicSection, createEmptySection, createItem } from "./shared";
describe("item actions create-item", () => {
test("should add it to first section", () => {
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
const input = {
sections: [createEmptySection("1", 2), createEmptySection("2", 0), createEmptySection("3", 1)],
columnCount: 4,
} satisfies Pick<Board, "sections" | "columnCount">;
const result = createItemCallback({
kind: "clock",
})(input as unknown as Board);
const firstSection = result.sections.find((section) => section.id === "2");
const item = firstSection?.items.at(0);
expect(item).toEqual(expect.objectContaining({ kind: "clock", xOffset: 5, yOffset: 5 }));
expect(spy).toHaveBeenCalledWith([], input.columnCount);
});
test("should correctly pass dynamic section and items to getFirstEmptyPosition", () => {
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
const firstSection = createEmptySection("2", 0);
const expectedItem = createItem({ id: "12", xOffset: 1, yOffset: 2, width: 3, height: 2 });
firstSection.items.push(expectedItem);
const dynamicSectionInFirst = createDynamicSection({
id: "4",
parentSectionId: "2",
yOffset: 0,
xOffset: 0,
width: 2,
height: 2,
});
const input = {
sections: [
createEmptySection("1", 2),
firstSection,
createEmptySection("3", 1),
dynamicSectionInFirst,
createDynamicSection({ id: "5", parentSectionId: "3", yOffset: 1 }),
],
columnCount: 4,
} satisfies Pick<Board, "sections" | "columnCount">;
const result = createItemCallback({
kind: "clock",
})(input as unknown as Board);
const firstSectionResult = result.sections.find((section) => section.id === "2");
const item = firstSectionResult?.items.find((item) => item.id !== "12");
expect(item).toEqual(expect.objectContaining({ kind: "clock", xOffset: 5, yOffset: 5 }));
expect(spy).toHaveBeenCalledWith([expectedItem, dynamicSectionInFirst], input.columnCount);
});
});
@@ -0,0 +1,52 @@
import { describe, expect, test, vi } from "vitest";
import type { Board } from "~/app/[locale]/boards/_types";
import { duplicateItemCallback } from "../duplicate-item";
import * as emptyPosition from "../empty-position";
import { createEmptySection, createItem } from "./shared";
describe("item actions duplicate-item", () => {
test("should copy it in the same section", () => {
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
const currentSection = createEmptySection("2", 1);
const currentItem = createItem({
id: "1",
xOffset: 1,
yOffset: 3,
width: 3,
height: 2,
kind: "minecraftServerStatus",
integrationIds: ["1"],
options: { address: "localhost" },
advancedOptions: { customCssClasses: ["test"] },
});
const otherItem = createItem({
id: "2",
});
currentSection.items.push(currentItem, otherItem);
const input = {
columnCount: 10,
sections: [createEmptySection("1", 0), currentSection, createEmptySection("3", 2)],
} satisfies Pick<Board, "sections" | "columnCount">;
const result = duplicateItemCallback({ itemId: currentItem.id })(input as unknown as Board);
const section = result.sections.find((section) => section.id === "2");
expect(section?.items.length).toBe(3);
const duplicatedItem = section?.items.find((item) => item.id !== currentItem.id && item.id !== otherItem.id);
expect(duplicatedItem).toEqual(
expect.objectContaining({
kind: "minecraftServerStatus",
xOffset: 5,
yOffset: 5,
width: 3,
height: 2,
integrationIds: ["1"],
options: { address: "localhost" },
advancedOptions: { customCssClasses: ["test"] },
}),
);
});
});
@@ -0,0 +1,138 @@
import { describe, expect, test } from "vitest";
import type { Item } from "~/app/[locale]/boards/_types";
import { getFirstEmptyPosition } from "../empty-position";
describe("get first empty position", () => {
test.each([
[[[" ", " ", " ", " "]], [1, 1], 0, 0],
[[["a", " ", " ", " "]], [1, 1], 1, 0],
[[[" ", "a", " ", " "]], [1, 1], 0, 0],
[
[
["a", "a", " ", " "],
["a", "a", " ", " "],
],
[1, 1],
2,
0,
],
[[["a", "a", "a", "a"]], [1, 1], 0, 1],
[
[
["a", "a", "a", "a"],
["a", "a", "a", "a"],
],
[1, 1],
0,
2,
],
[
[
["a", "a", " ", "b", "b"],
["a", "a", " ", "b", "b"],
],
[1, 2],
2,
0,
],
[
[
["a", "a", " ", " ", "b", "b"],
["a", "a", " ", " ", "b", "b"],
],
[2, 2],
2,
0,
],
[
[
["a", "a", " ", "d", "b", "b"],
["a", "a", "c", "e", "b", "b"],
],
[1, 1],
2,
0,
],
[
[
["a", "a", " ", " ", "b", "b"],
["a", "a", " ", "e", "b", "b"],
],
[2, 2],
0,
2,
],
])("should return the first empty position", (layout, size, expectedX, expectedY) => {
const elements = createElementsFromLayout(layout);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const result = getFirstEmptyPosition(elements, layout[0]!.length, undefined, { width: size[0]!, height: size[1]! });
expect(result).toEqual({ xOffset: expectedX, yOffset: expectedY });
});
test.each([
[[[" ", " "]], [1, 1], 0, 0, 1],
[[["a", " "]], [1, 1], 1, 0, 1],
[[["a", "a"]], [1, 1], undefined, undefined, 1],
[[["a", "a"]], [1, 1], 0, 1, 2],
[
[
["a", "b", " ", " "],
["a", "c", " ", "d"],
],
[2, 2],
undefined,
undefined,
3,
],
[
[
["a", "b", " ", " "],
["a", "c", " ", "d"],
],
[2, 2],
0,
2,
4,
],
[[["a", "b"]], [2, 1], 0, 1, 2],
])("should return the first empty position with limited rows", (layout, size, expectedX, expectedY, rowCount) => {
const elements = createElementsFromLayout(layout);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const result = getFirstEmptyPosition(elements, layout[0]!.length, rowCount, { width: size[0]!, height: size[1]! });
expect(result).toEqual(expectedX !== undefined ? { xOffset: expectedX, yOffset: expectedY } : undefined);
});
});
const createElementsFromLayout = (layout: string[][]) => {
const elements: (Pick<Item, "xOffset" | "yOffset" | "width" | "height"> & { char: string })[] = [];
for (let yOffset = 0; yOffset < layout.length; yOffset++) {
const row = layout[yOffset];
if (!row) continue;
for (let xOffset = 0; xOffset < row.length; xOffset++) {
const item = row[xOffset];
if (item === " " || !item) continue;
const existing = elements.find((element) => element.char === item);
if (existing) {
existing.height = yOffset - existing.yOffset + 1;
existing.width = xOffset - existing.xOffset + 1;
continue;
}
elements.push({
yOffset,
xOffset,
width: 1,
height: 1,
char: item,
});
}
}
return elements;
};
@@ -0,0 +1,32 @@
import type { DynamicSection, EmptySection, Item } from "~/app/[locale]/boards/_types";
export const createEmptySection = (id: string, yOffset: number): EmptySection => ({
id,
kind: "empty",
yOffset,
xOffset: 0,
items: [],
});
export const createDynamicSection = (section: Omit<Partial<DynamicSection>, "kind">): DynamicSection => ({
id: section.id ?? "0",
kind: "dynamic",
parentSectionId: section.parentSectionId ?? "0",
height: section.height ?? 1,
width: section.width ?? 1,
yOffset: section.yOffset ?? 0,
xOffset: section.xOffset ?? 0,
items: section.items ?? [],
});
export const createItem = (item: Partial<Item>): Item => ({
id: item.id ?? "0",
width: item.width ?? 1,
height: item.height ?? 1,
yOffset: item.yOffset ?? 0,
xOffset: item.xOffset ?? 0,
kind: item.kind ?? "clock",
integrationIds: item.integrationIds ?? [],
options: item.options ?? {},
advancedOptions: item.advancedOptions ?? { customCssClasses: [] },
});
@@ -1,12 +1,13 @@
import { useCallback } from "react";
import type { Modify } from "@homarr/common/types";
import { createId } from "@homarr/db/client";
import type { WidgetKind } from "@homarr/definitions";
import type { BoardItemAdvancedOptions } from "@homarr/validation";
import type { EmptySection, Item } from "~/app/[locale]/boards/_types";
import type { Item } from "~/app/[locale]/boards/_types";
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client";
import type { CreateItemInput } from "./actions/create-item";
import { createItemCallback } from "./actions/create-item";
import type { DuplicateItemInput } from "./actions/duplicate-item";
import { duplicateItemCallback } from "./actions/duplicate-item";
interface MoveAndResizeItem {
itemId: string;
@@ -42,87 +43,19 @@ interface UpdateItemIntegrations {
newIntegrations: string[];
}
interface CreateItem {
kind: WidgetKind;
}
interface DuplicateItem {
itemId: string;
}
export const useItemActions = () => {
const { updateBoard } = useUpdateBoard();
const createItem = useCallback(
({ kind }: CreateItem) => {
updateBoard((previous) => {
const lastSection = previous.sections
.filter((section): section is EmptySection => section.kind === "empty")
.sort((sectionA, sectionB) => sectionB.yOffset - sectionA.yOffset)[0];
if (!lastSection) return previous;
const widget = {
id: createId(),
kind,
options: {},
width: 1,
height: 1,
integrationIds: [],
advancedOptions: {
customCssClasses: [],
},
} satisfies Modify<
Omit<Item, "yOffset" | "xOffset">,
{
kind: WidgetKind;
}
>;
return {
...previous,
sections: previous.sections.map((section) => {
// Return same section if item is not in it
if (section.id !== lastSection.id) return section;
return {
...section,
items: section.items.concat(widget as unknown as Item),
};
}),
};
});
(input: CreateItemInput) => {
updateBoard(createItemCallback(input));
},
[updateBoard],
);
const duplicateItem = useCallback(
({ itemId }: DuplicateItem) => {
updateBoard((previous) => {
const itemToDuplicate = previous.sections
.flatMap((section) => section.items)
.find((item) => item.id === itemId);
if (!itemToDuplicate) return previous;
const newItem = {
...itemToDuplicate,
id: createId(),
yOffset: undefined,
xOffset: undefined,
} satisfies Modify<Item, { yOffset?: number; xOffset?: number }>;
return {
...previous,
sections: previous.sections.map((section) => {
// Return same section if item is not in it
if (!section.items.some((item) => item.id === itemId)) return section;
return {
...section,
items: section.items.concat(newItem as unknown as Item),
};
}),
};
});
({ itemId }: DuplicateItemInput) => {
updateBoard(duplicateItemCallback({ itemId }));
},
[updateBoard],
);
@@ -79,7 +79,12 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
>
<Throw
error={new NoIntegrationSelectedError()}
when={widgetSupportsIntegrations && item.integrationIds.length === 0}
when={
widgetSupportsIntegrations &&
item.integrationIds.length === 0 &&
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
(!("integrationsRequired" in definition) || definition.integrationsRequired !== false)
}
/>
<BoardItemMenu offset={4} item={newItem} />
<Comp
@@ -1,11 +1,11 @@
import { useCallback, useRef } from "react";
import { Button, Grid, Group, NumberInput, Stack } from "@mantine/core";
import { z } from "zod";
import { useZodForm } from "@homarr/form";
import type { GridStack } from "@homarr/gridstack";
import { createModal } from "@homarr/modals";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { z } from "@homarr/validation";
import type { Item } from "~/app/[locale]/boards/_types";
@@ -1,12 +1,12 @@
"use client";
import { Button, Group, Stack, TextInput } from "@mantine/core";
import type { z } from "zod";
import { clientApi } from "@homarr/api/client";
import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
interface InnerProps {
@@ -2,19 +2,32 @@ import { Card, Collapse, Group, Stack, Title, UnstyledButton } from "@mantine/co
import { useDisclosure } from "@mantine/hooks";
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import type { CategorySection } from "~/app/[locale]/boards/_types";
import { useRequiredBoard } from "~/app/[locale]/boards/(content)/_context";
import { CategoryMenu } from "./category/category-menu";
import { GridStack } from "./gridstack/gridstack";
import classes from "./item.module.css";
interface Props {
section: CategorySection;
}
export const BoardCategorySection = ({ section }: Props) => {
const [opened, { toggle }] = useDisclosure(false);
const { mutate } = clientApi.section.changeCollapsed.useMutation();
const board = useRequiredBoard();
const [opened, { toggle }] = useDisclosure(section.collapsed, {
onOpen() {
mutate({ sectionId: section.id, collapsed: true });
},
onClose() {
mutate({ sectionId: section.id, collapsed: false });
},
});
return (
<Card withBorder p={0}>
<Card style={{ "--opacity": board.opacity / 100 }} withBorder p={0} className={classes.itemCard}>
<Stack>
<Group wrap="nowrap" gap="sm">
<UnstyledButton w="100%" p="sm" onClick={toggle}>
@@ -67,6 +67,7 @@ const createSections = (categoryCount: number) => {
name: `Category ${index}`,
yOffset: index,
xOffset: 0,
collapsed: false,
items: [],
})) satisfies Section[];
};
@@ -118,6 +118,7 @@ const createSections = (initialYOffsets: number[]) => {
id: yOffset.toString(),
kind: index % 2 === 0 ? "empty" : "category",
name: "Category",
collapsed: false,
yOffset,
xOffset: 0,
items: [createItem({ id: yOffset.toString() })],
@@ -40,6 +40,7 @@ export const useCategoryActions = () => {
kind: "category",
yOffset,
xOffset: 0,
collapsed: false,
items: [],
},
{
@@ -89,6 +90,7 @@ export const useCategoryActions = () => {
kind: "category",
yOffset: lastYOffset + 1,
xOffset: 0,
collapsed: false,
items: [],
},
{
@@ -1,9 +1,9 @@
import { Button, Group, Stack, TextInput } from "@mantine/core";
import { z } from "zod";
import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
import { z } from "@homarr/validation";
interface Category {
id: string;
+2 -7
View File
@@ -12,11 +12,7 @@ export const env = createEnv({
* Specify your server-side environment variables schema here. This way you can ensure the app isn't
* built with invalid env vars.
*/
server: {
// Comma separated list of docker hostnames that can be used to connect to query the docker endpoints (localhost:2375,host.docker.internal:2375, ...)
DOCKER_HOSTNAMES: z.string().optional(),
DOCKER_PORTS: z.string().optional(),
},
server: {},
/**
* Specify your client-side environment variables schema here.
* For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
@@ -30,9 +26,8 @@ export const env = createEnv({
runtimeEnv: {
PORT: process.env.PORT,
NODE_ENV: process.env.NODE_ENV,
DOCKER_HOSTNAMES: process.env.DOCKER_HOSTNAMES,
DOCKER_PORTS: process.env.DOCKER_PORTS,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
},
skipValidation: shouldSkipEnvValidation(),
emptyStringAsUndefined: true,
});
+2 -2
View File
@@ -4,7 +4,7 @@ import { createTRPCClient, httpLink } from "@trpc/client";
import SuperJSON from "superjson";
import type { AppRouter } from "@homarr/api";
import { createHeadersCallbackForSource } from "@homarr/api/shared";
import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
import { createI18nMiddleware } from "@homarr/translation/middleware";
export async function middleware(request: NextRequest) {
@@ -34,7 +34,7 @@ export const config = {
export const serverFetchApi = createTRPCClient<AppRouter>({
links: [
httpLink({
url: `http://${process.env.HOSTNAME ?? "localhost"}:3000/api/trpc`,
url: getTrpcUrl(),
transformer: SuperJSON,
headers: createHeadersCallbackForSource("server-fetch"),
}),
+3 -3
View File
@@ -38,15 +38,15 @@
"dayjs": "^1.11.13",
"dotenv": "^16.4.7",
"superjson": "2.2.2",
"undici": "7.2.3"
"undici": "7.3.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.10.7",
"@types/node": "^22.12.0",
"dotenv-cli": "^8.0.0",
"eslint": "^9.18.0",
"eslint": "^9.19.0",
"prettier": "^3.4.2",
"tsx": "4.19.2",
"typescript": "^5.7.3"
+2 -2
View File
@@ -33,8 +33,8 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.5.13",
"eslint": "^9.18.0",
"@types/ws": "^8.5.14",
"eslint": "^9.19.0",
"prettier": "^3.4.2",
"typescript": "^5.7.3"
}
+8
View File
@@ -39,6 +39,14 @@ const handler = applyWSSHandler({
});
}
},
// Enable heartbeat messages to keep connection open (disabled by default)
keepAlive: {
enabled: true,
// server ping message interval in milliseconds
pingMs: 30000,
// connection is terminated if pong message is not received in this many milliseconds
pongWaitMs: 5000,
},
});
wss.on("connection", (websocket, incomingMessage) => {
+3 -3
View File
@@ -49,9 +49,9 @@ describe("Onboarding", () => {
environment: {
AUTH_PROVIDERS: "ldap",
AUTH_LDAP_URI: "ldap://host.docker.internal:3890",
AUTH_LDAP_BASE: "",
AUTH_LDAP_BIND_DN: "",
AUTH_LDAP_BIND_PASSWORD: "",
AUTH_LDAP_BASE: "not-used",
AUTH_LDAP_BIND_DN: "not-used",
AUTH_LDAP_BIND_PASSWORD: "not-used",
},
mounts: {
"/appdata": localMountPath,
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.0.1",
"version": "1.4.0",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -38,29 +38,29 @@
"@semantic-release/github": "^11.0.1",
"@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.3.3",
"@turbo/gen": "^2.3.4",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^3.0.2",
"@vitest/ui": "^3.0.2",
"@vitest/coverage-v8": "^3.0.4",
"@vitest/ui": "^3.0.4",
"conventional-changelog-conventionalcommits": "^8.0.0",
"cross-env": "^7.0.3",
"jsdom": "^26.0.0",
"prettier": "^3.4.2",
"semantic-release": "^24.2.1",
"testcontainers": "^10.16.0",
"turbo": "^2.3.3",
"testcontainers": "^10.17.2",
"turbo": "^2.3.4",
"typescript": "^5.7.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.2"
"vitest": "^3.0.4"
},
"packageManager": "pnpm@9.15.4",
"engines": {
"node": ">=22.13.0"
"node": ">=22.13.1"
},
"pnpm": {
"allowNonAppliedPatches": true,
"overrides": {
"proxmox-api>undici": "7.2.3"
"proxmox-api>undici": "7.3.0"
},
"patchedDependencies": {
"pretty-print-error": "patches/pretty-print-error.patch"
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.18.0",
"eslint": "^9.19.0",
"typescript": "^5.7.3"
}
}
+6 -5
View File
@@ -29,6 +29,7 @@
"@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/docker": "workspace:^0.1.0",
"@homarr/icons": "workspace:^0.1.0",
"@homarr/integrations": "workspace:^0.1.0",
"@homarr/log": "workspace:^",
@@ -42,20 +43,20 @@
"@trpc/client": "next",
"@trpc/react-query": "next",
"@trpc/server": "next",
"dockerode": "4.0.2",
"lodash.clonedeep": "^4.5.0",
"next": "15.1.5",
"next": "15.1.6",
"pretty-print-error": "^1.1.2",
"react": "19.0.0",
"react-dom": "19.0.0",
"superjson": "2.2.2",
"trpc-to-openapi": "^2.1.2"
"trpc-to-openapi": "^2.1.2",
"zod": "^3.24.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.34",
"eslint": "^9.18.0",
"eslint": "^9.19.0",
"prettier": "^3.4.2",
"typescript": "^5.7.3"
}
+1 -14
View File
@@ -5,7 +5,7 @@ import { createTRPCReact } from "@trpc/react-query";
import SuperJSON from "superjson";
import type { AppRouter } from ".";
import { createHeadersCallbackForSource } from "./shared";
import { createHeadersCallbackForSource, getTrpcUrl } from "./shared";
export const clientApi = createTRPCReact<AppRouter>();
export const fetchApi = createTRPCClient<AppRouter>({
@@ -17,16 +17,3 @@ export const fetchApi = createTRPCClient<AppRouter>({
}),
],
});
function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin;
return `http://localhost:${process.env.PORT ?? 3000}`;
}
/**
* Creates the full url for the trpc api endpoint
* @returns
*/
export function getTrpcUrl() {
return `${getBaseUrl()}/api/trpc`;
}
+42 -39
View File
@@ -1,4 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import type { Session } from "@homarr/auth";
import { hasQueryAccessToIntegrationsAsync } from "@homarr/auth/server";
@@ -9,7 +10,6 @@ import type { Database } from "@homarr/db";
import { and, eq, inArray } from "@homarr/db";
import { integrations } from "@homarr/db/schema";
import type { IntegrationKind } from "@homarr/definitions";
import { z } from "@homarr/validation";
import { publicProcedure } from "../trpc";
@@ -98,56 +98,59 @@ export const createManyIntegrationMiddleware = <TKind extends IntegrationKind>(
action: IntegrationAction,
...kinds: AtLeastOneOf<TKind> // Ensure at least one kind is provided
) => {
return publicProcedure
.input(z.object({ integrationIds: z.array(z.string()).min(1) }))
.use(async ({ ctx, input, next }) => {
const dbIntegrations = await ctx.db.query.integrations.findMany({
where: and(inArray(integrations.id, input.integrationIds), inArray(integrations.kind, kinds)),
with: {
secrets: true,
items: {
return publicProcedure.input(z.object({ integrationIds: z.array(z.string()) })).use(async ({ ctx, input, next }) => {
const dbIntegrations =
input.integrationIds.length >= 1
? await ctx.db.query.integrations.findMany({
where: and(inArray(integrations.id, input.integrationIds), inArray(integrations.kind, kinds)),
with: {
item: {
secrets: true,
items: {
with: {
section: {
columns: {
boardId: true,
item: {
with: {
section: {
columns: {
boardId: true,
},
},
},
},
},
},
userPermissions: true,
groupPermissions: true,
},
},
userPermissions: true,
groupPermissions: true,
},
})
: [];
const offset = input.integrationIds.length - dbIntegrations.length;
if (offset !== 0) {
throw new TRPCError({
code: "NOT_FOUND",
message: `${offset} of the specified integrations not found or not of kinds ${kinds.join(",")}: ([${input.integrationIds.join(",")}] compared to [${dbIntegrations.map(({ id, kind }) => `${kind}:${id}`).join(",")}])`,
});
}
const offset = input.integrationIds.length - dbIntegrations.length;
if (offset !== 0) {
throw new TRPCError({
code: "NOT_FOUND",
message: `${offset} of the specified integrations not found or not of kinds ${kinds.join(",")}: ([${input.integrationIds.join(",")}] compared to [${dbIntegrations.map(({ id, kind }) => `${kind}:${id}`).join(",")}])`,
});
}
if (dbIntegrations.length >= 1) {
await throwIfActionIsNotAllowedAsync(action, ctx.db, dbIntegrations, ctx.session);
}
return next({
ctx: {
integrations: dbIntegrations.map(
({ secrets, kind, items: _ignore1, groupPermissions: _ignore2, userPermissions: _ignore3, ...rest }) => ({
...rest,
kind: kind as TKind,
decryptedSecrets: secrets.map((secret) => ({
...secret,
value: decryptSecret(secret.value),
})),
}),
),
},
});
return next({
ctx: {
integrations: dbIntegrations.map(
({ secrets, kind, items: _ignore1, groupPermissions: _ignore2, userPermissions: _ignore3, ...rest }) => ({
...rest,
kind: kind as TKind,
decryptedSecrets: secrets.map((secret) => ({
...secret,
value: decryptSecret(secret.value),
})),
}),
),
},
});
});
};
/**
+1 -1
View File
@@ -1,9 +1,9 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { and, eq } from "@homarr/db";
import { items } from "@homarr/db/schema";
import type { WidgetKind } from "@homarr/definitions";
import { z } from "@homarr/validation";
import { publicProcedure } from "../trpc";
+2
View File
@@ -15,6 +15,7 @@ import { logRouter } from "./router/log";
import { mediaRouter } from "./router/medias/media-router";
import { onboardRouter } from "./router/onboard/onboard-router";
import { searchEngineRouter } from "./router/search-engine/search-engine-router";
import { sectionRouter } from "./router/section/section-router";
import { serverSettingsRouter } from "./router/serverSettings";
import { updateCheckerRouter } from "./router/update-checker";
import { userRouter } from "./router/user";
@@ -27,6 +28,7 @@ export const appRouter = createTRPCRouter({
invite: inviteRouter,
integration: integrationRouter,
board: boardRouter,
section: sectionRouter,
app: innerAppRouter,
searchEngine: searchEngineRouter,
widget: widgetRouter,
+3 -2
View File
@@ -1,10 +1,11 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { asc, createId, eq, inArray, like } from "@homarr/db";
import { apps } from "@homarr/db/schema";
import { selectAppSchema } from "@homarr/db/validationSchemas";
import { getIconForName } from "@homarr/icons";
import { validation, z } from "@homarr/validation";
import { validation } from "@homarr/validation";
import { convertIntersectionToZodObject } from "../schema-merger";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
@@ -102,7 +103,7 @@ export const appRouter = createTRPCRouter({
return app;
}),
byIds: protectedProcedure.input(z.array(z.string())).query(async ({ ctx, input }) => {
byIds: publicProcedure.input(z.array(z.string())).query(async ({ ctx, input }) => {
return await ctx.db.query.apps.findMany({
where: inArray(apps.id, input),
});
+8 -2
View File
@@ -1,5 +1,6 @@
import { TRPCError } from "@trpc/server";
import superjson from "superjson";
import { z } from "zod";
import { constructBoardPermissions } from "@homarr/auth/shared";
import type { DeviceType } from "@homarr/common/server";
@@ -16,6 +17,7 @@ import {
integrationItems,
integrationUserPermissions,
items,
sectionCollapseStates,
sections,
users,
} from "@homarr/db/schema";
@@ -25,7 +27,7 @@ import { importOldmarrAsync } from "@homarr/old-import";
import { importJsonFileSchema } from "@homarr/old-import/shared";
import { oldmarrConfigSchema } from "@homarr/old-schema";
import type { BoardItemAdvancedOptions } from "@homarr/validation";
import { createSectionSchema, sharedItemSchema, validation, z, zodUnionFromArray } from "@homarr/validation";
import { createSectionSchema, sharedItemSchema, validation, zodUnionFromArray } from "@homarr/validation";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
import { throwIfActionForbiddenAsync } from "./board/board-access";
@@ -1024,6 +1026,9 @@ const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, use
},
sections: {
with: {
collapseStates: {
where: eq(sectionCollapseStates.userId, userId ?? ""),
},
items: {
with: {
integrations: {
@@ -1058,9 +1063,10 @@ const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, use
return {
...otherBoardProperties,
sections: sections.map((section) =>
sections: sections.map(({ collapseStates, ...section }) =>
parseSection({
...section,
collapsed: collapseStates.at(0)?.collapsed ?? false,
items: section.items.map(({ integrations: itemIntegrations, ...item }) => ({
...item,
integrationIds: itemIntegrations.map((item) => item.integration.id),
+11 -14
View File
@@ -1,26 +1,24 @@
import { TRPCError } from "@trpc/server";
import type Docker from "dockerode";
import type { Container } from "dockerode";
import { z } from "zod";
import { db, like, or } from "@homarr/db";
import { icons } from "@homarr/db/schema";
import type { DockerContainerState } from "@homarr/definitions";
import { DockerSingleton } from "@homarr/docker";
import type { Container, ContainerInfo, ContainerState, Docker, Port } from "@homarr/docker";
import { logger } from "@homarr/log";
import { createCacheChannel } from "@homarr/redis";
import { z } from "@homarr/validation";
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
import { DockerSingleton } from "./docker-singleton";
const dockerCache = createCacheChannel<{
containers: (Docker.ContainerInfo & { instance: string; iconUrl: string | null })[];
containers: (ContainerInfo & { instance: string; iconUrl: string | null })[];
}>("docker-containers", 5 * 60 * 1000);
export const dockerRouter = createTRPCRouter({
getContainers: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
const result = await dockerCache
.consumeAsync(async () => {
const dockerInstances = DockerSingleton.getInstance();
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 }) =>
@@ -33,8 +31,7 @@ export const dockerRouter = createTRPCRouter({
),
).then((containers) => containers.flat());
const extractImage = (container: Docker.ContainerInfo) =>
container.Image.split("/").at(-1)?.split(":").at(0) ?? "";
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
@@ -151,7 +148,7 @@ const getContainerOrDefaultAsync = async (instance: Docker, id: string) => {
};
const getContainerOrThrowAsync = async (id: string) => {
const dockerInstances = DockerSingleton.getInstance();
const dockerInstances = DockerSingleton.getInstances();
const containers = await Promise.all(dockerInstances.map(({ instance }) => getContainerOrDefaultAsync(instance, id)));
const foundContainer = containers.find((container) => container) ?? null;
@@ -168,21 +165,21 @@ const getContainerOrThrowAsync = async (id: string) => {
interface DockerContainer {
name: string;
id: string;
state: DockerContainerState;
state: ContainerState;
image: string;
ports: Docker.Port[];
ports: Port[];
iconUrl: string | null;
}
function sanitizeContainers(
containers: (Docker.ContainerInfo & { instance: string; iconUrl: string | null })[],
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 DockerContainerState,
state: container.State as ContainerState,
image: container.Image,
ports: container.Ports,
iconUrl: container.iconUrl,
@@ -1,51 +0,0 @@
import Docker from "dockerode";
interface DockerInstance {
host: string;
instance: Docker;
}
export class DockerSingleton {
private static instances: DockerInstance[];
private createInstances() {
const instances: DockerInstance[] = [];
const hostVariable = process.env.DOCKER_HOST;
const portVariable = process.env.DOCKER_PORT;
if (hostVariable === undefined || portVariable === undefined) {
instances.push({ host: "socket", instance: new Docker() });
return instances;
}
const hosts = hostVariable.split(",");
const ports = portVariable.split(",");
if (hosts.length !== ports.length) {
throw new Error("The number of hosts and ports must match");
}
hosts.forEach((host, i) => {
instances.push({
host: `${host}:${ports[i]}`,
instance: new Docker({
host,
port: parseInt(ports[i] ?? "", 10),
}),
});
return instances;
});
return instances;
}
public static findInstance(key: string): DockerInstance | undefined {
return this.instances.find((instance) => instance.host === key);
}
public static getInstance(): DockerInstance[] {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!DockerSingleton.instances) {
DockerSingleton.instances = new DockerSingleton().createInstances();
}
return this.instances;
}
}
+2 -1
View File
@@ -1,10 +1,11 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import type { Database } from "@homarr/db";
import { and, createId, eq, like, not, sql } from "@homarr/db";
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema";
import { everyoneGroup } from "@homarr/definitions";
import { validation, z } from "@homarr/validation";
import { validation } from "@homarr/validation";
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, protectedProcedure } from "../trpc";
import { throwIfCredentialsDisabled } from "./invite/checks";
@@ -1,10 +1,11 @@
import { z } from "zod";
import { analyseOldmarrImportForRouterAsync, analyseOldmarrImportInputSchema } from "@homarr/old-import/analyse";
import {
ensureValidTokenOrThrow,
importInitialOldmarrAsync,
importInitialOldmarrInputSchema,
} from "@homarr/old-import/import";
import { z } from "@homarr/validation";
import { createTRPCRouter, onboardingProcedure } from "../../trpc";
import { nextOnboardingStepAsync } from "../onboard/onboard-queries";
@@ -1,4 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { objectEntries } from "@homarr/common";
import { decryptSecret, encryptSecret } from "@homarr/common/server";
@@ -23,7 +24,7 @@ import {
integrationSecretKindObject,
} from "@homarr/definitions";
import { integrationCreator } from "@homarr/integrations";
import { validation, z } from "@homarr/validation";
import { validation } from "@homarr/validation";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
@@ -205,7 +206,10 @@ export const integrationRouter = createTRPCRouter({
);
}
if (input.attemptSearchEngineCreation) {
if (
input.attemptSearchEngineCreation &&
integrationDefs[input.kind].category.flatMap((category) => category).includes("search")
) {
const icon = getIconUrl(input.kind);
await ctx.db.insert(searchEngines).values({
id: createId(),
@@ -1,8 +1,11 @@
import { formatError } from "pretty-print-error";
import { decryptSecret } from "@homarr/common/server";
import type { Integration } from "@homarr/db/schema";
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
import { getAllSecretKindOptions } from "@homarr/definitions";
import { integrationCreator, IntegrationTestConnectionError } from "@homarr/integrations";
import { logger } from "@homarr/log";
type FormIntegration = Integration & {
secrets: {
@@ -28,11 +31,22 @@ export const testConnectionAsync = async (
source: "form" as const,
}));
const decryptedDbSecrets = dbSecrets.map((secret) => ({
...secret,
value: decryptSecret(secret.value),
source: "db" as const,
}));
const decryptedDbSecrets = dbSecrets
.map((secret) => {
try {
return {
...secret,
value: decryptSecret(secret.value),
source: "db" as const,
};
} catch (error) {
logger.warn(
`Failed to decrypt secret from database integration="${integration.name}" secretKind="${secret.kind}"\n${formatError(error)}`,
);
return null;
}
})
.filter((secret) => secret !== null);
const sourcedSecrets = [...formSecrets, ...decryptedDbSecrets];
const secretKinds = getSecretKindOption(integration.kind, sourcedSecrets);
+1 -1
View File
@@ -1,10 +1,10 @@
import { randomBytes } from "crypto";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { asc, createId, eq } from "@homarr/db";
import { invites } from "@homarr/db/schema";
import { selectInviteSchema } from "@homarr/db/validationSchemas";
import { z } from "@homarr/validation";
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
import { throwIfCredentialsDisabled } from "./invite/checks";
+2 -1
View File
@@ -1,5 +1,6 @@
import type { z } from "zod";
import { fetchWithTimeout } from "@homarr/common";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
import { createTRPCRouter, publicProcedure } from "../trpc";
@@ -1,10 +1,11 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import type { InferInsertModel } from "@homarr/db";
import { and, createId, desc, eq, like } from "@homarr/db";
import { iconRepositories, icons, medias } from "@homarr/db/schema";
import { createLocalImageUrl, LOCAL_ICON_REPOSITORY_SLUG, mapMediaToIcon } from "@homarr/icons/local";
import { validation, z } from "@homarr/validation";
import { validation } from "@homarr/validation";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../../trpc";
@@ -1,6 +1,8 @@
import { z } from "zod";
import { onboarding } from "@homarr/db/schema";
import { onboardingSteps } from "@homarr/definitions";
import { z, zodEnumFromArray } from "@homarr/validation";
import { zodEnumFromArray } from "@homarr/validation";
import { createTRPCRouter, publicProcedure } from "../../trpc";
import { getOnboardingOrFallbackAsync, nextOnboardingStepAsync } from "./onboard-queries";
@@ -1,10 +1,11 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { asc, createId, eq, like, sql } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import { searchEngines, users } from "@homarr/db/schema";
import { integrationCreator } from "@homarr/integrations";
import { validation, z } from "@homarr/validation";
import { validation } from "@homarr/validation";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
@@ -0,0 +1,52 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { and, eq } from "@homarr/db";
import { sectionCollapseStates, sections } from "@homarr/db/schema";
import { createTRPCRouter, protectedProcedure } from "../../trpc";
export const sectionRouter = createTRPCRouter({
changeCollapsed: protectedProcedure
.input(
z.object({
sectionId: z.string(),
collapsed: z.boolean(),
}),
)
.mutation(async ({ ctx, input }) => {
const section = await ctx.db.query.sections.findFirst({
where: and(eq(sections.id, input.sectionId), eq(sections.kind, "category")),
with: {
collapseStates: {
where: eq(sectionCollapseStates.userId, ctx.session.user.id),
},
},
});
if (!section) {
throw new TRPCError({
code: "NOT_FOUND",
message: `Section not found id=${input.sectionId}`,
});
}
if (section.collapseStates.length === 0) {
await ctx.db.insert(sectionCollapseStates).values({
sectionId: section.id,
userId: ctx.session.user.id,
collapsed: input.collapsed,
});
return;
}
await ctx.db
.update(sectionCollapseStates)
.set({
collapsed: input.collapsed,
})
.where(
and(eq(sectionCollapseStates.sectionId, section.id), eq(sectionCollapseStates.userId, ctx.session.user.id)),
);
}),
});
+3 -1
View File
@@ -1,7 +1,9 @@
import { z } from "zod";
import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
import type { ServerSettings } from "@homarr/server-settings";
import { defaultServerSettingsKeys } from "@homarr/server-settings";
import { validation, z } from "@homarr/validation";
import { validation } from "@homarr/validation";
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, publicProcedure } from "../trpc";
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
+3 -1
View File
@@ -812,7 +812,7 @@ describe("saveBoard should save full board", () => {
expect(integration).toBeUndefined();
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "modify");
});
it.each([[{ kind: "empty" as const }], [{ kind: "category" as const, name: "My first category" }]])(
it.each([[{ kind: "empty" as const }], [{ kind: "category" as const, collapsed: false, name: "My first category" }]])(
"should add section when present in input",
async (partialSection) => {
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
@@ -1023,6 +1023,7 @@ describe("saveBoard should save full board", () => {
yOffset: 1,
xOffset: 0,
name: "Test",
collapsed: true,
items: [],
},
{
@@ -1031,6 +1032,7 @@ describe("saveBoard should save full board", () => {
name: "After",
yOffset: 0,
xOffset: 0,
collapsed: false,
items: [],
},
],
+34 -62
View File
@@ -1,4 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
import type { Database } from "@homarr/db";
@@ -8,7 +9,7 @@ import { selectUserSchema } from "@homarr/db/validationSchemas";
import { credentialsAdminGroup } from "@homarr/definitions";
import type { SupportedAuthProvider } from "@homarr/definitions";
import { logger } from "@homarr/log";
import { validation, z } from "@homarr/validation";
import { validation } from "@homarr/validation";
import { convertIntersectionToZodObject } from "../schema-merger";
import {
@@ -21,6 +22,7 @@ import {
import { throwIfActionForbiddenAsync } from "./board/board-access";
import { throwIfCredentialsDisabled } from "./invite/checks";
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
import { changeSearchPreferencesAsync, changeSearchPreferencesInputSchema } from "./user/change-search-preferences";
export const userRouter = createTRPCRouter({
initUser: onboardingProcedure
@@ -214,6 +216,7 @@ export const userRouter = createTRPCRouter({
firstDayOfWeek: true,
pingIconsEnabled: true,
defaultSearchEngineId: true,
openSearchInNewTab: true,
}),
)
.meta({ openapi: { method: "GET", path: "/api/users/{userId}", tags: ["users"], protect: true } })
@@ -238,6 +241,7 @@ export const userRouter = createTRPCRouter({
firstDayOfWeek: true,
pingIconsEnabled: true,
defaultSearchEngineId: true,
openSearchInNewTab: true,
},
where: eq(users.id, input.userId),
});
@@ -404,7 +408,13 @@ export const userRouter = createTRPCRouter({
});
}
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.userId), "view");
// Only allow user to select boards they have access to
if (input.homeBoardId) {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.homeBoardId), "view");
}
if (input.mobileHomeBoardId) {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.mobileHomeBoardId), "view");
}
await ctx.db
.update(users)
@@ -416,40 +426,32 @@ export const userRouter = createTRPCRouter({
}),
changeDefaultSearchEngine: protectedProcedure
.input(
convertIntersectionToZodObject(validation.user.changeDefaultSearchEngine.and(z.object({ userId: z.string() }))),
convertIntersectionToZodObject(
validation.user.changeSearchPreferences.omit({ openInNewTab: true }).and(z.object({ userId: z.string() })),
),
)
.output(z.void())
.meta({ openapi: { method: "PATCH", path: "/api/users/changeSearchEngine", tags: ["users"], protect: true } })
.meta({
openapi: {
method: "PATCH",
path: "/api/users/changeSearchEngine",
tags: ["users"],
protect: true,
deprecated: true,
},
})
.mutation(async ({ input, ctx }) => {
const user = ctx.session.user;
// Only admins can change other users passwords
if (!user.permissions.includes("admin") && user.id !== input.userId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
const dbUser = await ctx.db.query.users.findFirst({
columns: {
id: true,
},
where: eq(users.id, input.userId),
await changeSearchPreferencesAsync(ctx.db, ctx.session, {
...input,
openInNewTab: undefined,
});
if (!dbUser) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
await ctx.db
.update(users)
.set({
defaultSearchEngineId: input.defaultSearchEngineId,
})
.where(eq(users.id, input.userId));
}),
changeSearchPreferences: protectedProcedure
.input(convertIntersectionToZodObject(changeSearchPreferencesInputSchema))
.output(z.void())
.meta({ openapi: { method: "PATCH", path: "/api/users/search-preferences", tags: ["users"], protect: true } })
.mutation(async ({ input, ctx }) => {
await changeSearchPreferencesAsync(ctx.db, ctx.session, input);
}),
changeColorScheme: protectedProcedure
.input(validation.user.changeColorScheme)
@@ -463,21 +465,6 @@ export const userRouter = createTRPCRouter({
})
.where(eq(users.id, ctx.session.user.id));
}),
getPingIconsEnabledOrDefault: publicProcedure.query(async ({ ctx }) => {
if (!ctx.session?.user) {
return false;
}
const user = await ctx.db.query.users.findFirst({
columns: {
id: true,
pingIconsEnabled: true,
},
where: eq(users.id, ctx.session.user.id),
});
return user?.pingIconsEnabled ?? false;
}),
changePingIconsEnabled: protectedProcedure
.input(validation.user.pingIconsEnabled.and(validation.common.byId))
.mutation(async ({ input, ctx }) => {
@@ -496,21 +483,6 @@ export const userRouter = createTRPCRouter({
})
.where(eq(users.id, ctx.session.user.id));
}),
getFirstDayOfWeekForUserOrDefault: publicProcedure.input(z.undefined()).query(async ({ ctx }) => {
if (!ctx.session?.user) {
return 1 as const;
}
const user = await ctx.db.query.users.findFirst({
columns: {
id: true,
firstDayOfWeek: true,
},
where: eq(users.id, ctx.session.user.id),
});
return user?.firstDayOfWeek ?? (1 as const);
}),
changeFirstDayOfWeek: protectedProcedure
.input(convertIntersectionToZodObject(validation.user.firstDayOfWeek.and(validation.common.byId)))
.output(z.void())
@@ -0,0 +1,50 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import type { Session } from "@homarr/auth";
import type { Modify } from "@homarr/common/types";
import { eq } from "@homarr/db";
import type { Database } from "@homarr/db";
import { users } from "@homarr/db/schema";
import { validation } from "@homarr/validation";
export const changeSearchPreferencesInputSchema = validation.user.changeSearchPreferences.and(
z.object({ userId: z.string() }),
);
export const changeSearchPreferencesAsync = async (
db: Database,
session: Session,
input: Modify<z.infer<typeof changeSearchPreferencesInputSchema>, { openInNewTab: boolean | undefined }>,
) => {
const user = session.user;
// Only admins can change other users passwords
if (!user.permissions.includes("admin") && user.id !== input.userId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
const dbUser = await db.query.users.findFirst({
columns: {
id: true,
},
where: eq(users.id, input.userId),
});
if (!dbUser) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
await db
.update(users)
.set({
defaultSearchEngineId: input.defaultSearchEngineId,
openSearchInNewTab: input.openInNewTab,
})
.where(eq(users.id, input.userId));
};
+1 -1
View File
@@ -1,8 +1,8 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { sendPingRequestAsync } from "@homarr/ping";
import { pingChannel, pingUrlChannel } from "@homarr/redis";
import { z } from "@homarr/validation";
import { createTRPCRouter, publicProcedure } from "../../trpc";
+2 -1
View File
@@ -1,7 +1,8 @@
import { z } from "zod";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { radarrReleaseTypes } from "@homarr/integrations/types";
import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar";
import { z } from "@homarr/validation";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
+1 -1
View File
@@ -1,4 +1,5 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import type { Modify } from "@homarr/common/types";
import type { Integration } from "@homarr/db/schema";
@@ -7,7 +8,6 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions";
import type { DownloadClientJobsAndStatus } from "@homarr/integrations";
import { downloadClientItemSchema, integrationCreator } from "@homarr/integrations";
import { downloadClientRequestHandler } from "@homarr/request-handler/downloads";
import { z } from "@homarr/validation";
import type { IntegrationAction } from "../../middlewares/integration";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
@@ -1,11 +1,11 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { integrationCreator, MediaRequestStatus } from "@homarr/integrations";
import type { MediaRequest } from "@homarr/integrations/types";
import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list";
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
import { z } from "@homarr/validation";
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
+1 -1
View File
@@ -1,9 +1,9 @@
import { TRPCError } from "@trpc/server";
import SuperJSON from "superjson";
import { z } from "zod";
import { eq } from "@homarr/db";
import { items } from "@homarr/db/schema";
import { z } from "@homarr/validation";
import { createTRPCRouter, protectedProcedure } from "../../trpc";
+32 -7
View File
@@ -1,12 +1,37 @@
import type { RssFeed } from "@homarr/cron-jobs";
import { createItemChannel } from "@homarr/redis";
import { z } from "zod";
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
import { createOneItemMiddleware } from "../../middlewares/item";
import { createTRPCRouter, publicProcedure } from "../../trpc";
export const rssFeedRouter = createTRPCRouter({
getFeeds: publicProcedure.unstable_concat(createOneItemMiddleware("rssFeed")).query(async ({ input }) => {
const channel = createItemChannel<RssFeed[]>(input.itemId);
return await channel.getAsync();
}),
getFeeds: publicProcedure
.input(
z.object({
urls: z.array(z.string()),
maximumAmountPosts: z.number(),
}),
)
.query(async ({ input }) => {
const rssFeeds = await Promise.all(
input.urls.map(async (url) => {
const innerHandler = rssFeedsRequestHandler.handler({
url,
count: input.maximumAmountPosts,
});
return await innerHandler.getCachedOrUpdatedDataAsync({
forceUpdate: false,
});
}),
);
return rssFeeds
.flatMap((rssFeed) => rssFeed.data.entries)
.slice(0, input.maximumAmountPosts)
.sort((entryA, entryB) => {
return entryA.published && entryB.published
? new Date(entryB.published).getTime() - new Date(entryA.published).getTime()
: 0;
});
}),
});
@@ -1,9 +1,9 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { integrationCreator } from "@homarr/integrations";
import { smartHomeEntityStateRequestHandler } from "@homarr/request-handler/smart-home-entity-state";
import { z } from "@homarr/validation";
import type { IntegrationAction } from "../../middlewares/integration";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
+5 -1
View File
@@ -6,7 +6,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
export const weatherRouter = createTRPCRouter({
atLocation: publicProcedure.input(validation.widget.weather.atLocationInput).query(async ({ input }) => {
const res = await fetchWithTimeout(
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min&current_weather=true&timezone=auto`,
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max&current_weather=true&timezone=auto`,
);
const json: unknown = await res.json();
const weather = await validation.widget.weather.atLocationOutput.parseAsync(json);
@@ -18,6 +18,10 @@ export const weatherRouter = createTRPCRouter({
weatherCode: weather.daily.weathercode[index] ?? 404,
maxTemp: weather.daily.temperature_2m_max[index],
minTemp: weather.daily.temperature_2m_min[index],
sunrise: weather.daily.sunrise[index],
sunset: weather.daily.sunset[index],
maxWindSpeed: weather.daily.wind_speed_10m_max[index],
maxWindGusts: weather.daily.wind_gusts_10m_max[index],
};
}),
};
+2 -2
View File
@@ -1,5 +1,5 @@
import type { AnyZodObject, ZodIntersection, ZodObject } from "@homarr/validation";
import { z } from "@homarr/validation";
import { z } from "zod";
import type { AnyZodObject, ZodIntersection, ZodObject } from "zod";
export function convertIntersectionToZodObject<TIntersection extends ZodIntersection<AnyZodObject, AnyZodObject>>(
intersection: TIntersection,
+15
View File
@@ -36,3 +36,18 @@ async function importCookiesAsync() {
.map(({ name, value }) => `${name}=${value}`)
.join(";");
}
function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin;
return `http://${process.env.HOSTNAME ?? "localhost"}:3000`;
}
export const trpcPath = "/api/trpc";
/**
* Creates the full url for the trpc api endpoint
* @returns
*/
export function getTrpcUrl() {
return `${getBaseUrl()}${trpcPath}`;
}
+1 -1
View File
@@ -9,6 +9,7 @@
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import type { OpenApiMeta } from "trpc-to-openapi";
import { ZodError } from "zod";
import type { Session } from "@homarr/auth";
import { FlattenError } from "@homarr/common";
@@ -16,7 +17,6 @@ import { userAgent } from "@homarr/common/server";
import { db } from "@homarr/db";
import type { GroupPermissionKey, OnboardingStep } from "@homarr/definitions";
import { logger } from "@homarr/log";
import { ZodError } from "@homarr/validation";
import { getOnboardingOrFallbackAsync } from "./router/onboard/onboard-queries";
Loaded 100 of 255 files, more files were not shown because too many files have changed in this diff. Show more