Compare commits

...
107 Commits
Author SHA1 Message Date
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
Releases Homarr 81a8cf4b36 chore(release): 1.0.1 [skip ci] 2025-01-17 19:14:12 +00:00
homarr-releases[bot] b5c008b4ad chore(release): automatic release v1.0.1 2025-01-17 19:13:04 +00:00
Toine Rademacher (toineenzo)andMeier Lukas 343438391b chore: Fixed broken links in README.md (#1985)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-01-17 17:39:40 +00:00
Toine Rademacher (toineenzo) 8400b9fb11 chore: Change icon and issue link to new repo in layout.tsx and index.tsx (#1986) 2025-01-17 17:35:20 +00:00
Meier Lukas fe65e73fff chore: add version 1.0.0 to bug issue template (#1983) 2025-01-17 18:23:58 +01:00
Meier Lukas e023edf9d6 fix: add missing old-import proxmox integration kind mapping (#1979) 2025-01-17 16:12:02 +01:00
Meier Lukas 927913cf4e fix: add error from ldap bind operation to log statement (#1980) 2025-01-17 16:11:50 +01:00
Meier Lukas 07d8103051 chore: update docs sitemap (#1982) 2025-01-17 16:11:29 +01:00
homarr-renovate[bot] fe08ccf589 chore(deps): update vitest monorepo to v3 (major) (#1964)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-17 16:10:59 +01:00
homarr-renovate[bot] c55207bd59 fix(deps): update dependency flag-icons to ^7.3.2 (#1981)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-17 15:09:46 +00:00
homarr-renovate[bot] e35811c921 fix(deps): update nextjs monorepo to ^15.1.5 (#1978)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-17 15:09:21 +00:00
Meier Lukas e481e116ff Merge branch 'main' into dev 2025-01-17 13:52:59 +01:00
Meier Lukas 32be09a4a7 docs: remove beta banner from readme 2025-01-17 13:51:34 +01:00
247 changed files with 15677 additions and 4840 deletions

No files matched your search

+6 -1
View File
@@ -31,6 +31,11 @@ body:
label: Version
description: What version of Homarr are you running?
options:
- 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
@@ -75,4 +80,4 @@ body:
- Other (describe in "additional information")
default: 0
validations:
required: false
required: false
+1 -1
View File
@@ -1 +1 @@
22.13.0
22.13.1
+93
View File
@@ -1,3 +1,96 @@
## [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
* add error from ldap bind operation to log statement ([#1980](https://github.com/homarr-labs/homarr/issues/1980)) ([927913c](https://github.com/homarr-labs/homarr/commit/927913cf4e0a6e4f3a6b7dbf9ae9034d0780b5fb))
* add missing old-import proxmox integration kind mapping ([#1979](https://github.com/homarr-labs/homarr/issues/1979)) ([e023edf](https://github.com/homarr-labs/homarr/commit/e023edf9d62417b054eb565a7f794267539eb992))
* **deps:** update dependency flag-icons to ^7.3.2 ([#1981](https://github.com/homarr-labs/homarr/issues/1981)) ([c55207b](https://github.com/homarr-labs/homarr/commit/c55207bd59f0c51869e8fdb36e083a8327ca46b1))
* **deps:** update nextjs monorepo to ^15.1.5 ([#1978](https://github.com/homarr-labs/homarr/issues/1978)) ([e35811c](https://github.com/homarr-labs/homarr/commit/e35811c921aea8c96dca398841ece396d71d4979))
## 1.0.0 (2025-01-17)
### ⚠ BREAKING CHANGES
+1 -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
+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.65.1",
"@tanstack/react-query-devtools": "^5.65.1",
"@tanstack/react-query-next-experimental": "^5.65.1",
"@trpc/client": "next",
"@trpc/next": "next",
"@trpc/react-query": "next",
@@ -62,11 +65,11 @@
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"dotenv": "^16.4.7",
"flag-icons": "^7.3.1",
"flag-icons": "^7.3.2",
"glob": "^11.0.1",
"jotai": "^2.11.0",
"jotai": "^2.11.1",
"mantine-react-table": "2.0.0-beta.8",
"next": "15.1.4",
"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/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": "^4.19.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"
@@ -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[];
@@ -48,11 +48,11 @@ 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: board.faviconImageUrl ?? undefined,
apple: board.faviconImageUrl ?? undefined,
},
appleWebApp: {
startupImage: { url: board.faviconImageUrl ? board.faviconImageUrl : "/logo/logo.png" },
startupImage: { url: 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 />
@@ -153,7 +153,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
{
label: t("items.help.items.submitIssue"),
icon: IconBrandGithub,
href: "https://github.com/ajnart/homarr/issues/new/choose",
href: "https://github.com/homarr-labs/homarr/issues/new/choose",
external: true,
},
{
@@ -165,7 +165,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
{
label: t("items.help.items.sourceCode"),
icon: IconGitFork,
href: "https://github.com/ajnart/homarr",
href: "https://github.com/homarr-labs/homarr",
external: true,
},
],
@@ -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";
+3 -1
View File
@@ -30,7 +30,9 @@ const handler = auth(async (req) => {
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,
});
+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 -10
View File
@@ -24,10 +24,10 @@
<strong>Demo ✨</strong>
</a>
•
<a href="https://homarr.dev/docs/getting-started/installation/">
<a href="https://homarr.dev/docs/category/installation-1/">
<strong>Install 💻</strong>
</a> •
<a href="https://translate.homarr.dev/">
<a href="https://crowdin.com/project/homarr_labs">
<strong>Translations 🈺</strong>
</a> •
<a href="https://discord.com/invite/aCsmEV5RgA">
@@ -35,13 +35,6 @@
</a>
</p>
> [!WARNING]
> This is a beta version of Homarr. You can download the old but stable version at https://github.com/ajnart/homarr/. Running this version is at your own risk.
Simplify the management of your server with Homarr - a sleek, modern dashboard that puts all of your apps and services at your fingertips. With Homarr, you can access and control everything in one location. Homarr seamlessly integrates with the apps you've added, providing you with valuable information and giving you complete control. Installation is a breeze, and Homarr supports a wide range of deployment methods.
<br/>
<br/>
![Features Section](./section-features.png)
@@ -100,7 +93,7 @@ Homarr has a [built-in collection of widgets and integrations](https://homarr.de
Since we are updating Homarr very frequently, we recommend reading our official installation guides:
<h2>
<a href="https://homarr.dev/docs/getting-started/installation/">
<a href="https://homarr.dev/docs/category/installation-1/">
Please click here for official installation instructions
</a>
</h2>
+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.0",
"version": "1.3.1",
"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": "^2.1.8",
"@vitest/ui": "^2.1.8",
"@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": "^2.1.8"
"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.4",
"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"
}
+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";
+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,
+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";
+7 -4
View File
@@ -2,9 +2,11 @@ import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapte
import { cookies } from "next/headers";
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { formatError } from "pretty-print-error";
import { db } from "@homarr/db";
import type { SupportedAuthProvider } from "@homarr/definitions";
import { logger } from "@homarr/log";
import { createAdapter } from "./adapter";
import { createSessionCallback } from "./callbacks";
@@ -26,15 +28,16 @@ export const createConfiguration = (
const adapter = createAdapter(db, provider);
return NextAuth({
logger: {
error: (code, ...message) => {
error: (error) => {
// Remove the big error message for failed login attempts
// as it is not useful for the user.
if (code.name === "CredentialsSignin") {
console.warn("The login attempt of a user was not successful.");
if (error.name === "CredentialsSignin") {
logger.warn("The login attempt of a user was not successful.");
return;
}
console.error(code, ...message);
logger.error(formatError(error));
logger.error(formatError(error.cause));
},
},
trustHost: true,
+1
View File
@@ -86,4 +86,5 @@ export const env = createEnv({
AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE: process.env.AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE,
},
skipValidation,
emptyStringAsUndefined: true,
});
+6 -4
View File
@@ -30,14 +30,16 @@
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@t3-oss/env-nextjs": "^0.11.1",
"@t3-oss/env-nextjs": "^0.12.0",
"bcrypt": "^5.1.1",
"cookies": "^0.9.1",
"ldapts": "7.3.1",
"next": "15.1.4",
"next": "15.1.6",
"next-auth": "5.0.0-beta.25",
"pretty-print-error": "^1.1.2",
"react": "19.0.0",
"react-dom": "19.0.0"
"react-dom": "19.0.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -45,7 +47,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "5.0.2",
"@types/cookies": "0.9.0",
"eslint": "^9.18.0",
"eslint": "^9.19.0",
"prettier": "^3.4.2",
"typescript": "^5.7.3"
}
Loaded 100 of 247 files, more files were not shown because too many files have changed in this diff. Show more