Compare commits

...
Author SHA1 Message Date
Manuel 168a951ad9 feat: notification system 2024-12-05 18:12:07 +01:00
Meier LukasandManuel 71f4f8c4c8 feat: pretty-print-error to print errors in console (#1596)
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
2024-12-05 17:24:23 +01:00
homarr-crowdin[bot]andMeier Lukas 86cd1017b9 chore(lang): updated translations from crowdin (#1612)
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-05 07:00:41 +01:00
Meier Lukas a5b366a92e chore: add skip_untranslated_strings option for crowdin download 2024-12-05 06:18:54 +01:00
homarr-renovate[bot] ef65270778 fix(deps): update nextjs monorepo to ^14.2.20 (#1610)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-04 21:38:54 +00:00
Meier Lukas f38f3438c9 chore: add workflow_dispatch to crowdin_upload for initial translation upload 2024-12-04 21:50:48 +01:00
Meier Lukas 096f03432c feat: integrate crowdin #1402 (#1599) 2024-12-04 21:43:02 +01:00
Meier Lukas bf45860966 fix(deps): update gridstack.js fork to v1.11.2 (#1605) 2024-12-04 21:38:29 +01:00
Meier Lukas 2686dad630 fix: remove unnecessary complex copy and build steps in dockerfile (#1595) 2024-12-04 21:37:27 +01:00
homarr-renovate[bot]andMeier Lukas 1037a747ea fix(deps): update dependency @dnd-kit/sortable to v10 (#1609)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-04 21:35:20 +01:00
homarr-renovate[bot] 6c7cdb3adb fix(deps): update dependency @dnd-kit/core to ^6.3.0 (#1608)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-04 21:27:51 +01:00
homarr-renovate[bot] d8006e7dcc chore(deps): update dependency @types/react to ^18.3.13 (#1607)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-04 16:41:40 +00:00
homarr-renovate[bot] 770bfc65e3 chore(deps): update node.js to v22.12.0 (#1601)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-04 14:02:22 +01:00
homarr-renovate[bot] feb93ef0c7 fix(deps): update testcontainers-node monorepo to ^10.16.0 (#1604)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-04 10:13:26 +00:00
homarr-renovate[bot] bbec01385f fix(deps): update dependency prettier to ^3.4.2 (#1602)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-04 10:04:13 +00:00
homarr-renovate[bot] 2e23b3dad8 fix(deps): update dependency drizzle-kit to ^0.29.1 (#1603)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-04 09:48:52 +00:00
homarr-renovate[bot] 825b1a3643 fix(deps): update dependency sass to ^1.82.0 (#1600)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-03 23:28:19 +00:00
homarr-renovate[bot] 2193b6d8f8 chore(deps): update node.js to v22.12.0 (#1598)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-03 21:11:43 +00:00
homarr-renovate[bot] 31cc1abb51 fix(deps): update dependency dotenv to ^16.4.7 (#1597)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-03 17:48:43 +00:00
homarr-renovate[bot] fb1f0003e6 fix(deps): update dependency undici to v7.1.0 (#1593)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-03 17:11:35 +00:00
homarr-renovate[bot] b0d623047e fix(deps): update nextjs monorepo to ^14.2.19 (#1594)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-03 16:40:29 +00:00
homarr-renovate[bot] 16cf818449 fix(deps): update dependency drizzle-orm to ^0.37.0 (#1592)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-03 14:31:30 +00:00
homarr-renovate[bot] 3b705f43b8 fix(deps): update tanstack-query monorepo to ^5.62.2 (#1590)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-03 14:11:32 +00:00
homarr-renovate[bot] a8e045b223 fix(deps): update dependency drizzle-kit to ^0.29.0 (#1591)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-03 13:49:59 +00:00
homarr-renovate[bot] 57bbd43c74 fix(deps): update dependency sass to ^1.81.1 (#1589)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-03 03:49:38 +00:00
homarr-renovate[bot] 716a2ab9d1 fix(deps): update dependency dotenv to ^16.4.6 (#1588)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-03 03:23:47 +00:00
homarr-renovate[bot] 23cc4659c4 fix(deps): update dependency @tabler/icons-react to ^3.24.0 (#1587)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-02 21:37:55 +00:00
SeDemal a1a6dc9d6e fix: Allow username to be capitalized on basic credentials login (#1585) 2024-12-02 20:44:49 +01:00
homarr-renovate[bot] 0e01dd8695 fix(deps): update tanstack-query monorepo to ^5.62.1 (#1584)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-02 19:10:00 +00:00
SeDemal 70ab29454a fix: fix binary conflicts for linux (#1583) 2024-12-02 19:28:45 +01:00
homarr-renovate[bot] 5ecf93a317 fix(deps): update dependency typescript-eslint to ^8.17.0 (#1582)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-02 17:42:03 +00:00
homarr-renovate[bot] 7f2713b0b9 chore(deps): update vitest monorepo to ^2.1.8 (#1581)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-02 15:12:49 +00:00
homarr-renovate[bot] 838f7aae4d chore(deps): update vitest monorepo to ^2.1.7 (#1580)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-02 10:37:50 +00:00
homarr-renovate[bot] 7343bdde13 fix(deps): update dependency trpc-to-openapi to ^2.1.0 (#1579)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-02 03:49:57 +00:00
homarr-renovate[bot] 3ffdc05442 fix(deps): update dependency trpc-to-openapi to ^2.0.4 (#1578)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-01 22:31:13 +00:00
Manuel a53a2cdd34 feat: add prettier packagejson plugin (#1577) 2024-12-01 22:59:11 +01:00
homarr-renovate[bot] 0ab9d0bd69 fix(deps): update dependency trpc-to-openapi to ^2.0.3 (#1576)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-01 04:03:12 +00:00
homarr-renovate[bot] fb4a19bfe4 fix(deps): update dependency tldts to ^6.1.65 (#1575)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-30 18:35:59 +00:00
Meier Lukas b277f444b2 fix: trailing slash integration url issues (#1571) 2024-11-30 10:54:50 +01:00
homarr-renovate[bot] d5f76218cd fix(deps): update dependency ldapts to v7.2.2 (#1574)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-29 23:11:24 +00:00
homarr-renovate[bot] 9968f149c6 chore(deps): update dependency eslint to ^9.16.0 (#1573)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-29 21:48:31 +00:00
homarr-renovate[bot] 6ad5fd5529 fix(deps): update tanstack-query monorepo to ^5.62.0 (#1569)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-29 13:44:12 +00:00
homarr-renovate[bot] 9b6eafc5fa chore(deps): update pnpm to v9.14.4 (#1568)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-29 11:25:53 +00:00
homarr-renovate[bot] cebc925b87 fix(deps): update dependency mysql2 to v3.11.5 (#1567)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-28 21:27:13 +00:00
homarr-renovate[bot] fd42bde76d fix(deps): update dependency @tabler/icons-react to ^3.23.0 (#1566)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-28 20:12:47 +00:00
Meier Lukas 2d7efc3ffa fix: open-api doc generation failing, post patch and delete not exported (#1565) 2024-11-28 20:21:29 +01:00
homarr-renovate[bot]andMeier Lukas 44e2105a8c fix(deps): update dependency undici to v7 (#1560)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-11-28 18:10:29 +01:00
homarr-renovate[bot]andMeier Lukas 4df6006f0e fix(deps): update dependency typescript-eslint to ^8.16.0 (#1544)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-11-28 18:00:41 +01:00
homarr-renovate[bot] c1f2ede834 chore(deps): update pnpm to v9.14.3 (#1564)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-28 16:54:39 +00:00
homarr-renovate[bot] 85a7b7df55 chore(deps): update dependency @types/node to ^22.10.1 (#1562)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-28 13:36:53 +00:00
homarr-renovate[bot] 09dae45ae8 fix(deps): update mantine monorepo to ^7.14.3 (#1563)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-28 13:01:56 +00:00
Meier Lukas 52d2a88b61 fix: moving categories up and down is not working (#1542) 2024-11-27 21:00:10 +01:00
homarr-renovate[bot] f2e7349cbd fix(deps): update tanstack-query monorepo to ^5.61.5 (#1559)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-27 17:48:58 +00:00
homarr-renovate[bot] fc734e5931 chore(deps): update turbo monorepo to ^2.3.3 (#1558)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-27 17:42:19 +00:00
homarr-renovate[bot] 9f982906f2 fix(deps): update dependency video.js to ^8.20.0 (#1561)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-27 17:31:08 +00:00
homarr-renovate[bot] f2268c8041 fix(deps): update dependency @million/lint to v1.0.13 (#1557)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-26 23:28:05 +00:00
homarr-renovate[bot] c1fe6551ce fix(deps): update tiptap monorepo to v2.10.3 (#1556)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-26 17:31:54 +00:00
homarr-renovate[bot] 91a199f7d6 fix(deps): update tanstack-query monorepo to ^5.61.4 (#1555)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-26 13:52:34 +00:00
homarr-renovate[bot] 27b8f65238 fix(deps): update dependency prettier to ^3.4.1 (#1554)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-26 13:45:22 +00:00
homarr-renovate[bot] 37f961c8cc chore(deps): update vitest monorepo to ^2.1.6 (#1553)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-26 13:31:41 +00:00
homarr-renovate[bot] 63f863748e chore(deps): update dependency @vitejs/plugin-react to ^4.3.4 (#1552)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-26 11:25:24 +00:00
homarr-renovate[bot] 907c28c945 fix(deps): update dependency next-intl to v3.25.3 (#1551)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-26 10:36:21 +00:00
homarr-renovate[bot] 2e9b99dff7 fix(deps): update dependency prettier to ^3.4.0 (#1550)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-26 05:32:01 +00:00
homarr-renovate[bot] 93aedd994f fix(deps): update dependency better-sqlite3 to ^11.6.0 (#1548)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-26 04:14:30 +00:00
homarr-renovate[bot] bfe284cdd1 chore(deps): update dependency @types/node to ^22.10.0 (#1549)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-26 03:54:50 +00:00
homarr-renovate[bot] 591c5b5cff chore(deps): update turbo monorepo to ^2.3.2 (#1547)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-25 22:11:35 +00:00
homarr-renovate[bot] 3cbc20bdfe chore(deps): update dependency @types/node to ^22.9.4 (#1546)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-25 21:27:45 +00:00
homarr-renovate[bot] b6c28da32b fix(deps): update dependency next-intl to v3.25.2 (#1545)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-25 19:09:38 +00:00
Meier Lukas 7e349bb04b refactor: improve design of no integrations found (#1543)
* refactor: improve design of no integrations found

* fix: deepsource issue
2024-11-25 17:31:14 +01:00
homarr-renovate[bot] b68977c52c fix(deps): update dependency @dnd-kit/sortable to v9 (#1535)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-24 13:47:50 +01:00
Meier Lukas 74e1b6b544 fix: transfer group not invalidating path (#1539) 2024-11-24 13:45:00 +01:00
Meier Lukas 1c4d4f4e9d fix: rename default to home board (#1538) 2024-11-24 13:44:43 +01:00
Meier Lukas ecf299e03b fix: icon picker suspense issue (#1533)
* fix: icon picker suspense issue

* fix: remove unused variable
2024-11-24 13:44:15 +01:00
Meier Lukas 225356cc4f fix: add beforeunload logic for board in edit mode (#1531) 2024-11-24 13:43:50 +01:00
Meier Lukas 32e6b8ee45 fix: external users can be added to groups manually (#1540) 2024-11-24 13:43:25 +01:00
homarr-renovate[bot] 2e2814707e fix(deps): update mantine monorepo to ^7.14.2 (#1537)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-24 10:49:24 +00:00
homarr-renovate[bot] 24daba743e fix(deps): update tanstack-query monorepo to ^5.61.3 (#1536)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-24 03:56:56 +00:00
Manuel d76b4d0ec1 feat: #1243 add api routes (#1286) 2024-11-23 22:05:44 +01:00
homarr-renovate[bot]andMeier Lukas 982ab4393e chore(deps): update dependency typescript to ^5.7.2 (#1523)
* chore(deps): update dependency typescript to ^5.7.2

* chore(deps): update dependency typescript to ^5.7.2

* fix: typecheck issue

* fix: typecheck issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-11-23 19:39:20 +01:00
homarr-renovate[bot] 5a3d9745d9 fix(deps): update dependency @dnd-kit/core to ^6.2.0 (#1534)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-23 17:27:37 +00:00
Meier Lukas 4a94d602c0 fix: auth broken cause of consistent table column rename (#1532) 2024-11-23 17:43:28 +01:00
Meier Lukas 9fb14df210 feat: replace home page with home board (#1530) 2024-11-23 17:19:46 +01:00
Meier Lukas 38143b679e fix: matcher for translation middleware only supports en and de (#1528) 2024-11-23 17:19:04 +01:00
Meier Lukas 1fc48f9db0 feat: add permission section to create user page (#1524)
* feat: add permission section to create user page

* fix: deepsource issues
2024-11-23 17:18:29 +01:00
Meier Lukas 8fea983c2e refactor: make casing for column names consistent (#1517)
* refactor: make casing for column names consistent

* fix: format issues
2024-11-23 17:17:00 +01:00
Meier Lukas 32ee9f3dcc refactor: add request handlers for centralized cached requests (#1504)
* feat: add object base64 hash method

* chore: add script to add package

* feat: add request-handler package

* wip: add request handlers for all jobs and widget api procedures

* wip: remove errors shown in logs, add missing decryption for secrets in cached-request-job-handler

* wip: highly improve request handler, add request handlers for calendar, media-server, indexer-manager and more, add support for multiple inputs from job handler creator

* refactor: move media-server requests to request-handler, add invalidation logic for dns-hole and media requests

* refactor: remove unused integration item middleware

* feat: add invalidation to switch entity action of smart-home

* fix: lint issues

* chore: use integration-kind-by-category instead of union for request-handlers

* fix: build not working for tasks and websocket

* refactor: add more logs

* refactor: readd timestamp logic for diconnect status

* fix: lint and typecheck issue

* chore: address pull request feedback
2024-11-23 17:16:44 +01:00
homarr-renovate[bot] cdfb61fb28 fix(deps): update dependency tldts to ^6.1.64 (#1529)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-23 14:39:52 +00:00
homarr-renovate[bot] 482ca497f4 chore(deps): update dependency @types/node to ^22.9.3 (#1527)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-23 05:12:09 +00:00
homarr-renovate[bot] 219a6adedd chore(deps): update dependency @types/node to ^22.9.2 (#1526)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-23 02:15:25 +00:00
homarr-renovate[bot] 50c96833a6 fix(deps): update dependency drizzle-orm to ^0.36.4 (#1522)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-22 12:21:14 +00:00
homarr-renovate[bot] 6018b20d4e fix(deps): update testcontainers-node monorepo to ^10.15.0 (#1521)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-22 11:25:35 +00:00
homarr-renovate[bot] ba4c9a683e fix(deps): update tiptap monorepo to v2.10.2 (#1520)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-22 10:36:00 +00:00
homarr-renovate[bot] 86ae346979 fix(deps): update dependency tldts to ^6.1.63 (#1519)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-22 02:20:10 +00:00
homarr-renovate[bot] 06e4623f8d chore(deps): update dependency dotenv-cli to ^7.4.4 (#1518)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-21 19:23:10 +00:00
homarr-renovate[bot] 85a69b13e3 fix(deps): update tiptap monorepo to v2.10.1 (#1516)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-21 15:31:36 +00:00
homarr-renovate[bot] a9a3a4cdce chore(deps): update turbo monorepo to ^2.3.1 (#1515)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-21 15:12:29 +00:00
Meier Lukas 2ffa9d0e71 fix: dispatch request-logger hides negative numbers, dates and date times (#1506)
* fix: dispatch request-logger hides negative numbers, dates and date times

* test: add unit test for undici log agent

* fix: deepsource issue
2024-11-20 22:36:10 +01:00
Meier Lukas 73cb0b99ac fix: items can not be placed in newly created dynamic section without saving (#1513)
* fix: items can not be placed in newly created dynamic section without saving

* docs: add comments above hooks with notice about specific order
2024-11-20 22:35:27 +01:00
Manuel 563c5d8f3c feat: add readarr integration (#1446) 2024-11-20 22:23:25 +01:00
Meier Lukas 72eda1f225 fix: api keys authentication does not work #1511 (#1512)
* fix: api keys authentication does not work #1511

* chore: add ip and user-agent to logs of unauthenticated api-keys
2024-11-20 21:55:04 +01:00
homarr-renovate[bot] a2a65abd3d fix(deps): update dependency tldts to ^6.1.62 (#1514)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-20 20:41:36 +00:00
Manuel ed74604a54 feat: add lidarr integration (#1433) 2024-11-20 19:03:09 +01:00
homarr-renovate[bot] ed10d63a04 fix(deps): update tanstack-query monorepo to ^5.61.0 (#1510)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-20 13:42:31 +00:00
homarr-renovate[bot] ae6137710f fix(deps): update dependency jotai to ^2.10.3 (#1509)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-20 13:27:36 +00:00
homarr-renovate[bot] a27a2c3da1 chore(deps): update pnpm to v9.14.2 (#1508)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-20 11:25:09 +00:00
homarr-renovate[bot] 860d2e4f69 fix(deps): update tiptap monorepo to v2.10.0 (#1507)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-20 10:36:07 +00:00
homarr-renovate[bot] 35f13d2a01 chore(deps): update pnpm to v9.14.1 (#1505)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-20 02:18:58 +00:00
homarr-renovate[bot] 21dc2138b7 chore(deps): update dependency vite-tsconfig-paths to ^5.1.3 (#1503)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-19 21:27:20 +00:00
Manuel 441cbbe717 feat: #1408 improve icon picker design (#1412)
* feat: #1408 improve icon picker design

* fix: formatting

* fix: ui

* feat: pr feedback
2024-11-19 21:59:33 +01:00
homarr-renovate[bot] 0a46a8e477 chore(deps): update dependency @types/better-sqlite3 to v7.6.12 (#1502)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-19 18:52:05 +00:00
homarr-renovate[bot] fa625df16d chore(deps): update dependency @types/node to ^22.9.1 (#1501)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-19 17:48:24 +00:00
Meier Lukas 18b579eb5f fix: user is undefined when using use-suspense-query (#1498) 2024-11-19 17:16:50 +01:00
homarr-renovate[bot] 2f76e537b7 fix(deps): update tanstack-query monorepo to ^5.60.6 (#1500)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-19 11:10:41 +00:00
homarr-renovate[bot] 7ea9154b67 chore(deps): update dependency dotenv-cli to ^7.4.3 (#1499)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-19 10:50:36 +00:00
homarr-renovate[bot]andMeier Lukas e233d60f87 chore(deps): update dependency eslint to ^9.15.0 (#1490)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-11-18 19:34:07 +01:00
homarr-renovate[bot] b8877137ac fix(deps): update dependency typescript-eslint to ^8.15.0 (#1497)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-18 18:38:08 +01:00
homarr-renovate[bot] 6e772e0585 chore(deps): update dependency @types/dockerode to ^3.3.32 (#1496)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-18 06:54:44 +00:00
Thomas Camlong 4ba9e94dd6 fix: fix i18n-ally extension config (#1462)
* fix: fix i18n-ally extension config

* fix: update scopeRangeRegex to support useScopedI18n and remove unused locales path
2024-11-17 21:37:01 +01:00
Meier Lukas 0ee343b99e feat: add more group permissions (#1453)
* feat: add more group permissions

* feat: restrict access with app permissions

* feat: restrict access with search-engine permissions

* feat: restrict access with media permissions

* refactor: remove permissions for users, groups and invites

* test: adjust app router tests with app permissions

* fix: integration page accessible without session

* fix: search for users, groups and integrations shown to unauthenticated users

* chore: address pull request feedback
2024-11-17 21:31:25 +01:00
Meier Lukas 879aa1152f fix: make oldmarr app schema less restrictive (#1495) 2024-11-17 21:31:08 +01:00
Meier Lukas 1105f0029f fix: sessions from inactive providers can still be used (#1458)
* fix: sessions from inactive providers can still be used

* fix(lint): dependency keys not sorted

* chore: address pull request feedback
2024-11-17 21:30:46 +01:00
Meier Lukas f3ebd6ccdf fix: everyone group page throws error (#1450) 2024-11-17 09:49:23 +01:00
homarr-renovate[bot] 775d0a53de fix(deps): update mantine monorepo to ^7.14.1 (#1494)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-16 15:39:09 +00:00
homarr-renovate[bot] 70f9dc5460 fix(deps): update dependency @ctrl/deluge to v7 (#1468)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-16 13:15:07 +01:00
homarr-renovate[bot] 27931be982 fix(deps): update dependency @ctrl/qbittorrent to ^9.1.0 (#1466)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-16 11:48:25 +00:00
homarr-renovate[bot] e9a74a01b9 fix(deps): update dependency @auth/drizzle-adapter to ^1.7.4 (#1493)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-16 11:33:59 +00:00
homarr-renovate[bot] d4f0aa761d fix(deps): update dependency @auth/core to ^0.37.4 (#1492)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-16 11:24:09 +00:00
homarr-renovate[bot] 6dcf1ad344 fix(deps): update tanstack-query monorepo to ^5.60.5 (#1489)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-15 18:38:16 +00:00
homarr-renovate[bot] 41f816be5b fix(deps): update tanstack-query monorepo to ^5.60.4 (#1488)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-15 16:13:53 +00:00
homarr-renovate[bot] 2fb014f2df fix(deps): update dependency drizzle-orm to ^0.36.3 (#1487)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-15 15:11:31 +00:00
homarr-renovate[bot] 908d0bbc50 fix(deps): update dependency sass to ^1.81.0 (#1486)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-15 03:40:16 +00:00
homarr-renovate[bot] 36fc707705 chore(deps): update pnpm to v9.13.2 (#1485)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-15 03:09:18 +00:00
homarr-renovate[bot] 5d38030841 chore(deps): update turbo monorepo to ^2.3.0 (#1484)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-14 18:38:03 +00:00
homarr-renovate[bot] 4264f2320e chore(deps): update pnpm to v9.13.1 (#1483)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-14 17:40:48 +00:00
homarr-renovate[bot] 12c3bb9848 fix(deps): update tanstack-query monorepo to ^5.60.2 (#1482)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-14 13:24:45 +00:00
homarr-renovate[bot] 9bc585648c fix(deps): update dependency drizzle-orm to ^0.36.2 (#1481)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-14 11:24:18 +00:00
homarr-renovate[bot] acc440bb89 fix(deps): update dependency drizzle-kit to ^0.28.1 (#1480)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-14 10:49:29 +00:00
homarr-renovate[bot] ccee3dd217 fix(deps): update nextjs monorepo to ^14.2.18 (#1479)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-13 21:35:08 +00:00
homarr-renovate[bot] 2edb9c701a chore(deps): update vitest monorepo to ^2.1.5 (#1478)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-13 15:40:29 +00:00
homarr-renovate[bot] 07f89afea8 fix(deps): update dependency undici to v6.21.0 (#1477)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-13 13:48:15 +00:00
homarr-renovate[bot] 684b542979 chore(deps): update pnpm to v9.13.0 (#1476)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-13 13:20:41 +00:00
homarr-renovate[bot] 3194733e95 chore(deps): update dependency node-loader to ^2.1.0 (#1475)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-13 12:57:16 +00:00
homarr-renovate[bot] 3ca97fef0b fix(deps): update dependency tldts to ^6.1.61 (#1474)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-13 09:28:55 +00:00
homarr-renovate[bot] 7a9e9a1e0d fix(deps): update dependency next-intl to v3.25.1 (#1473)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-13 08:50:49 +00:00
homarr-renovate[bot] 9bd0e0f4ee fix(deps): update dependency sass to ^1.80.7 (#1472)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-12 23:35:14 +00:00
homarr-renovate[bot] a1fddaa6a6 fix(deps): update dependency @tabler/icons-react to ^3.22.0 (#1471)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-12 22:10:25 +00:00
homarr-renovate[bot] 11ce6faa37 fix(deps): update mantine monorepo to ^7.14.0 (#1470)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-12 15:39:11 +00:00
homarr-renovate[bot] f220bb6244 fix(deps): update dependency @ctrl/transmission to ^7.1.0 (#1467)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-11 20:48:38 +00:00
homarr-renovate[bot] 53acff9143 fix(deps): update dependency typescript-eslint to ^8.14.0 (#1465)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-11 17:36:55 +00:00
homarr-renovate[bot] 75d764bca8 fix(deps): update dependency jotai to ^2.10.2 (#1464)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-11 15:28:21 +00:00
homarr-renovate[bot] 2a45c5b946 chore(deps): update dependency vite-tsconfig-paths to ^5.1.2 (#1461)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-10 17:26:20 +00:00
homarr-renovate[bot] 4ab40c6fa8 fix(deps): update dependency tldts to ^6.1.60 (#1460)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-10 10:30:11 +00:00
homarr-renovate[bot] ce32b4839d fix(deps): update dependency @million/lint to v1.0.12 (#1459)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-10 09:48:24 +00:00
SeDemal ceafd3747b fix: remove unused properties in SabNZBd schema (#1415)
* fix: Add boolean to retry entry for sabnzbd history schema

* fix: remove unused values from schema
2024-11-10 10:31:41 +01:00
Yossi Hillali 15abfbbdf7 fix: notebook save button on align buttons (#1456) 2024-11-10 10:28:31 +01:00
homarr-renovate[bot] fc4cddcbac fix(deps): update dependency winston to v3.17.0 (#1457)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-10 03:39:40 +00:00
Meier Lukas 04895da08a fix: actions user has no permission to are shown in spotlight (#1452) 2024-11-09 10:45:13 +01:00
homarr-renovate[bot] 747922f558 chore(deps): update dependency vite-tsconfig-paths to ^5.1.1 (#1455)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-09 04:31:38 +00:00
Meier Lukas 0aa72d118c fix: websockets on https do not work (#1444) 2024-11-08 21:30:05 +01:00
homarr-renovate[bot] 2306ecda9f fix(deps): update mantine monorepo to ^7.13.5 (#1449)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-08 10:48:32 +00:00
homarr-renovate[bot] 87221093d2 fix(deps): update dependency next-intl to v3.25.0 (#1448)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-08 10:32:59 +00:00
Yossi Hillali c51c5db3d7 feat: date format on weather widget (#1443)
* feat: date format on weather widget

* fix: type check error

* fix: reviewed changes

* fix: reviewed changes
2024-11-08 11:33:03 +02:00
ManuelandMeier Lukas aa503992af feat: #1047 add overseerr search (#1411)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-11-08 09:43:25 +01:00
homarr-renovate[bot] 2a7d648049 fix(deps): update dependency tldts to ^6.1.59 (#1447)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-08 00:48:19 +00:00
homarr-renovate[bot] 27b8692a27 chore(deps): update actions/checkout action to v4 (#1431)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-07 20:55:44 +01:00
Meier Lukas fe4dfc484b fix: adding volume /appdata removes created directories (#1445) 2024-11-07 19:48:45 +01:00
homarr-renovate[bot] 23574359f0 fix(deps): update dependency swagger-ui-react to ^5.18.2 (#1442)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-07 14:28:57 +00:00
homarr-renovate[bot] c6a1aeef4d fix(deps): update dependency drizzle-kit to ^0.28.0 (#1439)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-06 20:48:30 +00:00
homarr-renovate[bot] 7cd3156bd6 fix(deps): update dependency @ianvs/prettier-plugin-sort-imports to ^4.4.0 (#1441)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-06 20:38:07 +00:00
homarr-renovate[bot] 7966476cad fix(deps): update dependency drizzle-orm to ^0.36.1 (#1440)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-06 20:29:23 +00:00
homarr-renovate[bot] 10cebff4ed fix(deps): update dependency drizzle-kit to ^0.27.2 (#1437)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-06 14:48:31 +00:00
homarr-renovate[bot] e05567d100 fix(deps): update tanstack-query monorepo to ^5.59.20 (#1438)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-06 14:41:05 +00:00
homarr-renovate[bot] cd18458d0c fix(deps): update nextjs monorepo to ^14.2.17 (#1435)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-06 00:49:02 +00:00
homarr-renovate[bot] 31c661bc69 fix(deps): update testcontainers-node monorepo to ^10.14.0 (#1434)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-05 23:26:51 +00:00
homarr-renovate[bot] 0809d5aa74 fix(deps): update dependency swagger-ui-react to ^5.18.1 (#1432)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-05 22:11:01 +00:00
Meier Lukas 9535154128 ci: add e2e workflow (#1422)
* ci: add e2e workflow

* fix: skip showing error

* fix: when e2e skipped, e2e still executed

* fix: job env not accessible in if

* fix: env not accessible at all in job if statement

* fix: wrong env used for step ifs

* ci: add reusable e2e-execution workflow

* fix: e2e trigger fails not supported reusable workflow within steps

* fix: composite e2e-execution action not found

* fix: reference to composite action not working without checkout

* fix: composite action has to be named action.yml

* fix: id for docker should not have spaces

* fix: shell property missing

* fix: typo one dash to much

* ci: add e2e tests

* chore: change node-version to 22

* chore: move e2e tests to code-quality workflow as duration of e2e step is about 4 minutes

* fix: deepsource issues
2024-11-05 21:11:14 +01:00
Meier Lukas 319fb47744 fix: change node-version for composite setup action to 22, remove unnecessary installation of node from docker deployment (#1429) 2024-11-05 21:06:40 +01:00
homarr-renovate[bot] 7e1305c6a9 fix(deps): update dependency mysql2 to v3.11.4 (#1430)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-05 18:14:20 +00:00
homarr-renovate[bot] 4643c0f004 fix(deps): update dependency swagger-ui-react to ^5.18.0 (#1428)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-05 14:41:40 +00:00
homarr-renovate[bot] 68a72ce5e3 chore(deps): update dependency @types/node to ^22.9.0 (#1426)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-05 13:48:27 +00:00
homarr-renovate[bot] e332a6fcbe fix(deps): update dependency @drizzle-team/brocli to ^0.11.0 (#1427)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-05 13:37:45 +00:00
Meier Lukas b7fc1ae4bb fix: middleware fetch failed for docker container (#1424)
* fix: middleware fetch failed for docker container

* fix: lint warning
2024-11-04 20:33:41 +01:00
Meier Lukas cf5bcab732 fix: credentials login not working cause of cookie secure flag not possible for http (#1421)
* fix: credentials login not working cause of cookie secure flag not possible for http

* chore: add missing comment

* fix: lint issue
2024-11-04 19:20:19 +01:00
homarr-renovate[bot] 97eb4c54e2 fix(deps): update dependency typescript-eslint to ^8.13.0 (#1420)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-04 17:48:14 +00:00
homarr-renovate[bot] 8f393aa9e8 chore(deps): update dependency vite-tsconfig-paths to ^5.1.0 (#1419)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-04 17:38:21 +00:00
homarr-renovate[bot] b45eeb95e9 fix(deps): update dependency @auth/drizzle-adapter to ^1.7.3 (#1417)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-04 13:25:23 +00:00
homarr-renovate[bot] 90c105541e chore(deps): update dependency concurrently to ^9.1.0 (#1418)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-04 12:59:18 +00:00
homarr-renovate[bot] 7a07f57d0c fix(deps): update dependency @auth/core to ^0.37.3 (#1416)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-04 11:26:11 +00:00
Manuel f31ccf2d6a feat: #1408 add local icon repository (#1413) 2024-11-04 10:02:54 +01:00
homarr-renovate[bot] aec9ec1efd chore(deps): update dependency @types/node to ^22.8.7 (#1409)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-03 14:48:26 +00:00
homarr-renovate[bot] 8983734408 fix(deps): update tanstack-query monorepo to ^5.59.19 (#1410)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-03 14:39:40 +00:00
Manuel 0c067c4e6f feat: use consistent line ending (#1407) 2024-11-03 00:44:34 +01:00
ManuelandMeier Lukas 34506b6339 feat: #1254 display group owner (#1406)
* feat: #1254 display group owner

* fix: format

* fix: format issue

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-11-03 00:33:49 +01:00
Meier Lukas 0ff7c8903b feat: add support for all languages from old homarr (#1394)
* feat: add support for all languages from old homarr

* fix: add mantine-react-table translations, remove arabic language

* refactor: change translations to json for better crowdin support

* fix: issues with loading dayjs and mantine-react-table translations

* chore: add additional translations with variables

* fix: format and deepsource issues

* fix: test failing because of missing coverage exclusions

* fix: format issues

* fix: format issue
2024-11-03 00:11:27 +01:00
Manuel b294bf68cf feat: #511 log user logged in (#1405) 2024-11-03 00:06:49 +01:00
homarr-renovate[bot] deb5a716db fix(deps): update tanstack-query monorepo to ^5.59.17 (#1403)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-02 20:37:48 +00:00
Meier Lukas 326b769c23 feat: add server settings for default board, default color scheme and default locale (#1373)
* feat: add server settings for default board, default color scheme and default locale

* chore: address pull request feedback

* test: adjust unit tests to match requirements

* fix: deepsource issue

* chore: add deepsource as dependency to translation library

* refactor: restructure language-combobox, adjust default locale for next-intl

* chore: change cookie keys prefix from homarr- to homarr.
2024-11-02 21:15:46 +01:00
ManuelandMeier Lukas 49c0ebea6d feat: add bookmark widget (#964)
* feat: add bookmark widget

* fix: item component type issue, widget-ordered-object-list-input item component issue

* feat: add button in items list

* wip

* wip: bookmark options dnd

* wip: improve widget sortable item list

* feat: add sortable item list input to widget edit modal

* feat: implement bookmark widget

* chore: address pull request feedback

* fix: format issues

* fix: lockfile not up to date

* fix: import configuration missing and apps not imported

* fix: bookmark items not sorted

* feat: add flex layouts to bookmark widget

* fix: deepsource issue

* fix: add missing layout bookmarks old-import options mapping

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-11-02 18:44:36 +01:00
homarr-renovate[bot] f8bdd9c5c7 chore(deps): update dependency @types/ws to ^8.5.13 (#1401)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-02 09:27:16 +00:00
homarr-renovate[bot] d908b3f676 fix(deps): update dependency sass to ^1.80.6 (#1399)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-02 08:32:26 +00:00
homarr-renovate[bot] baabf71707 fix(deps): update dependency tldts to ^6.1.58 (#1400)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-02 08:14:10 +00:00
homarr-renovate[bot] 02ddb1c9c6 fix(deps): update dependency winston to v3.16.0 (#1398)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-01 21:34:11 +00:00
homarr-renovate[bot] 3ff8539fec chore(deps): update dependency eslint to ^9.14.0 (#1396)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-01 19:24:24 +00:00
homarr-renovate[bot] 3bfdc2ded4 fix(deps): update dependency drizzle-kit to ^0.27.1 (#1395)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-11-01 19:09:49 +00:00
homarr-renovate[bot] 747660ee69 chore(deps): update dependency @types/node to ^22.8.6 (#1393)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-31 18:37:37 +00:00
homarr-renovate[bot]andMeier Lukas 17401d1d72 fix(deps): update dependency typescript-eslint to ^8.12.2 (#1381)
* fix(deps): update dependency typescript-eslint to ^8.12.2

* 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>
2024-10-31 16:00:47 +01:00
homarr-renovate[bot] 134ecf1ee7 chore(deps): update dependency node to v22 (#1385)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-31 14:40:09 +01:00
homarr-renovate[bot] 4e9b0af54c fix(deps): update dependency next-intl to v3.24.0 (#1392)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-31 07:27:29 +00:00
homarr-renovate[bot] fd2ce2eee2 chore(deps): update dependency @types/node to ^20.17.4 (#1391)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-31 06:15:30 +00:00
homarr-renovate[bot] 74a8495903 fix(deps): update dependency sass to ^1.80.5 (#1388)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-30 14:31:33 +00:00
homarr-renovate[bot] d3f9696127 fix(deps): update dependency drizzle-kit to ^0.27.0 (#1389)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-30 14:11:25 +00:00
homarr-renovate[bot] 3c3dbf51dc fix(deps): update dependency drizzle-orm to ^0.36.0 (#1390)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-30 13:48:37 +00:00
homarr-renovate[bot] f03688144f chore(deps): update dependency @types/node to ^20.17.3 (#1387)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-29 17:30:35 +00:00
homarr-renovate[bot] 8da1dd0d5f fix(deps): update dependency tldts to ^6.1.57 (#1386)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-29 06:53:02 +00:00
homarr-renovate[bot] 7a4c9a9b2e fix(deps): update dependency @jellyfin/sdk to ^0.11.0 (#1384)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-28 21:35:20 +00:00
homarr-renovate[bot] 958c62c926 fix(deps): update dependency @tabler/icons-react to ^3.21.0 (#1382)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-28 21:27:25 +00:00
homarr-renovate[bot] 8d2d151787 fix(deps): update dependency @drizzle-team/brocli to ^0.10.2 (#1380)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-28 21:11:03 +00:00
homarr-renovate[bot] 87f4298d47 chore(deps): update dependency @types/node to ^20.17.2 (#1383)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-28 20:48:30 +00:00
homarr-renovate[bot] 7ca67a13f1 chore(deps): update vitest monorepo to ^2.1.4 (#1379)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-28 13:26:16 +00:00
homarr-renovate[bot] 9df0f2b5b5 chore(deps): update pnpm to v9.12.3 (#1378)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-28 03:39:06 +00:00
homarr-renovate[bot] 5db973f935 chore(deps): update dependency tsx to v4.19.2 (#1377)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-10-26 23:34:21 +00:00
homarr-renovate[bot]andMeier Lukas a4ae38b5e4 fix(deps): update dependency next-intl to v3.23.5 (#1376)
* fix(deps): update dependency next-intl to v3.23.5

* fix: typecheck issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-10-26 23:15:52 +02:00
Meier Lukas 4502569223 refactor: move from next-international to next-intl (#1368)
* refactor: move from next-international to next-intl

* refactor: restructure translation package,

* chore: change i18n-allay framework to next-intl

* fix: add missing bold html tag to translation

* fix: format issue

* fix: address deepsource issues

* fix: remove international-types dependency

* fix: lint and typecheck issues

* fix: typecheck issue

* fix: typecheck issue

* fix: issue with translations
2024-10-26 22:46:14 +02:00
Meier Lukas db198c6dab feat: add media management (#1337)
* feat: add media management

* feat: add missing page search item

* fix: medias should be hidden for anonymous users

* chore: rename show-all to include-from-all-users

* fix: inconsistent table column for creator-id of media

* fix: schema check not working because of custom type for blob in mysql

* chore: temporarily remove migrations

* chore: readd removed migrations
2024-10-26 22:45:32 +02:00
399 changed files with 100682 additions and 9214 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+25 -3
View File
@@ -67,11 +67,33 @@ jobs:
- name: Test
run: pnpm test
- name: 'Report Coverage'
- name: "Report Coverage"
# Set if: always() to also generate the report if tests are failing
# Only works if you set `reportOnFailure: true` in your vite config as specified above
if: always()
uses: davelosert/vitest-coverage-report-action@v2
if: always()
uses: davelosert/vitest-coverage-report-action@v2
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup
uses: ./tooling/github/setup
- name: Build docker image
id: build-docker-image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64
context: .
push: false
tags: homarr-e2e
network: host
env:
SKIP_ENV_VALIDATION: true
- name: Run E2E Tests
shell: bash
run: pnpm test:e2e
build:
runs-on: ubuntu-latest
@@ -0,0 +1,40 @@
name: "[Crowdin] Download translations"
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *" # every day at midnight
jobs:
download-crowdin-translations:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Obtain token
id: obtainToken
uses: tibdex/github-app-token@v2
with:
private_key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
app_id: ${{ secrets.CROWDIN_APP_ID }}
- name: Download Crowdin translations
uses: crowdin/github-action@v2
with:
upload_sources: false
upload_translations: false
download_translations: true
localization_branch_name: crowdin
create_pull_request: true
pull_request_title: "chore(lang): updated translations from crowdin"
pull_request_body: "New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)"
pull_request_base_branch_name: "dev"
github_user_name: "Crowdin Homarr"
github_user_email: "190541745+homarr-crowdin[bot]@users.noreply.github.com"
skip_untranslated_strings: true
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
+28
View File
@@ -0,0 +1,28 @@
name: "[Crowdin] Upload translations"
on:
workflow_dispatch:
push:
paths:
- "packages/translation/src/lang/**"
branches: [dev]
jobs:
upload-crowdin-translations:
# Don't run this action if the downloaded translations are being pushed
if: "!contains(github.event.head_commit.message, 'chore(lang)')"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Upload Crowdin translations
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: true
download_translations: false
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
+4 -21
View File
@@ -33,9 +33,6 @@ jobs:
deploy:
name: Deploy docker image
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20]
steps:
- name: Discord notification
if: ${{ github.events.inputs.send-notifications != false }}
@@ -57,21 +54,7 @@ jobs:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "Semver computed next tag to be ${{ steps.semver.outputs.next }}. Current is ${{ steps.semver.outputs.current }}"
- uses: pnpm/action-setup@v2
with:
version: 8
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Discord notification
if: ${{ github.events.inputs.send-notifications != false }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "Built application artifacts. Building images..."
args: "Semver computed next tag to be ${{ steps.semver.outputs.next }}. Current is ${{ steps.semver.outputs.current }}. Building images..."
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
@@ -90,9 +73,9 @@ jobs:
tags: |
type=raw,value=alpha
type=raw,value=early-adopters
# tags: |
# type=raw,value=latest
# type=raw,value=${{ steps.semver.outputs.next }}
# tags: |
# type=raw,value=latest
# type=raw,value=${{ steps.semver.outputs.next }}
- name: Build and push
id: buildPushAction
uses: docker/build-push-action@v6
+1 -1
View File
@@ -1 +1 @@
20.18.0
22.12.0
+2 -1
View File
@@ -3,6 +3,7 @@
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"yoavbls.pretty-ts-errors",
"million.million-lint"
"million.million-lint",
"lokalise.i18n-ally"
]
}
+36
View File
@@ -0,0 +1,36 @@
# .vscode/i18n-ally-custom-framework.yml
# An array of strings which contain Language Ids defined by VS Code
# You can check available language ids here: https://code.visualstudio.com/docs/languages/identifiers
languageIds:
- javascript
- typescript
- javascriptreact
- typescriptreact
# An array of RegExes to find the key usage. **The key should be captured in the first match group**.
# You should unescape RegEx strings in order to fit in the YAML file
# To help with this, you can use https://www.freeformatter.com/json-escape.html
usageMatchRegex:
# The following example shows how to detect `t("your.i18n.keys")`
# the `{key}` will be placed by a proper keypath matching regex,
# you can ignore it and use your own matching rules as well
- "[^\\w\\d]t\\(['\"`]({key})['\"`]"
# A RegEx to set a custom scope range. This scope will be used as a prefix when detecting keys
# and works like how the i18next framework identifies the namespace scope from the
# useTranslation() hook.
# You should unescape RegEx strings in order to fit in the YAML file
# To help with this, you can use https://www.freeformatter.com/json-escape.html
scopeRangeRegex: "(getScopedI18n|useScopedI18n)\\(\\s*['\"](.*?)['\"]\\)"
# An array of strings containing refactor templates.
# The "$1" will be replaced by the keypath specified.
# Optional: uncomment the following two lines to use
# refactorTemplates:
# - i18n.get("$1")
# If set to true, only enables this custom framework (will disable all built-in frameworks)
monopoly: true
+8 -5
View File
@@ -27,9 +27,12 @@
"Umami"
],
"i18n-ally.dirStructure": "auto",
"i18n-ally.enabledFrameworks": ["next-international"],
"i18n-ally.localesPaths": ["./packages/translation/src/lang/"],
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.extract.keyMaxLength": 0,
"i18n-ally.keystyle": "flat"
"i18n-ally.displayLanguage": "en",
"i18n-ally.enabledFrameworks": [
"custom"
],
"i18n-ally.localesPaths": [
"packages/translation/src/lang",
],
"i18n-ally.keystyle": "auto",
}
+12 -45
View File
@@ -1,4 +1,4 @@
FROM node:20.18.0-alpine AS base
FROM node:22.12.0-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
@@ -6,46 +6,15 @@ RUN apk update
# Set working directory
WORKDIR /app
COPY . .
RUN npm i -g turbo
RUN turbo prune @homarr/nextjs --docker --out-dir ./next-out
RUN turbo prune @homarr/tasks --docker --out-dir ./tasks-out
RUN turbo prune @homarr/websocket --docker --out-dir ./websocket-out
RUN turbo prune @homarr/db --docker --out-dir ./migration-out
RUN turbo prune @homarr/cli --docker --out-dir ./cli-out
# Add lockfile and package.json's of isolated subworkspace
FROM base AS installer
RUN apk add --no-cache libc6-compat curl bash
RUN apk update
WORKDIR /app
COPY . .
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/tasks-out/json/ .
COPY --from=builder /app/websocket-out/json/ .
COPY --from=builder /app/migration-out/json/ .
COPY --from=builder /app/cli-out/json/ .
COPY --from=builder /app/next-out/json/ .
COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
# Is used for postinstall of docs definitions
COPY --from=builder /app/packages/definitions/src/docs ./packages/definitions/src/docs
# Uses the lockfile to install the dependencies
RUN corepack enable pnpm && pnpm install --recursive --frozen-lockfile
# Install sharp for image optimization
RUN corepack enable pnpm && pnpm install sharp -w
# Build the project
COPY --from=builder /app/tasks-out/full/ .
COPY --from=builder /app/websocket-out/full/ .
COPY --from=builder /app/next-out/full/ .
COPY --from=builder /app/migration-out/full/ .
COPY --from=builder /app/cli-out/full/ .
# Copy static data as it is not part of the build
COPY static-data ./static-data
ARG SKIP_ENV_VALIDATION='true'
@@ -59,8 +28,6 @@ WORKDIR /app
# gettext is required for envsubst
RUN apk add --no-cache redis nginx bash gettext
RUN mkdir /appdata
RUN mkdir /appdata/db
RUN mkdir /appdata/redis
VOLUME /appdata
RUN mkdir /secrets
VOLUME /secrets
@@ -71,7 +38,7 @@ RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Enable homarr cli
COPY --from=installer --chown=nextjs:nodejs /app/packages/cli/cli.cjs /app/apps/cli/cli.cjs
COPY --from=builder --chown=nextjs:nodejs /app/packages/cli/cli.cjs /app/apps/cli/cli.cjs
RUN echo $'#!/bin/bash\ncd /app/apps/cli && node ./cli.cjs "$@"' > /usr/bin/homarr
RUN chmod +x /usr/bin/homarr
@@ -85,20 +52,20 @@ RUN mkdir -p /var/cache/nginx && chown -R nextjs:nodejs /var/cache/nginx && \
mkdir -p /etc/nginx/templates /etc/nginx/ssl/certs && chown -R nextjs:nodejs /etc/nginx
USER nextjs
COPY --from=installer /app/apps/nextjs/next.config.mjs .
COPY --from=installer /app/apps/nextjs/package.json .
COPY --from=builder /app/apps/nextjs/next.config.mjs .
COPY --from=builder /app/apps/nextjs/package.json .
COPY --from=installer --chown=nextjs:nodejs /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
COPY --from=installer --chown=nextjs:nodejs /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
COPY --from=installer --chown=nextjs:nodejs /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
COPY --from=builder --chown=nextjs:nodejs /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
COPY --from=builder --chown=nextjs:nodejs /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
COPY --from=installer --chown=nextjs:nodejs /app/packages/db/migrations ./db/migrations
COPY --from=builder --chown=nextjs:nodejs /app/packages/db/migrations ./db/migrations
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=installer --chown=nextjs:nodejs /app/apps/nextjs/.next/standalone ./
COPY --from=installer --chown=nextjs:nodejs /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
COPY --from=installer --chown=nextjs:nodejs /app/apps/nextjs/public ./apps/nextjs/public
COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
COPY --from=builder --chown=nextjs:nodejs /app/apps/nextjs/public ./apps/nextjs/public
COPY --chown=nextjs:nodejs scripts/run.sh ./run.sh
COPY --chown=nextjs:nodejs scripts/generateEncryptionKey.js ./generateEncryptionKey.js
COPY --chown=nextjs:nodejs packages/redis/redis.conf /app/redis.conf
+6 -2
View File
@@ -2,11 +2,15 @@
import "@homarr/auth/env.mjs";
import MillionLint from "@million/lint";
import createNextIntlPlugin from "next-intl/plugin";
import "./src/env.mjs";
// Package path does not work... so we need to use relative path
const withNextIntl = createNextIntlPlugin("../../packages/translation/src/request.ts");
/** @type {import("next").NextConfig} */
const config = {
const nextConfig = {
output: "standalone",
reactStrictMode: true,
/** We already do linting and typechecking as separate tasks in CI */
@@ -34,4 +38,4 @@ const config = {
// Skip transform is used because of webpack loader, without it for example 'Tooltip.Floating' will not work and show an error
const withMillionLint = MillionLint.next({ rsc: true, skipTransform: true, telemetry: false });
export default config;
export default withNextIntl(nextConfig);
+23 -23
View File
@@ -23,7 +23,7 @@
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/form": "workspace:^0.1.0",
"@homarr/gridstack": "^1.0.3",
"@homarr/gridstack": "^1.11.2",
"@homarr/integrations": "workspace:^0.1.0",
"@homarr/log": "workspace:^",
"@homarr/modals": "workspace:^0.1.0",
@@ -37,17 +37,17 @@
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "^7.13.4",
"@mantine/core": "^7.13.4",
"@mantine/hooks": "^7.13.4",
"@mantine/modals": "^7.13.4",
"@mantine/tiptap": "^7.13.4",
"@million/lint": "1.0.11",
"@mantine/colors-generator": "^7.14.3",
"@mantine/core": "^7.14.3",
"@mantine/hooks": "^7.14.3",
"@mantine/modals": "^7.14.3",
"@mantine/tiptap": "^7.14.3",
"@million/lint": "1.0.13",
"@t3-oss/env-nextjs": "^0.11.1",
"@tabler/icons-react": "^3.20.0",
"@tanstack/react-query": "^5.59.16",
"@tanstack/react-query-devtools": "^5.59.16",
"@tanstack/react-query-next-experimental": "5.59.16",
"@tabler/icons-react": "^3.24.0",
"@tanstack/react-query": "^5.62.2",
"@tanstack/react-query-devtools": "^5.62.2",
"@tanstack/react-query-next-experimental": "5.62.2",
"@trpc/client": "next",
"@trpc/next": "next",
"@trpc/react-query": "next",
@@ -58,21 +58,21 @@
"chroma-js": "^3.1.2",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"dotenv": "^16.4.5",
"dotenv": "^16.4.7",
"flag-icons": "^7.2.3",
"glob": "^11.0.0",
"jotai": "^2.10.1",
"jotai": "^2.10.3",
"mantine-react-table": "2.0.0-beta.7",
"next": "^14.2.16",
"next": "^14.2.20",
"postcss-preset-mantine": "^1.17.0",
"prismjs": "^1.29.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.1.2",
"react-simple-code-editor": "^0.14.1",
"sass": "^1.80.4",
"sass": "^1.82.0",
"superjson": "2.2.1",
"swagger-ui-react": "^5.17.14",
"swagger-ui-react": "^5.18.2",
"use-deep-compare-effect": "^1.8.1"
},
"devDependencies": {
@@ -80,15 +80,15 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "2.4.4",
"@types/node": "^20.17.1",
"@types/node": "^22.10.1",
"@types/prismjs": "^1.26.5",
"@types/react": "^18.3.12",
"@types/react": "^18.3.13",
"@types/react-dom": "^18.3.1",
"@types/swagger-ui-react": "^4.18.3",
"concurrently": "^9.0.1",
"eslint": "^9.13.0",
"node-loader": "^2.0.0",
"prettier": "^3.3.3",
"typescript": "^5.6.3"
"concurrently": "^9.1.0",
"eslint": "^9.16.0",
"node-loader": "^2.1.0",
"prettier": "^3.4.2",
"typescript": "^5.7.2"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 497 B

+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 575 289.83" width="575" height="289.83"><defs><path d="M575 24.91C573.44 12.15 563.97 1.98 551.91 0C499.05 0 76.18 0 23.32 0C10.11 2.17 0 14.16 0 28.61C0 51.84 0 237.64 0 260.86C0 276.86 12.37 289.83 27.64 289.83C79.63 289.83 495.6 289.83 547.59 289.83C561.65 289.83 573.26 278.82 575 264.57C575 216.64 575 48.87 575 24.91Z" id="d1pwhf9wy2"></path><path d="M69.35 58.24L114.98 58.24L114.98 233.89L69.35 233.89L69.35 58.24Z" id="g5jjnq26yS"></path><path d="M201.2 139.15C197.28 112.38 195.1 97.5 194.67 94.53C192.76 80.2 190.94 67.73 189.2 57.09C185.25 57.09 165.54 57.09 130.04 57.09L130.04 232.74L170.01 232.74L170.15 116.76L186.97 232.74L215.44 232.74L231.39 114.18L231.54 232.74L271.38 232.74L271.38 57.09L211.77 57.09L201.2 139.15Z" id="i3Prh1JpXt"></path><path d="M346.71 93.63C347.21 95.87 347.47 100.95 347.47 108.89C347.47 115.7 347.47 170.18 347.47 176.99C347.47 188.68 346.71 195.84 345.2 198.48C343.68 201.12 339.64 202.43 333.09 202.43C333.09 190.9 333.09 98.66 333.09 87.13C338.06 87.13 341.45 87.66 343.25 88.7C345.05 89.75 346.21 91.39 346.71 93.63ZM367.32 230.95C372.75 229.76 377.31 227.66 381.01 224.67C384.7 221.67 387.29 217.52 388.77 212.21C390.26 206.91 391.14 196.38 391.14 180.63C391.14 174.47 391.14 125.12 391.14 118.95C391.14 102.33 390.49 91.19 389.48 85.53C388.46 79.86 385.93 74.71 381.88 70.09C377.82 65.47 371.9 62.15 364.12 60.13C356.33 58.11 343.63 57.09 321.54 57.09C319.27 57.09 307.93 57.09 287.5 57.09L287.5 232.74L342.78 232.74C355.52 232.34 363.7 231.75 367.32 230.95Z" id="a4ov9rRGQm"></path><path d="M464.76 204.7C463.92 206.93 460.24 208.06 457.46 208.06C454.74 208.06 452.93 206.98 452.01 204.81C451.09 202.65 450.64 197.72 450.64 190C450.64 185.36 450.64 148.22 450.64 143.58C450.64 135.58 451.04 130.59 451.85 128.6C452.65 126.63 454.41 125.63 457.13 125.63C459.91 125.63 463.64 126.76 464.6 129.03C465.55 131.3 466.03 136.15 466.03 143.58C466.03 146.58 466.03 161.58 466.03 188.59C465.74 197.84 465.32 203.21 464.76 204.7ZM406.68 231.21L447.76 231.21C449.47 224.5 450.41 220.77 450.6 220.02C454.32 224.52 458.41 227.9 462.9 230.14C467.37 232.39 474.06 233.51 479.24 233.51C486.45 233.51 492.67 231.62 497.92 227.83C503.16 224.05 506.5 219.57 507.92 214.42C509.34 209.26 510.05 201.42 510.05 190.88C510.05 185.95 510.05 146.53 510.05 141.6C510.05 131 509.81 124.08 509.34 120.83C508.87 117.58 507.47 114.27 505.14 110.88C502.81 107.49 499.42 104.86 494.98 102.98C490.54 101.1 485.3 100.16 479.26 100.16C474.01 100.16 467.29 101.21 462.81 103.28C458.34 105.35 454.28 108.49 450.64 112.7C450.64 108.89 450.64 89.85 450.64 55.56L406.68 55.56L406.68 231.21Z" id="fk968BpsX"></path></defs><g><g><g><use xlink:href="#d1pwhf9wy2" opacity="1" fill="#f6c700" fill-opacity="1"></use><g><use xlink:href="#d1pwhf9wy2" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#g5jjnq26yS" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#g5jjnq26yS" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#i3Prh1JpXt" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#i3Prh1JpXt" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#a4ov9rRGQm" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#a4ov9rRGQm" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g><g><use xlink:href="#fk968BpsX" opacity="1" fill="#000000" fill-opacity="1"></use><g><use xlink:href="#fk968BpsX" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="708.767" height="179.332">
<path fill="#d51007" d="m158.431 165.498-8.354-22.708s-13.575 15.14-33.932 15.14c-18.013 0-30.802-15.662-30.802-40.721 0-32.106 16.182-43.591 32.107-43.591 22.969 0 30.277 14.878 36.543 33.934l8.354 26.103c8.351 25.318 24.013 45.678 69.17 45.678 32.37 0 54.295-9.918 54.295-36.02 0-21.143-12.009-32.107-34.458-37.328l-16.705-3.654c-11.484-2.61-14.877-7.309-14.877-15.14 0-8.875 7.046-14.096 18.533-14.096 12.529 0 19.315 4.699 20.36 15.923l26.102-3.133c-2.088-23.492-18.271-33.15-44.896-33.15-23.491 0-46.462 8.875-46.462 37.327 0 17.75 8.614 28.975 30.277 34.195l17.752 4.175c13.312 3.133 17.748 8.614 17.748 16.185 0 9.656-9.396 13.572-27.146 13.572-26.364 0-37.325-13.834-43.591-32.89l-8.614-26.101c-10.961-33.934-28.452-46.463-63.169-46.463-38.37 0-58.731 24.275-58.731 65.517 0 39.677 20.361 61.08 56.906 61.08 29.492 0 43.59-13.834 43.59-13.834zM46.726 153.229c-2.61.784-5.221 1.306-8.614 1.306-6.265 0-10.703-2.87-10.703-10.442V1.827H0v148.792c0 19.577 13.575 27.672 29.497 27.672 5.221 0 10.181-.785 16.446-2.349l.783-22.713zm330.185-4.176c-6.787 4.701-12.529 7.051-20.36 7.051-9.92 0-15.401-5.221-15.401-18.012V77.006h36.023V55.603H341.41V26.625l-27.669 3.394v25.583h-17.49v21.403h17.49v66.826c0 24.02 13.834 35.5 36.284 35.5 12.269 0 23.232-2.346 31.847-7.305l-4.961-22.973zm23.807 9.396c0 10.705 8.354 19.318 19.056 19.318 11.226 0 19.578-8.613 19.578-19.318 0-10.963-8.353-19.313-19.578-19.313-10.702 0-19.056 8.35-19.056 19.313zm67.009-81.443v99.195h27.409V77.006h30.803V55.603h-30.803V44.638c0-16.444 7.049-21.665 18.534-21.665 8.092 0 13.574 1.825 19.839 5.221l4.437-22.974C530.638 1.827 522.023 0 511.582 0c-22.973 0-43.855 10.963-43.855 43.593v12.01h-17.489v21.403h17.489zm167.427 2.352c-3.133-19.578-15.923-26.629-32.63-26.629-16.706 0-31.062 7.571-37.329 26.104l-3.393-23.23h-22.188v120.598h27.409v-68.129c0-23.235 12.008-32.11 24.799-32.11 13.312 0 18.795 8.875 18.795 23.232V176.2h27.147v-68.39c0-22.974 12.269-31.849 25.061-31.849 13.052 0 18.532 8.875 18.532 23.232v77.006h27.409v-86.66c0-25.843-15.14-36.81-35.24-36.81-16.965 0-32.107 7.571-38.372 26.629z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 190.24 81.52"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" y1="40.76" x2="190.24" y2="40.76" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset="0.56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 2</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M105.67,36.06h66.9A17.67,17.67,0,0,0,190.24,18.4h0A17.67,17.67,0,0,0,172.57.73h-66.9A17.67,17.67,0,0,0,88,18.4h0A17.67,17.67,0,0,0,105.67,36.06Zm-88,45h76.9A17.67,17.67,0,0,0,112.24,63.4h0A17.67,17.67,0,0,0,94.57,45.73H17.67A17.67,17.67,0,0,0,0,63.4H0A17.67,17.67,0,0,0,17.67,81.06ZM10.41,35.42h7.8V6.92h10.1V0H.31v6.9h10.1Zm28.1,0h7.8V8.25h.1l9,27.15h6l9.3-27.15h.1V35.4h7.8V0H66.76l-8.2,23.1h-.1L50.31,0H38.51ZM152.43,55.67a15.07,15.07,0,0,0-4.52-5.52,18.57,18.57,0,0,0-6.68-3.08,33.54,33.54,0,0,0-8.07-1h-11.7v35.4h12.75a24.58,24.58,0,0,0,7.55-1.15A19.34,19.34,0,0,0,148.11,77a16.27,16.27,0,0,0,4.37-5.5,16.91,16.91,0,0,0,1.63-7.58A18.5,18.5,0,0,0,152.43,55.67ZM145,68.6A8.8,8.8,0,0,1,142.36,72a10.7,10.7,0,0,1-4,1.82,21.57,21.57,0,0,1-5,.55h-4.05v-21h4.6a17,17,0,0,1,4.67.63,11.66,11.66,0,0,1,3.88,1.87A9.14,9.14,0,0,1,145,59a9.87,9.87,0,0,1,1,4.52A11.89,11.89,0,0,1,145,68.6Zm44.63-.13a8,8,0,0,0-1.58-2.62A8.38,8.38,0,0,0,185.63,64a10.31,10.31,0,0,0-3.17-1v-.1a9.22,9.22,0,0,0,4.42-2.82,7.43,7.43,0,0,0,1.68-5,8.42,8.42,0,0,0-1.15-4.65,8.09,8.09,0,0,0-3-2.72,12.56,12.56,0,0,0-4.18-1.3,32.84,32.84,0,0,0-4.62-.33h-13.2v35.4h14.5a22.41,22.41,0,0,0,4.72-.5,13.53,13.53,0,0,0,4.28-1.65,9.42,9.42,0,0,0,3.1-3,8.52,8.52,0,0,0,1.2-4.68A9.39,9.39,0,0,0,189.66,68.47ZM170.21,52.72h5.3a10,10,0,0,1,1.85.18,6.18,6.18,0,0,1,1.7.57,3.39,3.39,0,0,1,1.22,1.13,3.22,3.22,0,0,1,.48,1.82,3.63,3.63,0,0,1-.43,1.8,3.4,3.4,0,0,1-1.12,1.2,4.92,4.92,0,0,1-1.58.65,7.51,7.51,0,0,1-1.77.2h-5.65Zm11.72,20a3.9,3.9,0,0,1-1.22,1.3,4.64,4.64,0,0,1-1.68.7,8.18,8.18,0,0,1-1.82.2h-7v-8h5.9a15.35,15.35,0,0,1,2,.15,8.47,8.47,0,0,1,2.05.55,4,4,0,0,1,1.57,1.18,3.11,3.11,0,0,1,.63,2A3.71,3.71,0,0,1,181.93,72.72Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+47
View File
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="64mm"
height="64mm"
viewBox="0 0 64 64"
version="1.1"
id="svg5"
inkscape:version="1.2-alpha1 (b6a15bbbed, 2022-02-23)"
sodipodi:docname="vgmdb.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="2.1089995"
inkscape:cx="96.017091"
inkscape:cy="132.29021"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="456"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#000000;stroke-width:0.21854"
d="m 24.915135,62.650271 c 0.887879,-1.778597 2.041345,-3.66055 2.243584,-3.66055 0.373389,0 3.112623,2.34553 4.058216,3.474939 l 0.933036,1.114407 h -3.849247 -3.849246 z m 12.769897,0.133716 c 0.188983,-1.699691 0.540579,-9.438559 0.541952,-11.928786 l 0.0013,-2.561742 1.233546,-0.358603 c 1.858993,-0.540424 4.864719,-0.351614 6.306093,0.396126 2.102595,1.090761 3.373658,3.018391 3.382575,5.129841 0.0167,3.956206 -2.25508,7.722455 -5.608397,9.297806 -1.304436,0.612809 -1.766356,0.710219 -3.720191,0.784508 l -2.230831,0.08482 0.09384,-0.843972 z M 15.28167,58.692354 C 13.855849,58.372721 13.307133,58.098082 12.027965,57.063833 10.767623,56.044804 9.571224,54.160633 8.9251418,52.177293 8.4791686,50.808245 8.4074221,50.178438 8.4306037,47.836119 8.5286648,37.927793 11.975024,28.061437 18.601259,18.719217 c 1.935825,-2.729288 2.971061,-3.715223 4.909347,-4.675547 2.754154,-1.364545 6.425257,-1.245307 7.097695,0.230534 0.528157,1.159175 -0.351225,2.928281 -5.658254,11.38306 -3.70676,5.905347 -6.114927,9.18009 -8.52131,11.587702 l -1.747431,1.748322 0.748093,2.185403 c 1.052426,3.074452 2.737855,6.406887 3.730724,7.376398 l 0.831803,0.812233 1.792285,-1.012255 c 0.985756,-0.556742 3.87521,-2.58299 6.421008,-4.502779 l 4.628722,-3.490523 -0.152738,4.29269 c -0.08401,2.36098 -0.222472,4.480622 -0.3077,4.710314 -0.332428,0.895893 -8.050465,7.1666 -10.09734,8.203831 -2.185833,1.107645 -5.028822,1.564407 -6.994493,1.123754 z m 12.832756,-0.03877 c -0.369353,-0.206871 -0.546974,-0.44325 -0.453135,-0.603036 0.708434,-1.206309 4.008342,-5.172791 4.307161,-5.177197 0.225634,-0.0033 -0.01427,3.110825 -0.312679,4.058824 -0.487124,1.547514 -2.288476,2.42313 -3.541347,1.721409 z M 54.400377,47.270961 c -2.064993,-1.073365 -4.152108,-1.442529 -8.74161,-1.546198 -3.114822,-0.07036 -4.685696,-0.01346 -5.837847,0.211437 l -1.576311,0.307699 -0.101497,-0.893553 C 38.087288,44.858893 37.933571,42.73579 37.801516,40.63234 l -0.240099,-3.824455 0.497396,-0.409308 c 3.74237,-3.079615 11.651909,-7.818064 15.417757,-9.236475 2.486857,-0.936679 2.235049,-2.072647 2.235049,10.082864 0,5.829561 -0.07376,10.585246 -0.163906,10.568187 -0.09015,-0.01705 -0.606449,-0.261044 -1.147336,-0.542192 z M 23.409747,36.75325 c 0.981959,-3.570355 4.756318,-11.031164 6.826933,-13.49486 0.464503,-0.552684 0.586271,-0.604153 0.775553,-0.327811 0.553602,0.808232 1.251919,4.110924 1.621318,7.668016 l 0.126807,1.221076 -3.079512,1.795215 c -1.693731,0.987367 -3.814306,2.281815 -4.71239,2.876551 -0.898084,0.594738 -1.666906,1.081339 -1.708492,1.081339 -0.04159,0 0.02582,-0.368786 0.149783,-0.819526 z m 13.184647,-7.110817 c -0.04629,-0.145705 -0.341909,-1.543377 -0.656926,-3.10594 -0.315019,-1.562562 -0.864147,-3.967401 -1.220285,-5.344086 l -0.647526,-2.503063 1.150575,-1.176622 c 0.632814,-0.647143 2.643448,-2.301196 4.468071,-3.675673 3.438183,-2.589963 4.301443,-3.419286 4.80161,-4.6128516 0.714642,-1.7053778 -0.03157,-3.7643498 -1.67217,-4.613896 -0.898369,-0.4651992 -1.181912,-0.4997701 -3.278104,-0.3996832 -2.190425,0.1045861 -4.050576,0.523224 -8.85088,1.9919437 l -1.092702,0.334327 0.546351,-0.4412242 c 6.292255,-5.0815261 14.525967,-6.98424479 19.646997,-4.540202 2.438424,1.163752 4.050578,3.1575295 5.198539,6.4291158 l 0.605044,1.7243152 0.0047,7.3023343 0.0047,7.302333 -2.622483,0.481857 c -4.702423,0.864027 -9.663403,2.365904 -14.706862,4.452327 -1.118146,0.462565 -1.619586,0.580467 -1.67861,0.394688 z"
id="path6625" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -0,0 +1,5 @@
import definition from "../boards/(content)/(home)/_definition";
const { layout } = definition;
export default layout;
@@ -0,0 +1,7 @@
import definition from "../boards/(content)/(home)/_definition";
const { generateMetadataAsync: generateMetadata, page } = definition;
export default page;
export { generateMetadata };
@@ -1,14 +0,0 @@
import type { PropsWithChildren } from "react";
import { AppShellMain } from "@mantine/core";
import { MainHeader } from "~/components/layout/header";
import { ClientShell } from "~/components/layout/shell";
export default function MainLayout({ children }: PropsWithChildren) {
return (
<ClientShell hasNavigation={false}>
<MainHeader />
<AppShellMain>{children}</AppShellMain>
</ClientShell>
);
}
@@ -1,9 +0,0 @@
import { Stack, Title } from "@mantine/core";
export default function HomePage() {
return (
<Stack>
<Title>Home</Title>
</Stack>
);
}
@@ -0,0 +1,12 @@
"use client";
import type { PropsWithChildren } from "react";
import { useSuspenseDayJsLocalization } from "@homarr/translation/dayjs";
export const DayJsLoader = ({ children }: PropsWithChildren) => {
// Load the dayjs localization for the current locale with suspense
useSuspenseDayJsLocalization();
return children;
};
@@ -8,14 +8,18 @@ import dayjs from "dayjs";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { parseCookies, setClientCookie } from "@homarr/common";
import type { ColorScheme } from "@homarr/definitions";
import { colorSchemeCookieKey } from "@homarr/definitions";
export const CustomMantineProvider = ({ children }: PropsWithChildren) => {
export const CustomMantineProvider = ({
children,
defaultColorScheme,
}: PropsWithChildren<{ defaultColorScheme: ColorScheme }>) => {
const manager = useColorSchemeManager();
return (
<DirectionProvider>
<MantineProvider
defaultColorScheme="dark"
defaultColorScheme={defaultColorScheme}
colorSchemeManager={manager}
theme={createTheme({
primaryColor: "red",
@@ -28,12 +32,11 @@ export const CustomMantineProvider = ({ children }: PropsWithChildren) => {
);
};
function useColorSchemeManager(): MantineColorSchemeManager {
const key = "homarr-color-scheme";
export function useColorSchemeManager(): MantineColorSchemeManager {
const { data: session } = useSession();
const updateCookieValue = (value: Exclude<MantineColorScheme, "auto">) => {
setClientCookie(key, value, { expires: dayjs().add(1, "year").toDate(), path: "/" });
setClientCookie(colorSchemeCookieKey, value, { expires: dayjs().add(1, "year").toDate(), path: "/" });
};
const { mutate: mutateColorScheme } = clientApi.user.changeColorScheme.useMutation({
@@ -50,7 +53,7 @@ function useColorSchemeManager(): MantineColorSchemeManager {
try {
const cookies = parseCookies(document.cookie);
return (cookies[key] as MantineColorScheme | undefined) ?? defaultValue;
return (cookies[colorSchemeCookieKey] as MantineColorScheme | undefined) ?? defaultValue;
} catch {
return defaultValue;
}
@@ -1,12 +0,0 @@
import type { PropsWithChildren } from "react";
import { defaultLocale } from "@homarr/translation";
import { I18nProviderClient } from "@homarr/translation/client";
export const NextInternationalProvider = ({ children, locale }: PropsWithChildren<{ locale: string }>) => {
return (
<I18nProviderClient locale={locale} fallback={defaultLocale}>
{children}
</I18nProviderClient>
);
};
@@ -17,12 +17,21 @@ import {
import superjson from "superjson";
import type { AppRouter } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { clientApi, createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/client";
import { env } from "~/env.mjs";
const getWebSocketProtocol = () => {
// window is not defined on server side
if (typeof window === "undefined") {
return "ws";
}
return window.location.protocol === "https:" ? "wss" : "ws";
};
const constructWebsocketUrl = () => {
const fallback = "ws://localhost:3001/websockets";
const fallback = `${getWebSocketProtocol()}://localhost:3001/websockets`;
if (typeof window === "undefined") {
return fallback;
}
@@ -31,7 +40,7 @@ const constructWebsocketUrl = () => {
return fallback;
}
return `ws://${window.location.hostname}:${window.location.port}/websockets`;
return `${getWebSocketProtocol()}://${window.location.hostname}:${window.location.port}/websockets`;
};
const wsClient = createWSClient({
@@ -77,16 +86,13 @@ export function TRPCReactProvider(props: PropsWithChildren) {
return data;
},
},
url: `${getBaseUrl()}/api/trpc`,
url: getTrpcUrl(),
headers: createHeadersCallbackForSource("nextjs-react (form-data)"),
}),
false: unstable_httpBatchStreamLink({
transformer: superjson,
url: `${getBaseUrl()}/api/trpc`,
headers() {
const headers = new Headers();
headers.set("x-trpc-source", "nextjs-react");
return headers;
},
url: getTrpcUrl(),
headers: createHeadersCallbackForSource("nextjs-react (json)"),
}),
}),
}),
@@ -103,9 +109,3 @@ export function TRPCReactProvider(props: PropsWithChildren) {
</clientApi.Provider>
);
}
function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin;
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`;
}
@@ -1,6 +1,8 @@
"use client";
import { useCallback } from "react";
import type { MouseEvent } from "react";
import { useCallback, useEffect } from "react";
import { useRouter } from "next/navigation";
import { Group, Menu } from "@mantine/core";
import { useHotkeys } from "@mantine/hooks";
import {
@@ -16,7 +18,7 @@ import {
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useModalAction } from "@homarr/modals";
import { useConfirmModal, useModalAction } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
@@ -26,6 +28,7 @@ import { useCategoryActions } from "~/components/board/sections/category/categor
import { CategoryEditModal } from "~/components/board/sections/category/category-edit-modal";
import { useDynamicSectionActions } from "~/components/board/sections/dynamic/dynamic-actions";
import { HeaderButton } from "~/components/layout/header/button";
import { env } from "~/env.mjs";
import { useEditMode, useRequiredBoard } from "./_context";
export const BoardContentHeaderActions = () => {
@@ -139,6 +142,7 @@ const EditModeMenu = () => {
}, [board, isEditMode, saveBoard, setEditMode]);
useHotkeys([["mod+e", toggle]]);
usePreventLeaveWithDirty(isEditMode);
return (
<HeaderButton onClick={toggle} loading={isPending}>
@@ -146,3 +150,63 @@ const EditModeMenu = () => {
</HeaderButton>
);
};
const usePreventLeaveWithDirty = (isDirty: boolean) => {
const t = useI18n();
const { openConfirmModal } = useConfirmModal();
const router = useRouter();
useEffect(() => {
const handleClick = (event: MouseEvent<HTMLElement>) => {
const target = (event.target as HTMLElement).closest("a");
if (!target) return;
if (!isDirty) return;
event.preventDefault();
openConfirmModal({
title: t("board.action.edit.confirmLeave.title"),
children: t("board.action.edit.confirmLeave.message"),
onConfirm() {
router.push(target.href);
},
confirmProps: {
children: t("common.action.discard"),
},
});
};
const handlePopState = (event: Event) => {
if (isDirty) {
window.history.pushState(null, document.title, window.location.href);
event.preventDefault();
} else {
window.history.back();
}
};
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
if (!isDirty) return;
if (env.NODE_ENV === "development") return; // Allow to reload in development
event.preventDefault();
event.returnValue = true;
};
document.querySelectorAll("a").forEach((link) => {
link.addEventListener("click", handleClick as never);
});
window.addEventListener("popstate", handlePopState);
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
document.querySelectorAll("a").forEach((link) => {
link.removeEventListener("click", handleClick as never);
window.removeEventListener("popstate", handlePopState);
});
window.removeEventListener("beforeunload", handleBeforeUnload);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDirty]);
};
@@ -4,10 +4,17 @@ import type { PropsWithChildren } from "react";
import type { MantineColorsTuple } from "@mantine/core";
import { createTheme, darken, lighten, MantineProvider } from "@mantine/core";
import type { ColorScheme } from "@homarr/definitions";
import { useColorSchemeManager } from "../../_client-providers/mantine";
import { useRequiredBoard } from "./_context";
export const BoardMantineProvider = ({ children }: PropsWithChildren) => {
export const BoardMantineProvider = ({
children,
defaultColorScheme,
}: PropsWithChildren<{ defaultColorScheme: ColorScheme }>) => {
const board = useRequiredBoard();
const colorSchemeManager = useColorSchemeManager();
const theme = createTheme({
colors: {
@@ -18,7 +25,11 @@ export const BoardMantineProvider = ({ children }: PropsWithChildren) => {
autoContrast: true,
});
return <MantineProvider theme={theme}>{children}</MantineProvider>;
return (
<MantineProvider defaultColorScheme={defaultColorScheme} theme={theme} colorSchemeManager={colorSchemeManager}>
{children}
</MantineProvider>
);
};
export const generateColors = (hex: string) => {
@@ -12,7 +12,7 @@ import { BoardRenameModal } from "~/components/board/modals/board-rename-modal";
import { useRequiredBoard } from "../../(content)/_context";
import classes from "./danger.module.css";
export const DangerZoneSettingsContent = () => {
export const DangerZoneSettingsContent = ({ hideVisibility }: { hideVisibility: boolean }) => {
const board = useRequiredBoard();
const t = useScopedI18n("board.setting");
const router = useRouter();
@@ -90,14 +90,18 @@ export const DangerZoneSettingsContent = () => {
buttonText={t("section.dangerZone.action.rename.button")}
onClick={onRenameClick}
/>
<Divider />
<DangerZoneRow
label={t("section.dangerZone.action.visibility.label")}
description={t(`section.dangerZone.action.visibility.description.${visibility}`)}
buttonText={t(`section.dangerZone.action.visibility.button.${visibility}`)}
onClick={onVisibilityClick}
isPending={isChangeVisibilityPending}
/>
{hideVisibility ? null : (
<>
<Divider />
<DangerZoneRow
label={t("section.dangerZone.action.visibility.label")}
description={t(`section.dangerZone.action.visibility.description.${visibility}`)}
buttonText={t(`section.dangerZone.action.visibility.button.${visibility}`)}
onClick={onVisibilityClick}
isPending={isChangeVisibilityPending}
/>
</>
)}
<Divider />
<DangerZoneRow
label={t("section.dangerZone.action.delete.label")}
@@ -14,6 +14,8 @@ import { TRPCError } from "@trpc/server";
import { api } from "@homarr/api/server";
import { capitalize } from "@homarr/common";
import { db } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import type { TranslationObject } from "@homarr/translation";
import { getScopedI18n } from "@homarr/translation/server";
import type { TablerIcon } from "@homarr/ui";
@@ -63,6 +65,7 @@ const getBoardAndPermissionsAsync = async (params: Props["params"]) => {
export default async function BoardSettingsPage({ params, searchParams }: Props) {
const { board, permissions } = await getBoardAndPermissionsAsync(params);
const boardSettings = await getServerSettingByKeyAsync(db, "board");
const { hasFullAccess } = await getBoardPermissionsAsync(board);
const t = await getScopedI18n("board.setting");
@@ -92,7 +95,7 @@ export default async function BoardSettingsPage({ params, searchParams }: Props)
<BoardAccessSettings board={board} initialPermissions={permissions} />
</AccordionItemFor>
<AccordionItemFor value="dangerZone" icon={IconAlertTriangle} danger noPadding>
<DangerZoneSettingsContent />
<DangerZoneSettingsContent hideVisibility={boardSettings.homeBoardId === board.id} />
</AccordionItemFor>
</>
)}
@@ -8,6 +8,7 @@ import { logger } from "@homarr/log";
import { MainHeader } from "~/components/layout/header";
import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo";
import { ClientShell } from "~/components/layout/shell";
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
import type { Board } from "./_types";
import { BoardProvider } from "./(content)/_context";
import type { Params } from "./(content)/_creator";
@@ -37,10 +38,11 @@ export const createBoardLayout = <TParams extends Params>({
throw error;
});
const colorScheme = await getCurrentColorSchemeAsync();
return (
<BoardProvider initialBoard={initialBoard}>
<BoardMantineProvider>
<BoardMantineProvider defaultColorScheme={colorScheme}>
<CustomCss />
<ClientShell hasNavigation={false}>
<MainHeader
+20 -15
View File
@@ -6,19 +6,22 @@ import "@homarr/spotlight/styles.css";
import "@homarr/ui/styles.css";
import "~/styles/scroll-area.scss";
import { cookies } from "next/headers";
import { notFound } from "next/navigation";
import { NextIntlClientProvider } from "next-intl";
import { env } from "@homarr/auth/env.mjs";
import { auth } from "@homarr/auth/next";
import { ModalProvider } from "@homarr/modals";
import { Notifications } from "@homarr/notifications";
import { getScopedI18n } from "@homarr/translation/server";
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
import { getI18nMessages } from "@homarr/translation/server";
import { Analytics } from "~/components/layout/analytics";
import { SearchEngineOptimization } from "~/components/layout/search-engine-optimization";
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
import { DayJsLoader } from "./_client-providers/dayjs-loader";
import { JotaiProvider } from "./_client-providers/jotai";
import { CustomMantineProvider } from "./_client-providers/mantine";
import { NextInternationalProvider } from "./_client-providers/next-international";
import { AuthProvider } from "./_client-providers/session";
import { TRPCReactProvider } from "./_client-providers/trpc";
import { composeWrappers } from "./compose";
@@ -28,7 +31,8 @@ const fontSans = Inter({
variable: "--font-sans",
});
export const generateMetadata = (): Metadata => ({
// eslint-disable-next-line no-restricted-syntax
export const generateMetadata = async (): Promise<Metadata> => ({
title: "Homarr",
description:
"Simplify the management of your server with Homarr - a sleek, modern dashboard that puts all of your apps and services at your fingertips.",
@@ -47,7 +51,7 @@ export const generateMetadata = (): Metadata => ({
title: "Homarr",
capable: true,
startupImage: { url: "/logo/logo.png" },
statusBarStyle: getColorScheme() === "dark" ? "black-translucent" : "default",
statusBarStyle: (await getCurrentColorSchemeAsync()) === "dark" ? "black-translucent" : "default",
},
});
@@ -59,10 +63,14 @@ export const viewport: Viewport = {
};
export default async function Layout(props: { children: React.ReactNode; params: { locale: string } }) {
if (!isLocaleSupported(props.params.locale)) {
notFound();
}
const session = await auth();
const colorScheme = getColorScheme();
const tCommon = await getScopedI18n("common");
const direction = tCommon("direction");
const colorScheme = await getCurrentColorSchemeAsync();
const direction = isLocaleRTL(props.params.locale) ? "rtl" : "ltr";
const i18nMessages = await getI18nMessages();
const StackedProvider = composeWrappers([
(innerProps) => {
@@ -70,15 +78,16 @@ export default async function Layout(props: { children: React.ReactNode; params:
},
(innerProps) => <JotaiProvider {...innerProps} />,
(innerProps) => <TRPCReactProvider {...innerProps} />,
(innerProps) => <NextInternationalProvider {...innerProps} locale={props.params.locale} />,
(innerProps) => <CustomMantineProvider {...innerProps} />,
(innerProps) => <DayJsLoader {...innerProps} />,
(innerProps) => <NextIntlClientProvider {...innerProps} messages={i18nMessages} />,
(innerProps) => <CustomMantineProvider {...innerProps} defaultColorScheme={colorScheme} />,
(innerProps) => <ModalProvider {...innerProps} />,
]);
return (
// Instead of ColorSchemScript we use data-mantine-color-scheme to prevent flickering
<html
lang="en"
lang={props.params.locale}
dir={direction}
data-mantine-color-scheme={colorScheme}
style={{
@@ -99,7 +108,3 @@ export default async function Layout(props: { children: React.ReactNode; params:
</html>
);
}
const getColorScheme = () => {
return cookies().get("homarr-color-scheme")?.value ?? "dark";
};
@@ -1,6 +1,8 @@
import { notFound } from "next/navigation";
import { Container, Stack, Title } from "@mantine/core";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { getI18n } from "@homarr/translation/server";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
@@ -11,6 +13,11 @@ interface AppEditPageProps {
}
export default async function AppEditPage({ params }: AppEditPageProps) {
const session = await auth();
if (!session?.user.permissions.includes("app-modify-all")) {
notFound();
}
const app = await api.app.byId({ id: params.id });
const t = await getI18n();
@@ -1,11 +1,19 @@
import { notFound } from "next/navigation";
import { Container, Stack, Title } from "@mantine/core";
import { auth } from "@homarr/auth/next";
import { getI18n } from "@homarr/translation/server";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { AppNewForm } from "./_app-new-form";
export default async function AppNewPage() {
const session = await auth();
if (!session?.user.permissions.includes("app-create")) {
notFound();
}
const t = await getI18n();
return (
@@ -1,18 +1,27 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
import { IconApps, IconPencil } from "@tabler/icons-react";
import { IconBox, IconPencil } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { parseAppHrefWithVariablesServer } from "@homarr/common/server";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { ManageContainer } from "~/components/manage/manage-container";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NoResults } from "~/components/no-results";
import { AppDeleteButton } from "./_app-delete-button";
export default async function AppsPage() {
const session = await auth();
if (!session) {
redirect("/auth/login");
}
const apps = await api.app.all();
const t = await getScopedI18n("app");
@@ -22,9 +31,11 @@ export default async function AppsPage() {
<Stack>
<Group justify="space-between" align="center">
<Title>{t("page.list.title")}</Title>
<MobileAffixButton component={Link} href="/manage/apps/new">
{t("page.create.title")}
</MobileAffixButton>
{session.user.permissions.includes("app-create") && (
<MobileAffixButton component={Link} href="/manage/apps/new">
{t("page.create.title")}
</MobileAffixButton>
)}
</Group>
{apps.length === 0 && <AppNoResults />}
{apps.length > 0 && (
@@ -45,6 +56,7 @@ interface AppCardProps {
const AppCard = async ({ app }: AppCardProps) => {
const t = await getScopedI18n("app");
const session = await auth();
return (
<Card>
@@ -78,16 +90,18 @@ const AppCard = async ({ app }: AppCardProps) => {
</Group>
<Group>
<ActionIconGroup>
<ActionIcon
component={Link}
href={`/manage/apps/edit/${app.id}`}
variant="subtle"
color="gray"
aria-label={t("page.edit.title")}
>
<IconPencil size={16} stroke={1.5} />
</ActionIcon>
<AppDeleteButton app={app} />
{session?.user.permissions.includes("app-modify-all") && (
<ActionIcon
component={Link}
href={`/manage/apps/edit/${app.id}`}
variant="subtle"
color="gray"
aria-label={t("page.edit.title")}
>
<IconPencil size={16} stroke={1.5} />
</ActionIcon>
)}
{session?.user.permissions.includes("app-full-all") && <AppDeleteButton app={app} />}
</ActionIconGroup>
</Group>
</Group>
@@ -97,16 +111,17 @@ const AppCard = async ({ app }: AppCardProps) => {
const AppNoResults = async () => {
const t = await getI18n();
const session = await auth();
return (
<Card withBorder bg="transparent">
<Stack align="center" gap="sm">
<IconApps size="2rem" />
<Text fw={500} size="lg">
{t("app.page.list.noResults.title")}
</Text>
<Anchor href="/manage/apps/new">{t("app.page.list.noResults.action")}</Anchor>
</Stack>
</Card>
<NoResults
icon={IconBox}
title={t("app.page.list.noResults.title")}
action={{
label: t("app.page.list.noResults.action"),
href: "/manage/apps/new",
hidden: !session?.user.permissions.includes("app-create"),
}}
/>
);
};
@@ -1,7 +1,6 @@
"use client";
import { useState } from "react";
import { useParams } from "next/navigation";
import { ActionIcon, Avatar, Button, Card, Collapse, Group, Kbd, Stack, Text } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconEye, IconEyeOff } from "@tabler/icons-react";
@@ -23,7 +22,6 @@ interface SecretCardProps {
}
export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
const params = useParams<{ locale: string }>();
const t = useI18n();
const { isPublic } = integrationSecretKindObject[secret.kind];
const [publicSecretDisplayOpened, { toggle: togglePublicSecretDisplay }] = useDisclosure(false);
@@ -45,7 +43,7 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
<Group>
<Text c="gray.6" size="sm">
{t("integration.secrets.lastUpdated", {
date: dayjs().locale(params.locale).to(dayjs(secret.updatedAt)),
date: dayjs().to(dayjs(secret.updatedAt)),
})}
</Text>
{isPublic ? (
@@ -6,7 +6,7 @@ import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { IntegrationAvatar } from "@homarr/ui";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { catchTrpcNotFound } from "~/errors/trpc-not-found";
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
import { IntegrationAccessSettings } from "../../_components/integration-access-settings";
import { EditIntegrationForm } from "./_integration-edit-form";
@@ -1,6 +1,7 @@
import { Fragment } from "react";
import type { PropsWithChildren } from "react";
import Link from "next/link";
import { redirect } from "next/navigation";
import {
AccordionControl,
AccordionItem,
@@ -26,7 +27,7 @@ import {
Text,
Title,
} from "@mantine/core";
import { IconChevronDown, IconChevronUp, IconPencil } from "@tabler/icons-react";
import { IconChevronDown, IconChevronUp, IconPencil, IconPlugX } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
@@ -39,6 +40,7 @@ import { CountBadge, IntegrationAvatar } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NoResults } from "~/components/no-results";
import { ActiveTabAccordion } from "../../../../components/active-tab-accordion";
import { DeleteIntegrationActionButton } from "./_integration-buttons";
import { IntegrationCreateDropdownContent } from "./new/_integration-new-dropdown";
@@ -50,11 +52,16 @@ interface IntegrationsPageProps {
}
export default async function IntegrationsPage({ searchParams }: IntegrationsPageProps) {
const integrations = await api.integration.all();
const session = await auth();
if (!session) {
redirect("/auth/login");
}
const integrations = await api.integration.all();
const t = await getScopedI18n("integration");
const canCreateIntegrations = session?.user.permissions.includes("integration-create") ?? false;
const canCreateIntegrations = session.user.permissions.includes("integration-create");
return (
<ManageContainer>
@@ -114,7 +121,7 @@ const IntegrationList = async ({ integrations, activeTab }: IntegrationListProps
const hasFullAccess = session?.user.permissions.includes("integration-full-all") ?? false;
if (integrations.length === 0) {
return <div>{t("page.list.empty")}</div>;
return <NoResults icon={IconPlugX} title={t("page.list.noResults.title")} />;
}
const grouppedIntegrations = integrations.reduce(
+16 -1
View File
@@ -12,6 +12,7 @@ import {
IconLayoutDashboard,
IconLogs,
IconMailForward,
IconPhoto,
IconPlug,
IconQuestionMark,
IconReport,
@@ -51,16 +52,25 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
icon: IconBox,
href: "/manage/apps",
label: t("items.apps"),
hidden: !session,
},
{
icon: IconPlug,
href: "/manage/integrations",
label: t("items.integrations"),
hidden: !session,
},
{
icon: IconSearch,
href: "/manage/search-engines",
label: t("items.searchEngies"),
hidden: !session,
},
{
icon: IconPhoto,
href: "/manage/medias",
label: t("items.medias"),
hidden: !session,
},
{
icon: IconUser,
@@ -88,27 +98,32 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
{
label: t("items.tools.label"),
icon: IconTool,
hidden: !session?.user.permissions.includes("admin"),
// As permissions always include there children permissions, we can check other-view-logs as admin includes it
hidden: !session?.user.permissions.includes("other-view-logs"),
items: [
{
label: t("items.tools.items.docker"),
icon: IconBrandDocker,
href: "/manage/tools/docker",
hidden: !session?.user.permissions.includes("admin"),
},
{
label: t("items.tools.items.api"),
icon: IconPlug,
href: "/manage/tools/api",
hidden: !session?.user.permissions.includes("admin"),
},
{
label: t("items.tools.items.logs"),
icon: IconLogs,
href: "/manage/tools/logs",
hidden: !session?.user.permissions.includes("other-view-logs"),
},
{
label: t("items.tools.items.tasks"),
icon: IconReport,
href: "/manage/tools/tasks",
hidden: !session?.user.permissions.includes("admin"),
},
],
},
@@ -0,0 +1,32 @@
"use client";
import { ActionIcon, CopyButton, Tooltip } from "@mantine/core";
import { IconCheck, IconCopy } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { useI18n } from "@homarr/translation/client";
interface CopyMediaProps {
media: RouterOutputs["media"]["getPaginated"]["items"][number];
}
export const CopyMedia = ({ media }: CopyMediaProps) => {
const t = useI18n();
const url =
typeof window !== "undefined"
? `${window.location.protocol}://${window.location.hostname}:${window.location.port}/api/user-medias/${media.id}`
: "";
return (
<CopyButton value={url}>
{({ copy, copied }) => (
<Tooltip label={t("media.action.copy.label")} openDelay={500}>
<ActionIcon onClick={copy} color={copied ? "teal" : "gray"} variant="subtle">
{copied ? <IconCheck size={16} stroke={1.5} /> : <IconCopy size={16} stroke={1.5} />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
);
};
@@ -0,0 +1,40 @@
"use client";
import { ActionIcon, Tooltip } from "@mantine/core";
import { IconTrash } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useConfirmModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
interface DeleteMediaProps {
media: RouterOutputs["media"]["getPaginated"]["items"][number];
}
export const DeleteMedia = ({ media }: DeleteMediaProps) => {
const { openConfirmModal } = useConfirmModal();
const t = useI18n();
const { mutateAsync, isPending } = clientApi.media.deleteMedia.useMutation();
const onClick = () => {
openConfirmModal({
title: t("media.action.delete.label"),
children: t.rich("media.action.delete.description", { bName: () => <b>{media.name}</b> }),
// eslint-disable-next-line no-restricted-syntax
onConfirm: async () => {
await mutateAsync({ id: media.id });
await revalidatePathActionAsync("/manage/medias");
},
});
};
return (
<Tooltip label={t("media.action.delete.label")} openDelay={500}>
<ActionIcon color="red" variant="subtle" onClick={onClick} loading={isPending}>
<IconTrash color="red" size={16} stroke={1.5} />
</ActionIcon>
</Tooltip>
);
};
@@ -0,0 +1,36 @@
"use client";
import { useState } from "react";
import type { ChangeEvent } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Switch } from "@mantine/core";
import type { SwitchProps } from "@mantine/core";
import { useI18n } from "@homarr/translation/client";
type ShowAllSwitchProps = Pick<SwitchProps, "defaultChecked">;
export const IncludeFromAllUsersSwitch = ({ defaultChecked }: ShowAllSwitchProps) => {
const router = useRouter();
const pathName = usePathname();
const searchParams = useSearchParams();
const [checked, setChecked] = useState(defaultChecked);
const t = useI18n();
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setChecked(event.target.checked);
const params = new URLSearchParams(searchParams);
params.set("includeFromAllUsers", event.target.checked.toString());
if (params.has("page")) params.set("page", "1"); // Reset page to 1
router.replace(`${pathName}?${params.toString()}`);
};
return (
<Switch
defaultChecked={defaultChecked}
checked={checked}
label={t("management.page.media.includeFromAllUsers")}
onChange={onChange}
/>
);
};
@@ -0,0 +1,46 @@
"use client";
import { Button, FileButton } from "@mantine/core";
import { IconUpload } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { supportedMediaUploadFormats } from "@homarr/validation";
export const UploadMedia = () => {
const t = useI18n();
const { mutateAsync, isPending } = clientApi.media.uploadMedia.useMutation();
const handleFileUploadAsync = async (file: File | null) => {
if (!file) return;
const formData = new FormData();
formData.append("file", file);
await mutateAsync(formData, {
onSuccess() {
showSuccessNotification({
message: t("media.action.upload.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("media.action.upload.notification.error.message"),
});
},
async onSettled() {
await revalidatePathActionAsync("/manage/medias");
},
});
};
return (
<FileButton onChange={handleFileUploadAsync} accept={supportedMediaUploadFormats.join(",")}>
{({ onClick }) => (
<Button onClick={onClick} loading={isPending} rightSection={<IconUpload size={16} stroke={1.5} />}>
{t("media.action.upload.label")}
</Button>
)}
</FileButton>
);
};
@@ -0,0 +1,132 @@
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 type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { humanFileSize } from "@homarr/common";
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";
import { CopyMedia } from "./_actions/copy-media";
import { DeleteMedia } from "./_actions/delete-media";
import { IncludeFromAllUsersSwitch } from "./_actions/show-all";
import { UploadMedia } from "./_actions/upload-media";
const searchParamsSchema = z.object({
search: z.string().optional(),
includeFromAllUsers: z
.string()
.regex(/true|false/)
.catch("false")
.transform((value) => value === "true"),
pageSize: z.string().regex(/\d+/).transform(Number).catch(10),
page: z.string().regex(/\d+/).transform(Number).catch(1),
});
type SearchParamsSchemaInputFromSchema<TSchema extends Record<string, unknown>> = Partial<{
[K in keyof TSchema]: Exclude<TSchema[K], undefined> extends unknown[] ? string[] : string;
}>;
interface MediaListPageProps {
searchParams: SearchParamsSchemaInputFromSchema<z.infer<typeof searchParamsSchema>>;
}
export default async function GroupsListPage(props: MediaListPageProps) {
const session = await auth();
if (!session) {
return notFound();
}
const t = await getI18n();
const searchParams = searchParamsSchema.parse(props.searchParams);
const { items: medias, totalCount } = await api.media.getPaginated(searchParams);
return (
<ManageContainer size="xl">
<DynamicBreadcrumb />
<Stack>
<Title>{t("media.plural")}</Title>
<Group justify="space-between">
<Group>
<SearchInput placeholder={`${t("media.search")}...`} defaultValue={searchParams.search} />
{session.user.permissions.includes("media-view-all") && (
<IncludeFromAllUsersSwitch defaultChecked={searchParams.includeFromAllUsers} />
)}
</Group>
{session.user.permissions.includes("media-upload") && <UploadMedia />}
</Group>
<Table striped highlightOnHover>
<TableThead>
<TableTr>
<TableTh></TableTh>
<TableTh>{t("media.field.name")}</TableTh>
<TableTh>{t("media.field.size")}</TableTh>
<TableTh>{t("media.field.creator")}</TableTh>
<TableTh></TableTh>
</TableTr>
</TableThead>
<TableTbody>
{medias.map((media) => (
<Row key={media.id} media={media} />
))}
</TableTbody>
</Table>
<Group justify="end">
<TablePagination total={Math.ceil(totalCount / searchParams.pageSize)} />
</Group>
</Stack>
</ManageContainer>
);
}
interface RowProps {
media: RouterOutputs["media"]["getPaginated"]["items"][number];
}
const Row = async ({ media }: RowProps) => {
const session = await auth();
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}`}
alt={media.name}
width={64}
height={64}
style={{ objectFit: "contain" }}
/>
</TableTd>
<TableTd>{media.name}</TableTd>
<TableTd>{humanFileSize(media.size)}</TableTd>
<TableTd>
{media.creator ? (
<Group gap="sm">
<UserAvatar user={media.creator} size="sm" />
<Anchor component={Link} href={`/manage/users/${media.creator.id}/general`} size="sm">
{media.creator.name}
</Anchor>
</Group>
) : (
"-"
)}
</TableTd>
<TableTd w={64}>
<Group wrap="nowrap" gap="xs">
<CopyMedia media={media} />
{canDelete && <DeleteMedia media={media} />}
</Group>
</TableTd>
</TableTr>
);
};
@@ -64,6 +64,7 @@ export default async function ManagementPage() {
href: "/manage/apps",
subtitle: t("statisticLabel.resources"),
title: t("statistic.app"),
hidden: !session?.user,
},
{
count: statistics.countGroups,
@@ -1,8 +1,12 @@
"use client";
import Link from "next/link";
import { Button, Grid, Group, Stack, Textarea, TextInput } from "@mantine/core";
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 { 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";
@@ -25,6 +29,8 @@ export const SearchEngineForm = (props: SearchEngineFormProps) => {
const { submitButtonTranslation, handleSubmit, initialValues, isPending, disableShort } = props;
const t = useI18n();
const [integrationData] = clientApi.integration.allThatSupportSearch.useSuspenseQuery();
const form = useZodForm(validation.searchEngine.manage, {
initialValues: initialValues ?? {
name: "",
@@ -32,6 +38,7 @@ export const SearchEngineForm = (props: SearchEngineFormProps) => {
iconUrl: "",
urlTemplate: "",
description: "",
type: "generic",
},
});
@@ -52,11 +59,40 @@ export const SearchEngineForm = (props: SearchEngineFormProps) => {
</Grid.Col>
</Grid>
<IconPicker initialValue={initialValues?.iconUrl} {...form.getInputProps("iconUrl")} />
<TextInput
{...form.getInputProps("urlTemplate")}
withAsterisk
label={t("search.engine.field.urlTemplate.label")}
/>
<Fieldset legend={t("search.engine.page.edit.configControl")}>
<SegmentedControl
data={searchEngineTypes.map(
(type) =>
({
label: t(`search.engine.page.edit.searchEngineType.${type}`),
value: type,
}) satisfies SegmentedControlItem,
)}
{...form.getInputProps("type")}
fullWidth
/>
{form.values.type === "generic" && (
<TextInput
{...form.getInputProps("urlTemplate")}
withAsterisk
label={t("search.engine.field.urlTemplate.label")}
/>
)}
{form.values.type === "fromIntegration" && (
<WidgetIntegrationSelect
label="Integration"
data={integrationData}
canSelectMultiple={false}
onChange={(value) => form.setFieldValue("integrationId", value[0])}
value={form.values.integrationId !== undefined ? [form.values.integrationId] : []}
withAsterisk
/>
)}
</Fieldset>
<Textarea {...form.getInputProps("description")} label={t("search.engine.field.description.label")} />
<Group justify="end">
@@ -1,6 +1,8 @@
import { notFound } from "next/navigation";
import { Stack, Title } from "@mantine/core";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { getI18n } from "@homarr/translation/server";
import { ManageContainer } from "~/components/manage/manage-container";
@@ -12,6 +14,12 @@ interface SearchEngineEditPageProps {
}
export default async function SearchEngineEditPage({ params }: SearchEngineEditPageProps) {
const session = await auth();
if (!session?.user.permissions.includes("search-engine-modify-all")) {
notFound();
}
const searchEngine = await api.searchEngine.byId({ id: params.id });
const t = await getI18n();
@@ -1,5 +1,7 @@
import { notFound } from "next/navigation";
import { Stack, Title } from "@mantine/core";
import { auth } from "@homarr/auth/next";
import { getI18n } from "@homarr/translation/server";
import { ManageContainer } from "~/components/manage/manage-container";
@@ -7,6 +9,12 @@ import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { SearchEngineNewForm } from "./_search-engine-new-form";
export default async function SearchEngineNewPage() {
const session = await auth();
if (!session?.user.permissions.includes("search-engine-create")) {
notFound();
}
const t = await getI18n();
return (
@@ -1,9 +1,11 @@
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 type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { SearchInput, TablePagination } from "@homarr/ui";
import { z } from "@homarr/validation";
@@ -11,6 +13,7 @@ import { z } from "@homarr/validation";
import { ManageContainer } from "~/components/manage/manage-container";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NoResults } from "~/components/no-results";
import { SearchEngineDeleteButton } from "./_search-engine-delete-button";
const searchParamsSchema = z.object({
@@ -28,6 +31,12 @@ interface SearchEnginesPageProps {
}
export default async function SearchEnginesPage(props: SearchEnginesPageProps) {
const session = await auth();
if (!session) {
redirect("/auth/login");
}
const searchParams = searchParamsSchema.parse(props.searchParams);
const { items: searchEngines, totalCount } = await api.searchEngine.getPaginated(searchParams);
@@ -40,9 +49,11 @@ export default async function SearchEnginesPage(props: SearchEnginesPageProps) {
<Title>{tEngine("page.list.title")}</Title>
<Group justify="space-between" align="center">
<SearchInput placeholder={`${tEngine("search")}...`} defaultValue={searchParams.search} />
<MobileAffixButton component={Link} href="/manage/search-engines/new">
{tEngine("page.create.title")}
</MobileAffixButton>
{session.user.permissions.includes("search-engine-create") && (
<MobileAffixButton component={Link} href="/manage/search-engines/new">
{tEngine("page.create.title")}
</MobileAffixButton>
)}
</Group>
{searchEngines.length === 0 && <SearchEngineNoResults />}
{searchEngines.length > 0 && (
@@ -67,6 +78,7 @@ interface SearchEngineCardProps {
const SearchEngineCard = async ({ searchEngine }: SearchEngineCardProps) => {
const t = await getScopedI18n("search.engine");
const session = await auth();
return (
<Card>
@@ -91,23 +103,34 @@ const SearchEngineCard = async ({ searchEngine }: SearchEngineCardProps) => {
{searchEngine.description}
</Text>
)}
<Anchor href={searchEngine.urlTemplate.replace("%s", "test")} lineClamp={1} size="sm">
{searchEngine.urlTemplate}
</Anchor>
{searchEngine.type === "generic" && searchEngine.urlTemplate !== null && (
<Anchor href={searchEngine.urlTemplate.replace("%s", "test")} lineClamp={1} size="sm">
{searchEngine.urlTemplate}
</Anchor>
)}
{searchEngine.type === "fromIntegration" && searchEngine.integrationId !== null && (
<Text c="dimmed" size="sm">
{t("page.list.interactive")}
</Text>
)}
</Stack>
</Group>
<Group>
<ActionIconGroup>
<ActionIcon
component={Link}
href={`/manage/search-engines/edit/${searchEngine.id}`}
variant="subtle"
color="gray"
aria-label={t("page.edit.title")}
>
<IconPencil size={16} stroke={1.5} />
</ActionIcon>
<SearchEngineDeleteButton searchEngine={searchEngine} />
{session?.user.permissions.includes("search-engine-modify-all") && (
<ActionIcon
component={Link}
href={`/manage/search-engines/edit/${searchEngine.id}`}
variant="subtle"
color="gray"
aria-label={t("page.edit.title")}
>
<IconPencil size={16} stroke={1.5} />
</ActionIcon>
)}
{session?.user.permissions.includes("search-engine-full-all") && (
<SearchEngineDeleteButton searchEngine={searchEngine} />
)}
</ActionIconGroup>
</Group>
</Group>
@@ -117,16 +140,17 @@ const SearchEngineCard = async ({ searchEngine }: SearchEngineCardProps) => {
const SearchEngineNoResults = async () => {
const t = await getI18n();
const session = await auth();
return (
<Card withBorder bg="transparent">
<Stack align="center" gap="sm">
<IconSearch size="2rem" />
<Text fw={500} size="lg">
{t("search.engine.page.list.noResults.title")}
</Text>
<Anchor href="/manage/search-engines/new">{t("search.engine.page.list.noResults.action")}</Anchor>
</Stack>
</Card>
<NoResults
icon={IconSearch}
title={t("search.engine.page.list.noResults.title")}
action={{
label: t("search.engine.page.list.noResults.action"),
href: "/manage/search-engines/new",
hidden: !session?.user.permissions.includes("search-engine-create"),
}}
/>
);
};
@@ -0,0 +1,52 @@
"use client";
import { Group, Text } from "@mantine/core";
import { IconMoon, IconSun } from "@tabler/icons-react";
import type { ColorScheme } from "@homarr/definitions";
import { colorSchemes } from "@homarr/definitions";
import type { ServerSettings } from "@homarr/server-settings";
import { useScopedI18n } from "@homarr/translation/client";
import { SelectWithCustomItems } from "@homarr/ui";
import { CommonSettingsForm } from "./common-form";
export const AppearanceSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["appearance"] }) => {
const tApperance = useScopedI18n("management.page.settings.section.appearance");
return (
<CommonSettingsForm settingKey="appearance" defaultValues={defaultValues}>
{(form) => (
<>
<SelectWithCustomItems
label={tApperance("defaultColorScheme.label")}
data={colorSchemes.map((scheme) => ({
value: scheme,
label: tApperance(`defaultColorScheme.options.${scheme}`),
}))}
{...form.getInputProps("defaultColorScheme")}
SelectOption={ApperanceCustomOption}
/>
</>
)}
</CommonSettingsForm>
);
};
const appearanceIcons = {
light: IconSun,
dark: IconMoon,
};
const ApperanceCustomOption = ({ value, label }: { value: ColorScheme; label: string }) => {
const Icon = appearanceIcons[value];
return (
<Group>
<Icon size={16} stroke={1.5} />
<Text fz="sm" fw={500}>
{label}
</Text>
</Group>
);
};
@@ -0,0 +1,44 @@
"use client";
import { Group, Text } from "@mantine/core";
import { IconLayoutDashboard } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import type { ServerSettings } from "@homarr/server-settings";
import { useScopedI18n } from "@homarr/translation/client";
import { SelectWithCustomItems } from "@homarr/ui";
import { CommonSettingsForm } from "./common-form";
export const BoardSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["board"] }) => {
const tBoard = useScopedI18n("management.page.settings.section.board");
const [selectableBoards] = clientApi.board.getPublicBoards.useSuspenseQuery();
return (
<CommonSettingsForm settingKey="board" defaultValues={defaultValues}>
{(form) => (
<>
<SelectWithCustomItems
label={tBoard("homeBoard.label")}
description={tBoard("homeBoard.description")}
data={selectableBoards.map((board) => ({
value: board.id,
label: board.name,
image: board.logoImageUrl,
}))}
SelectOption={({ label, image }: { value: string; label: string; image: string | null }) => (
<Group>
{/* eslint-disable-next-line @next/next/no-img-element */}
{image ? <img width={16} height={16} src={image} alt={label} /> : <IconLayoutDashboard size={16} />}
<Text fz="sm" fw={500}>
{label}
</Text>
</Group>
)}
{...form.getInputProps("homeBoardId")}
/>
</>
)}
</CommonSettingsForm>
);
};
@@ -0,0 +1,57 @@
"use client";
import { Button, Group, Stack } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { useForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { ServerSettings } from "@homarr/server-settings";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
export const CommonSettingsForm = <TKey extends keyof ServerSettings>({
settingKey,
defaultValues,
children,
}: {
settingKey: TKey;
defaultValues: ServerSettings[TKey];
children: (form: ReturnType<typeof useForm<ServerSettings[TKey]>>) => React.ReactNode;
}) => {
const t = useI18n();
const tSettings = useScopedI18n("management.page.settings");
const { mutateAsync, isPending } = clientApi.serverSettings.saveSettings.useMutation({
onSuccess() {
showSuccessNotification({
message: tSettings("notification.success.message"),
});
},
onError() {
showErrorNotification({
message: tSettings("notification.error.message"),
});
},
});
const form = useForm({
initialValues: defaultValues,
});
const handleSubmitAsync = async (values: ServerSettings[TKey]) => {
await mutateAsync({
settingsKey: settingKey,
value: values,
});
};
return (
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
<Stack gap="sm">
{children(form)}
<Group justify="end">
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};
@@ -0,0 +1,26 @@
"use client";
import type { ServerSettings } from "@homarr/server-settings";
import type { SupportedLanguage } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client";
import { LanguageCombobox } from "~/components/language/language-combobox";
import { CommonSettingsForm } from "./common-form";
export const CultureSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["culture"] }) => {
const tCulture = useScopedI18n("management.page.settings.section.culture");
return (
<CommonSettingsForm settingKey="culture" defaultValues={defaultValues}>
{(form) => (
<>
<LanguageCombobox
label={tCulture("defaultLocale.label")}
value={form.getInputProps("defaultLocale").value as SupportedLanguage}
{...form.getInputProps("defaultLocale")}
/>
</>
)}
</CommonSettingsForm>
);
};
@@ -6,6 +6,9 @@ import { getScopedI18n } from "@homarr/translation/server";
import { CrawlingAndIndexingSettings } from "~/app/[locale]/manage/settings/_components/crawling-and-indexing.settings";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { AnalyticsSettings } from "./_components/analytics.settings";
import { AppearanceSettingsForm } from "./_components/appearance-settings-form";
import { BoardSettingsForm } from "./_components/board-settings-form";
import { CultureSettingsForm } from "./_components/culture-settings-form";
export async function generateMetadata() {
const t = await getScopedI18n("management");
@@ -18,14 +21,26 @@ export async function generateMetadata() {
export default async function SettingsPage() {
const serverSettings = await api.serverSettings.getAll();
const t = await getScopedI18n("management.page.settings");
const tSettings = await getScopedI18n("management.page.settings");
return (
<>
<DynamicBreadcrumb />
<Stack>
<Title order={1}>{t("title")}</Title>
<Title order={1}>{tSettings("title")}</Title>
<AnalyticsSettings initialData={serverSettings.analytics} />
<CrawlingAndIndexingSettings initialData={serverSettings.crawlingAndIndexing} />
<Stack>
<Title order={2}>{tSettings("section.board.title")}</Title>
<BoardSettingsForm defaultValues={serverSettings.board} />
</Stack>
<Stack>
<Title order={2}>{tSettings("section.appearance.title")}</Title>
<AppearanceSettingsForm defaultValues={serverSettings.appearance} />
</Stack>
<Stack>
<Title order={2}>{tSettings("section.culture.title")}</Title>
<CultureSettingsForm defaultValues={serverSettings.culture} />
</Stack>
</Stack>
</>
);
@@ -23,7 +23,7 @@ export const ApiKeysManagement = ({ apiKeys }: ApiKeysManagementProps) => {
const { mutate, isPending } = clientApi.apiKeys.create.useMutation({
async onSuccess(data) {
openModal({
apiKey: data.randomToken,
apiKey: data.apiKey,
});
await revalidatePathActionAsync("/manage/tools/api");
},
@@ -1,6 +1,6 @@
"use client";
import type { OpenAPIV3 } from "openapi-types";
import type { OpenAPIObject } from "openapi3-ts/oas31";
import SwaggerUI from "swagger-ui-react";
// workaround for CSS that cannot be processed by next.js, https://github.com/swagger-api/swagger-ui/issues/10045
@@ -9,7 +9,7 @@ import "../swagger-ui-overrides.css";
import "../swagger-ui.css";
interface SwaggerUIClientProps {
document: OpenAPIV3.Document;
document: OpenAPIObject;
}
export const SwaggerUIClient = ({ document }: SwaggerUIClientProps) => {
@@ -27,7 +27,7 @@ export async function generateMetadata() {
export default async function LogsManagementPage() {
const session = await auth();
if (!session?.user || !session.user.permissions.includes("admin")) {
if (!session?.user || !session.user.permissions.includes("other-view-logs")) {
notFound();
}
@@ -31,7 +31,9 @@ export const DeleteUserButton = ({ user }: DeleteUserButtonProps) => {
children: t("user.action.delete.confirm", { username: user.name }),
// eslint-disable-next-line no-restricted-syntax
async onConfirm() {
await mutateUserDeletionAsync(user.id);
await mutateUserDeletionAsync({
userId: user.id,
});
},
}),
[user, mutateUserDeletionAsync, openConfirmModal, t],
@@ -1,6 +1,7 @@
"use client";
import { Button, Group, Radio, Stack } from "@mantine/core";
import type { DayOfWeek } from "@mantine/dates";
import dayjs from "dayjs";
import localeData from "dayjs/plugin/localeData";
@@ -43,7 +44,7 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
});
const form = useZodForm(validation.user.firstDayOfWeek, {
initialValues: {
firstDayOfWeek: user.firstDayOfWeek,
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
},
});
@@ -6,9 +6,9 @@ import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { LanguageCombobox } from "~/components/language/language-combobox";
import { CurrentLanguageCombobox } from "~/components/language/current-language-combobox";
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
import { catchTrpcNotFound } from "~/errors/trpc-not-found";
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
import { createMetaTitle } from "~/metadata";
import { canAccessUserEditPage } from "../access";
import { ChangeHomeBoardForm } from "./_components/_change-home-board";
@@ -81,7 +81,7 @@ export default async function EditUserPage({ params }: Props) {
<Stack mb="lg">
<Title order={2}>{tGeneral("item.language")}</Title>
<LanguageCombobox />
<CurrentLanguageCombobox />
</Stack>
<Stack mb="lg">
@@ -10,7 +10,7 @@ import { UserAvatar } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { catchTrpcNotFound } from "~/errors/trpc-not-found";
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
import { NavigationLink } from "../groups/[id]/_navigation";
import { canAccessUserEditPage } from "./access";
@@ -5,7 +5,7 @@ import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { getScopedI18n } from "@homarr/translation/server";
import { catchTrpcNotFound } from "~/errors/trpc-not-found";
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
import { canAccessUserEditPage } from "../access";
import { ChangePasswordForm } from "./_components/_change-password-form";
@@ -1,20 +1,48 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { Card, PasswordInput, Stack, Stepper, Text, TextInput, Title } from "@mantine/core";
import { IconUserCheck } from "@tabler/icons-react";
import { startTransition, useCallback, useMemo, useState } from "react";
import {
Badge,
Button,
Card,
Group,
PasswordInput,
Stack,
Stepper,
Table,
Text,
TextInput,
Title,
Tooltip,
} from "@mantine/core";
import { useListState } from "@mantine/hooks";
import { IconPlus, IconUserCheck } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { everyoneGroup, groupPermissions } from "@homarr/definitions";
import type { GroupPermissionKey } from "@homarr/definitions";
import { useZodForm } from "@homarr/form";
import { useModalAction } from "@homarr/modals";
import { showErrorNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput, UserAvatar } from "@homarr/ui";
import { validation, z } from "@homarr/validation";
import { createCustomErrorParams } from "@homarr/validation/form";
import { GroupSelectModal } from "~/components/access/group-select-modal";
import { StepperNavigationComponent } from "./stepper-navigation";
export const UserCreateStepperComponent = () => {
interface GroupWithPermissions {
id: string;
name: string;
permissions: GroupPermissionKey[];
}
interface UserCreateStepperComponentProps {
initialGroups: GroupWithPermissions[];
}
export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperComponentProps) => {
const t = useScopedI18n("management.page.user.create");
const tUserField = useScopedI18n("user.field");
@@ -60,7 +88,10 @@ export const UserCreateStepperComponent = () => {
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
params: createCustomErrorParams("passwordsDoNotMatch"),
params: createCustomErrorParams({
key: "passwordsDoNotMatch",
params: {},
}),
}),
{
initialValues: {
@@ -70,7 +101,18 @@ export const UserCreateStepperComponent = () => {
},
);
const allForms = useMemo(() => [generalForm, securityForm], [generalForm, securityForm]);
const groupsForm = useZodForm(
z.object({
groups: z.array(z.string()),
}),
{
initialValues: {
groups: initialGroups.map((group) => group.id),
},
},
);
const allForms = useMemo(() => [generalForm, securityForm, groupsForm], [generalForm, securityForm, groupsForm]);
const activeForm = allForms[active];
const isCurrentFormValid = activeForm ? activeForm.isValid : () => true;
@@ -83,10 +125,11 @@ export const UserCreateStepperComponent = () => {
email: generalForm.values.email,
password: securityForm.values.password,
confirmPassword: securityForm.values.confirmPassword,
groupIds: groupsForm.values.groups,
});
}
nextStep();
}, [active, generalForm, mutateAsync, securityForm, nextStep]);
}, [active, generalForm, securityForm, groupsForm, mutateAsync, nextStep]);
const reset = useCallback(() => {
setActive(0);
@@ -141,13 +184,18 @@ export const UserCreateStepperComponent = () => {
</Card>
</form>
</Stepper.Step>
<Stepper.Step
label={t("step.permissions.label")}
description={t("step.permissions.description")}
allowStepSelect={false}
allowStepClick={false}
>
3
<Stepper.Step label={t("step.groups.label")} allowStepSelect={false} allowStepClick={false}>
<Card p="xl">
<GroupsForm
initialGroups={initialGroups}
addGroup={(groupId) =>
groupsForm.setValues((value) => ({ groups: value.groups?.concat(groupId) ?? [groupId] }))
}
removeGroup={(groupId) => {
groupsForm.setValues((value) => ({ groups: value.groups?.filter((group) => group !== groupId) ?? [] }));
}}
/>
</Card>
</Stepper.Step>
<Stepper.Step label={t("step.review.label")} allowStepSelect={false} allowStepClick={false}>
<Card p="xl">
@@ -180,3 +228,111 @@ export const UserCreateStepperComponent = () => {
</>
);
};
interface GroupsFormProps {
addGroup: (groupId: string) => void;
removeGroup: (groupId: string) => void;
initialGroups: GroupWithPermissions[];
}
const GroupsForm = ({ addGroup, removeGroup, initialGroups }: GroupsFormProps) => {
const t = useI18n();
const [groups, { append, filter }] = useListState<GroupWithPermissions>(initialGroups);
const { openModal } = useModalAction(GroupSelectModal);
const handleAddClick = () => {
openModal({
presentGroupIds: groups.map((group) => group.id),
withPermissions: true,
onSelect({ id, name, permissions }) {
if (!permissions) return;
startTransition(() => {
addGroup(id);
append({ id, name, permissions });
});
},
});
};
const handleGroupRemove = (id: string) => {
filter((group) => group.id !== id);
removeGroup(id);
};
return (
<form>
<Stack>
<Group justify="space-between">
<Stack gap={0}>
<Text fw={500}>{t("management.page.user.create.step.groups.title")}</Text>
<Text size="sm" c="gray.6">
{t("management.page.user.create.step.groups.description", { everyoneGroup })}
</Text>
</Stack>
<Button
variant="subtle"
color="gray"
leftSection={<IconPlus size={16} stroke={1.5} />}
onClick={handleAddClick}
>
{t("common.action.add")}
</Button>
</Group>
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>{t("group.field.name")}</Table.Th>
<Table.Th>{t("permission.title")}</Table.Th>
<Table.Th></Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{groups.map((group) => (
<Table.Tr key={group.id}>
<Table.Td>{group.name}</Table.Td>
<Table.Td w="100%">
<Group gap="xs">
{Object.entries(groupPermissions)
.flatMap(([key, values]) =>
Array.isArray(values)
? values.map((value) => ({ key, value: value as string }))
: [{ key, value: key }],
)
.filter(({ key, value }) =>
group.permissions.some(
(permission) => permission === (key === value ? key : `${key}-${value}`),
),
)
.map(({ key, value }) => (
<PermissionBadge key={`${key}-${value}`} category={key} value={value} />
))}
</Group>
</Table.Td>
<Table.Td>
{group.name !== everyoneGroup && (
<Button variant="subtle" onClick={() => handleGroupRemove(group.id)}>
{t("common.action.remove")}
</Button>
)}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Stack>
</form>
);
};
const PermissionBadge = ({ category, value }: { category: string; value: string }) => {
const t = useI18n();
return (
<Tooltip label={t(`group.permission.${category}.item.${value}.description` as never)}>
<Badge color={category === "admin" ? "red" : "blue"} size="sm" variant="dot">
{t(`group.permission.${category}.item.${value}.label` as never)}
</Badge>
</Tooltip>
);
};
@@ -2,6 +2,9 @@ import { notFound } from "next/navigation";
import { auth } from "@homarr/auth/next";
import { isProviderEnabled } from "@homarr/auth/server";
import { db, inArray } from "@homarr/db";
import { groups } from "@homarr/db/schema/sqlite";
import { everyoneGroup } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
@@ -33,10 +36,22 @@ export default async function CreateUserPage() {
return notFound();
}
const initialGroups = await db.query.groups.findMany({
where: inArray(groups.name, [everyoneGroup]),
with: {
permissions: true,
},
});
return (
<>
<DynamicBreadcrumb />
<UserCreateStepperComponent />
<UserCreateStepperComponent
initialGroups={initialGroups.map((group) => ({
...group,
permissions: group.permissions.map(({ permission }) => permission),
}))}
/>
</>
);
}
@@ -2,7 +2,7 @@
import { useCallback } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@mantine/core";
import { Button, useMatches } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
@@ -61,8 +61,14 @@ export const DeleteGroup = ({ group }: DeleteGroupProps) => {
});
}, [tDelete, tRoot, openConfirmModal, group.id, group.name, mutateAsync, router]);
const fullWidth = useMatches({
xs: true,
sm: true,
md: false,
});
return (
<Button variant="subtle" color="red" onClick={handleDeletion}>
<Button variant="subtle" color="red" onClick={handleDeletion} fullWidth={fullWidth}>
{tDelete("label")}
</Button>
);
@@ -58,7 +58,7 @@ export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => {
},
);
},
[group.id, mutate, t],
[group.id, mutate, t, disabled],
);
return (
@@ -1,17 +1,19 @@
"use client";
import Link from "next/link";
import { Alert, Anchor } from "@mantine/core";
import { IconExclamationCircle } from "@tabler/icons-react";
import { createDocumentationLink } from "@homarr/definitions";
import { getI18n } from "@homarr/translation/server";
import { useI18n } from "@homarr/translation/client";
export const ReservedGroupAlert = async () => {
const t = await getI18n();
export const ReservedGroupAlert = () => {
const t = useI18n();
return (
<Alert variant="light" color="yellow" icon={<IconExclamationCircle size="1rem" stroke={1.5} />}>
{t("group.reservedNotice.message", {
checkoutDocs: (
{t.rich("group.reservedNotice.message", {
checkoutDocs: () => (
<Anchor
size="sm"
component={Link}
@@ -1,9 +1,10 @@
"use client";
import { useCallback, useState } from "react";
import { Button } from "@mantine/core";
import { Button, useMatches } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useConfirmModal, useModalAction } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
@@ -46,7 +47,7 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
userId: id,
},
{
onSuccess() {
async onSuccess() {
setInnerOwnerId(id);
showSuccessNotification({
title: tRoot("common.notification.transfer.success"),
@@ -55,6 +56,7 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
user: name,
}),
});
await revalidatePathActionAsync(`/manage/users/groups/${group.id}`);
},
onError() {
showErrorNotification({
@@ -74,8 +76,14 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
);
}, [group.id, group.name, innerOwnerId, mutateAsync, openConfirmModal, openModal, tRoot, tTransfer]);
const fullWidth = useMatches({
xs: true,
sm: true,
md: false,
});
return (
<Button variant="subtle" color="red" onClick={handleTransfer}>
<Button variant="subtle" color="red" onClick={handleTransfer} fullWidth={fullWidth}>
{tTransfer("label")}
</Button>
);
@@ -32,6 +32,7 @@ export const AddGroupMember = ({ groupId, presentUserIds }: AddGroupMemberProps)
await revalidatePathActionAsync(`/manage/users/groups/${groupId}}/members`);
},
presentUserIds,
excludeExternalProviders: true,
},
{
title: tMembersAdd("label"),
@@ -1,8 +1,9 @@
import { Stack, Title } from "@mantine/core";
import { Card, Group, Stack, Text, Title } from "@mantine/core";
import { api } from "@homarr/api/server";
import { everyoneGroup } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { UserAvatar } from "@homarr/ui";
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
import { DeleteGroup } from "./_delete-group";
@@ -30,6 +31,30 @@ export default async function GroupsDetailPage({ params }: GroupsDetailPageProps
<RenameGroupForm group={group} disabled={isReserved} />
<Title order={2}>{tGeneral("owner")}</Title>
<Card>
{group.owner ? (
<Group>
<UserAvatar user={{ name: group.owner.name, image: group.owner.image }} size={"lg"} />
<Stack align={"start"} gap={3}>
<Text fw={"bold"}>{group.owner.name}</Text>
<Text>{group.owner.email}</Text>
<Text c={"dimmed"} size={"sm"}>
{tGeneral("ownerOfGroup")}
</Text>
</Stack>
</Group>
) : (
<Group>
<Stack align={"start"} gap={3}>
<Text c={"dimmed"} size={"sm"}>
{tGeneral("ownerOfGroupDeleted")}
</Text>
</Stack>
</Group>
)}
</Card>
{!isReserved && (
<DangerZoneRoot>
<DangerZoneItem
@@ -45,9 +45,7 @@ export const PermissionForm = ({ children, initialPermissions }: PropsWithChildr
);
};
type FormType = {
[key in GroupPermissionKey]: boolean;
};
type FormType = Record<GroupPermissionKey, boolean>;
export const PermissionSwitch = ({ name }: { name: GroupPermissionKey }) => {
const form = useFormContext();
@@ -11,13 +11,9 @@ import { useModalAction } from "@homarr/modals";
import { showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import type { BoardItemAdvancedOptions } from "@homarr/validation";
import {
loadWidgetDynamic,
reduceWidgetOptionsWithDefaultValues,
WidgetEditModal,
widgetImports,
} from "@homarr/widgets";
import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
import { WidgetError } from "@homarr/widgets/errors";
import { WidgetEditModal } from "@homarr/widgets/modals";
import type { Dimensions } from "./_dimension-modal";
import { PreviewDimensionsModal } from "./_dimension-modal";
+47 -10
View File
@@ -1,15 +1,26 @@
import { createOpenApiFetchHandler } from "trpc-swagger/build/index.mjs";
import { headers } from "next/headers";
import { userAgent } from "next/server";
import type { NextRequest } from "next/server";
import { createOpenApiFetchHandler } from "trpc-to-openapi";
import { appRouter, createTRPCContext } from "@homarr/api";
import { hashPasswordAsync } from "@homarr/auth";
import type { Session } from "@homarr/auth";
import { createSessionAsync } from "@homarr/auth/server";
import { db, eq } from "@homarr/db";
import { apiKeys } from "@homarr/db/schema/sqlite";
import { logger } from "@homarr/log";
const handlerAsync = async (req: Request) => {
const handlerAsync = async (req: NextRequest) => {
const apiKeyHeaderValue = req.headers.get("ApiKey");
const session: Session | null = await getSessionOrDefaultFromHeadersAsync(apiKeyHeaderValue);
const ipAddress = req.ip ?? headers().get("x-forwarded-for");
const { ua } = userAgent(req);
const session: Session | null = await getSessionOrDefaultFromHeadersAsync(apiKeyHeaderValue, ipAddress, ua);
// Fallback to JSON if no content type is set
if (!req.headers.has("Content-Type")) {
req.headers.set("Content-Type", "application/json");
}
return createOpenApiFetchHandler({
req,
@@ -19,7 +30,11 @@ const handlerAsync = async (req: Request) => {
});
};
const getSessionOrDefaultFromHeadersAsync = async (apiKeyHeaderValue: string | null): Promise<Session | null> => {
const getSessionOrDefaultFromHeadersAsync = async (
apiKeyHeaderValue: string | null,
ipAdress: string | null,
userAgent: string,
): Promise<Session | null> => {
logger.info(
`Creating OpenAPI fetch handler for user ${apiKeyHeaderValue ? "with an api key" : "without an api key"}`,
);
@@ -28,12 +43,21 @@ const getSessionOrDefaultFromHeadersAsync = async (apiKeyHeaderValue: string | n
return null;
}
const [apiKeyId, apiKey] = apiKeyHeaderValue.split(".");
if (!apiKeyId || !apiKey) {
logger.warn(
`An attempt to authenticate over API has failed due to invalid API key format ip='${ipAdress}' userAgent='${userAgent}'`,
);
return null;
}
const apiKeyFromDb = await db.query.apiKeys.findFirst({
where: eq(apiKeys.apiKey, apiKeyHeaderValue),
where: eq(apiKeys.id, apiKeyId),
columns: {
id: true,
apiKey: false,
salt: false,
apiKey: true,
salt: true,
},
with: {
user: {
@@ -47,8 +71,15 @@ const getSessionOrDefaultFromHeadersAsync = async (apiKeyHeaderValue: string | n
},
});
if (apiKeyFromDb === undefined) {
logger.warn("An attempt to authenticate over API has failed");
if (!apiKeyFromDb) {
logger.warn(`An attempt to authenticate over API has failed ip='${ipAdress}' userAgent='${userAgent}'`);
return null;
}
const hashedApiKey = await hashPasswordAsync(apiKey, apiKeyFromDb.salt);
if (apiKeyFromDb.apiKey !== hashedApiKey) {
logger.warn(`An attempt to authenticate over API has failed ip='${ipAdress}' userAgent='${userAgent}'`);
return null;
}
@@ -56,4 +87,10 @@ const getSessionOrDefaultFromHeadersAsync = async (apiKeyHeaderValue: string | n
return await createSessionAsync(db, apiKeyFromDb.user);
};
export { handlerAsync as GET, handlerAsync as POST };
export {
handlerAsync as GET,
handlerAsync as POST,
handlerAsync as PUT,
handlerAsync as DELETE,
handlerAsync as PATCH,
};
@@ -5,10 +5,23 @@ import type { SupportedAuthProvider } from "@homarr/definitions";
import { logger } from "@homarr/log";
export const GET = async (req: NextRequest) => {
return await createHandlers(extractProvider(req)).handlers.GET(reqWithTrustedOrigin(req));
return await createHandlers(extractProvider(req), isSecureCookieEnabled(req)).handlers.GET(reqWithTrustedOrigin(req));
};
export const POST = async (req: NextRequest) => {
return await createHandlers(extractProvider(req)).handlers.POST(reqWithTrustedOrigin(req));
return await createHandlers(extractProvider(req), isSecureCookieEnabled(req)).handlers.POST(
reqWithTrustedOrigin(req),
);
};
/**
* wheter to use secure cookies or not, is only supported for https.
* For http it will not add the cookie as it is not considered secure.
* @param req request containing the url
* @returns true if the request is https, false otherwise
*/
const isSecureCookieEnabled = (req: NextRequest): boolean => {
const url = new URL(req.url);
return url.protocol === "https:";
};
/**
@@ -0,0 +1,29 @@
import { notFound } from "next/navigation";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { db, eq } from "@homarr/db";
import { medias } from "@homarr/db/schema/sqlite";
export async function GET(_req: NextRequest, { params }: { params: { id: string } }) {
const image = await db.query.medias.findFirst({
where: eq(medias.id, params.id),
columns: {
content: true,
contentType: true,
},
});
if (!image) {
notFound();
}
const headers = new Headers();
headers.set("Content-Type", image.contentType);
headers.set("Content-Length", image.content.length.toString());
return new NextResponse(image.content, {
status: 200,
headers,
});
}
@@ -2,13 +2,15 @@ import { useState } from "react";
import { Button, Group, Loader, Select, Stack } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import type { GroupPermissionKey } from "@homarr/definitions";
import { useForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
interface InnerProps {
withPermissions?: boolean;
presentGroupIds: string[];
onSelect: (props: { id: string; name: string }) => void | Promise<void>;
onSelect: (props: { id: string; name: string; permissions?: GroupPermissionKey[] }) => void | Promise<void>;
confirmLabel?: string;
}
@@ -18,7 +20,9 @@ interface GroupSelectFormType {
export const GroupSelectModal = createModal<InnerProps>(({ actions, innerProps }) => {
const t = useI18n();
const { data: groups, isPending } = clientApi.group.selectable.useQuery();
const { data: groups, isPending } = clientApi.group.selectable.useQuery({
withPermissions: innerProps.withPermissions,
});
const [loading, setLoading] = useState(false);
const form = useForm<GroupSelectFormType>();
const handleSubmitAsync = async (values: GroupSelectFormType) => {
@@ -28,6 +32,7 @@ export const GroupSelectModal = createModal<InnerProps>(({ actions, innerProps }
await innerProps.onSelect({
id: currentGroup.id,
name: currentGroup.name,
permissions: "permissions" in currentGroup ? (currentGroup.permissions as GroupPermissionKey[]) : undefined,
});
setLoading(false);
@@ -12,6 +12,7 @@ import { UserAvatar } from "@homarr/ui";
interface InnerProps {
presentUserIds: string[];
excludeExternalProviders?: boolean;
onSelect: (props: { id: string; name: string; image: string }) => void | Promise<void>;
confirmLabel?: string;
}
@@ -22,7 +23,9 @@ interface UserSelectFormType {
export const UserSelectModal = createModal<InnerProps>(({ actions, innerProps }) => {
const t = useI18n();
const { data: users, isPending } = clientApi.user.selectable.useQuery();
const { data: users, isPending } = clientApi.user.selectable.useQuery({
excludeExternalProviders: innerProps.excludeExternalProviders,
});
const [loading, setLoading] = useState(false);
const form = useForm<UserSelectFormType>();
const handleSubmitAsync = async (values: UserSelectFormType) => {
@@ -5,7 +5,8 @@ import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil, IconTrash } f
import { clientApi } from "@homarr/api/client";
import { useConfirmModal, useModalAction } from "@homarr/modals";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { WidgetEditModal, widgetImports } from "@homarr/widgets";
import { widgetImports } from "@homarr/widgets";
import { WidgetEditModal } from "@homarr/widgets/modals";
import type { Item } from "~/app/[locale]/boards/_types";
import { useEditMode } from "~/app/[locale]/boards/(content)/_context";
@@ -0,0 +1,54 @@
import type { RouterOutputs } from "@homarr/api";
import type { CategorySection } from "~/app/[locale]/boards/_types";
export interface MoveCategoryInput {
id: string;
direction: "up" | "down";
}
export const moveCategoryCallback =
(input: MoveCategoryInput) =>
(previous: RouterOutputs["board"]["getHomeBoard"]): RouterOutputs["board"]["getHomeBoard"] => {
const currentCategory = previous.sections.find(
(section): section is CategorySection => section.kind === "category" && section.id === input.id,
);
if (!currentCategory) {
return previous;
}
if (currentCategory.yOffset === 1 && input.direction === "up") {
return previous;
}
if (currentCategory.yOffset === previous.sections.length - 2 && input.direction === "down") {
return previous;
}
return {
...previous,
sections: previous.sections.map((section) => {
if (section.kind !== "category" && section.kind !== "empty") {
return section;
}
const offset = input.direction === "up" ? -2 : 2;
// Move category and empty section
if (section.yOffset === currentCategory.yOffset || section.yOffset === currentCategory.yOffset + 1) {
return {
...section,
yOffset: section.yOffset + offset,
};
}
if (
section.yOffset === currentCategory.yOffset + offset ||
section.yOffset === currentCategory.yOffset + offset + 1
) {
return {
...section,
yOffset: section.yOffset - offset,
};
}
return section;
}),
};
};
@@ -0,0 +1,76 @@
import { describe, expect, test } from "vitest";
import type { Section } from "~/app/[locale]/boards/_types";
import { moveCategoryCallback } from "../move-category";
describe("Move Category", () => {
test.each([
[3, [0, 3, 4, 1, 2, 5, 6]],
[5, [0, 1, 2, 5, 6, 3, 4]],
])("should move category up", (moveId, expectedOrder) => {
const sections = createSections(3);
const input = {
id: moveId.toString(),
direction: "up" as const,
};
const result = moveCategoryCallback(input)({ sections } as never);
expect(sortSections(result.sections).map((section) => parseInt(section.id, 10))).toEqual(expectedOrder);
});
test.each([
[1, [0, 3, 4, 1, 2, 5, 6]],
[3, [0, 1, 2, 5, 6, 3, 4]],
])("should move category down", (moveId, expectedOrder) => {
const sections = createSections(3);
const input = {
id: moveId.toString(),
direction: "down" as const,
};
const result = moveCategoryCallback(input)({ sections } as never);
expect(sortSections(result.sections).map((section) => parseInt(section.id, 10))).toEqual(expectedOrder);
});
test("should not move category up if it is at the top", () => {
const sections = createSections(3);
const input = {
id: "1",
direction: "up" as const,
};
const result = moveCategoryCallback(input)({ sections } as never);
expect(sortSections(result.sections).map((section) => parseInt(section.id, 10))).toEqual([0, 1, 2, 3, 4, 5, 6]);
});
test("should not move category down if it is at the bottom", () => {
const sections = createSections(3);
const input = {
id: "5",
direction: "down" as const,
};
const result = moveCategoryCallback(input)({ sections } as never);
expect(sortSections(result.sections).map((section) => parseInt(section.id, 10))).toEqual([0, 1, 2, 3, 4, 5, 6]);
});
});
const createSections = (categoryCount: number) => {
return Array.from({ length: categoryCount * 2 + 1 }, (_, index) => ({
id: index.toString(),
kind: index % 2 === 1 ? ("category" as const) : ("empty" as const),
name: `Category ${index}`,
yOffset: index,
xOffset: 0,
items: [],
})) satisfies Section[];
};
const sortSections = (sections: Section[]) => {
return sections.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset);
};
@@ -4,6 +4,8 @@ import { createId } from "@homarr/db/client";
import type { CategorySection, EmptySection, Section } from "~/app/[locale]/boards/_types";
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client";
import type { MoveCategoryInput } from "./actions/move-category";
import { moveCategoryCallback } from "./actions/move-category";
interface AddCategory {
name: string;
@@ -15,11 +17,6 @@ interface RenameCategory {
name: string;
}
interface MoveCategory {
id: string;
direction: "up" | "down";
}
interface RemoveCategory {
id: string;
}
@@ -128,52 +125,8 @@ export const useCategoryActions = () => {
);
const moveCategory = useCallback(
({ id, direction }: MoveCategory) => {
updateBoard((previous) => {
const currentCategory = previous.sections.find(
(section): section is CategorySection => section.kind === "category" && section.id === id,
);
if (!currentCategory) return previous;
if (currentCategory.yOffset === 1 && direction === "up") return previous;
if (currentCategory.yOffset === previous.sections.length - 2 && direction === "down") return previous;
return {
...previous,
sections: previous.sections.map((section) => {
if (section.kind !== "category" && section.kind !== "empty") return section;
const offset = direction === "up" ? -2 : 2;
// Move category and empty section
if (section.yOffset === currentCategory.yOffset || section.yOffset - 1 === currentCategory.yOffset) {
return {
...section,
yOffset: section.yOffset + offset,
};
}
if (
direction === "up" &&
(section.yOffset === currentCategory.yOffset - 2 || section.yOffset === currentCategory.yOffset - 1)
) {
return {
...section,
position: section.yOffset + 2,
};
}
if (
direction === "down" &&
(section.yOffset === currentCategory.yOffset + 2 || section.yOffset === currentCategory.yOffset + 3)
) {
return {
...section,
position: section.yOffset - 2,
};
}
return section;
}),
};
});
(input: MoveCategoryInput) => {
updateBoard(moveCategoryCallback(input));
},
[updateBoard],
);
@@ -71,15 +71,6 @@ export const useGridstack = (section: Omit<Section, "items">, itemIds: string[])
? section.width
: board.columnCount;
useCssVariableConfiguration({
columnCount,
gridRef,
wrapperRef,
width,
height,
isDynamic: section.kind === "dynamic",
});
const itemRefKeys = Object.keys(itemRefs.current);
// define items in itemRefs for easy access and reference to items
if (itemRefKeys.length !== itemIds.length) {
@@ -95,11 +86,6 @@ export const useGridstack = (section: Omit<Section, "items">, itemIds: string[])
});
}
// Toggle the gridstack to be static or not based on the edit mode
useEffect(() => {
gridRef.current?.setStatic(!isEditMode);
}, [isEditMode]);
const onChange = useCallback(
(changedNode: GridStackNode) => {
const id = changedNode.el?.getAttribute("data-id");
@@ -258,14 +244,40 @@ export const useGridstack = (section: Omit<Section, "items">, itemIds: string[])
};
}, [isEditMode, onAdd, onChange]);
/**
* IMPORTANT: This effect has to be placed after the effect to initialize the gridstack
* because we need the gridstack object to add the listeners
* Toggle the gridstack to be static or not based on the edit mode
*/
useEffect(() => {
gridRef.current?.setStatic(!isEditMode);
}, [isEditMode]);
const sectionHeight = section.kind === "dynamic" && "height" in section ? (section.height as number) : null;
// We want the amount of rows in a dynamic section to be the height of the section in the outer gridstack
/**
* IMPORTANT: This effect has to be placed after the effect to initialize the gridstack
* because we need the gridstack object to add the listeners
* We want the amount of rows in a dynamic section to be the height of the section in the outer gridstack
*/
useEffect(() => {
if (!sectionHeight) return;
gridRef.current?.row(sectionHeight);
}, [sectionHeight]);
/**
* IMPORTANT: This effect has to be placed after the effect to initialize the gridstack
* because we need the gridstack object to add the listeners
*/
useCssVariableConfiguration({
columnCount,
gridRef,
wrapperRef,
width,
height,
isDynamic: section.kind === "dynamic",
});
return {
refs: {
items: itemRefs,
@@ -1,9 +1,24 @@
import type { FocusEventHandler } from "react";
import { useState } from "react";
import { Combobox, Group, Image, InputBase, Skeleton, Text, useCombobox } from "@mantine/core";
import { startTransition, useState } from "react";
import {
Box,
Card,
Combobox,
Flex,
Image,
Indicator,
InputBase,
Paper,
Skeleton,
Stack,
Text,
UnstyledButton,
useCombobox,
} from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import { clientApi } from "@homarr/api/client";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { useScopedI18n } from "@homarr/translation/client";
interface IconPickerProps {
initialValue?: string;
@@ -18,55 +33,75 @@ export const IconPicker = ({ initialValue, onChange, error, onFocus, onBlur }: I
const [search, setSearch] = useState(initialValue ?? "");
const [previewUrl, setPreviewUrl] = useState<string | null>(initialValue ?? null);
const t = useI18n();
const tCommon = useScopedI18n("common");
const { data, isFetching } = clientApi.icon.findIcons.useQuery({
searchText: search,
const [debouncedSearch] = useDebouncedValue(search, 100);
// We use not useSuspenseQuery as it would cause an above Suspense boundary to trigger and so searching for something is bad UX.
const { data } = clientApi.icon.findIcons.useQuery({
searchText: debouncedSearch,
});
const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(),
});
const notNullableData = data?.icons ?? [];
const totalOptions = notNullableData.reduce((acc, group) => acc + group.icons.length, 0);
const groups = notNullableData.map((group) => {
const totalOptions = data?.icons.reduce((acc, group) => acc + group.icons.length, 0) ?? 0;
const groups = data?.icons.map((group) => {
const options = group.icons.map((item) => (
<Combobox.Option value={item.url} key={item.id}>
<Group>
<Image src={item.url} w={20} h={20} />
<Text>{item.name}</Text>
</Group>
</Combobox.Option>
<UnstyledButton
onClick={() => {
const value = item.url;
startTransition(() => {
setValue(value);
setPreviewUrl(value);
setSearch(value);
onChange(value);
combobox.closeDropdown();
});
}}
key={item.id}
>
<Indicator label="SVG" disabled={!item.url.endsWith(".svg")} size={16}>
<Card
p="sm"
pos="relative"
style={{
overflow: "visible",
cursor: "pointer",
}}
>
<Box w={25} h={25}>
<Image src={item.url} w={25} h={25} radius="md" />
</Box>
</Card>
</Indicator>
</UnstyledButton>
));
return (
<Combobox.Group label={group.slug} key={group.id}>
{options}
</Combobox.Group>
<Paper p="xs" key={group.slug} pt={2}>
<Text mb={8} size="sm" fw="bold">
{group.slug}
</Text>
<Flex gap={8} wrap={"wrap"}>
{options}
</Flex>
</Paper>
);
});
return (
<Combobox
onOptionSubmit={(value) => {
setValue(value);
setPreviewUrl(value);
setSearch(value);
onChange(value);
combobox.closeDropdown();
}}
store={combobox}
withinPortal
>
<Combobox store={combobox} withinPortal>
<Combobox.Target>
<InputBase
rightSection={<Combobox.Chevron />}
// eslint-disable-next-line @next/next/no-img-element
leftSection={previewUrl ? <img src={previewUrl} alt="" style={{ width: 20, height: 20 }} /> : null}
leftSection={
previewUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={previewUrl} alt="" style={{ width: 20, height: 20 }} />
) : null
}
value={search}
onChange={(event) => {
combobox.openDropdown();
@@ -91,18 +126,14 @@ export const IconPicker = ({ initialValue, onChange, error, onFocus, onBlur }: I
withAsterisk
error={error}
label={tCommon("iconPicker.label")}
placeholder={tCommon("iconPicker.header", { countIcons: data?.countIcons ?? 0 })}
/>
</Combobox.Target>
<Combobox.Dropdown>
<Combobox.Header>
<Text c="dimmed">{tCommon("iconPicker.header", { countIcons: data?.countIcons })}</Text>
</Combobox.Header>
<Combobox.Options mah={350} style={{ overflowY: "auto" }}>
{totalOptions > 0 ? (
groups
) : !isFetching ? (
<Combobox.Empty>{t("search.nothingFound")}</Combobox.Empty>
<Stack gap={4}>{groups}</Stack>
) : (
Array(15)
.fill(0)
@@ -0,0 +1,12 @@
"use client";
import { useChangeLocale, useCurrentLocale } from "@homarr/translation/client";
import { LanguageCombobox } from "./language-combobox";
export const CurrentLanguageCombobox = () => {
const currentLocale = useCurrentLocale();
const { changeLocale, isPending } = useChangeLocale();
return <LanguageCombobox value={currentLocale} onChange={changeLocale} isPending={isPending} />;
};
@@ -1,31 +1,35 @@
"use client";
import React from "react";
import { Combobox, Group, InputBase, Text, useCombobox } from "@mantine/core";
import { Combobox, Group, InputBase, Loader, ScrollArea, Text, useCombobox } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
import type { SupportedLanguage } from "@homarr/translation";
import { localeAttributes, supportedLanguages } from "@homarr/translation";
import { useChangeLocale, useCurrentLocale } from "@homarr/translation/client";
import { localeConfigurations, supportedLanguages } from "@homarr/translation";
import classes from "./language-combobox.module.css";
export const LanguageCombobox = () => {
interface LanguageComboboxProps {
label?: string;
value: SupportedLanguage;
onChange: (value: SupportedLanguage) => void;
isPending?: boolean;
}
export const LanguageCombobox = ({ label, value, onChange, isPending }: LanguageComboboxProps) => {
const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(),
});
const currentLocale = useCurrentLocale();
const changeLocale = useChangeLocale();
const handleOnOptionSubmit = React.useCallback(
(value: string) => {
if (!value) {
return;
}
changeLocale(value as SupportedLanguage);
onChange(value as SupportedLanguage);
combobox.closeDropdown();
},
[changeLocale, combobox],
[onChange, combobox],
);
const handleOnClick = React.useCallback(() => {
@@ -39,22 +43,26 @@ export const LanguageCombobox = () => {
component="button"
type="button"
pointer
label={label}
leftSection={isPending ? <Loader size={16} /> : null}
rightSection={<Combobox.Chevron />}
rightSectionPointerEvents="none"
onClick={handleOnClick}
variant="filled"
>
<OptionItem currentLocale={currentLocale} localeKey={currentLocale} />
<OptionItem currentLocale={value} localeKey={value} />
</InputBase>
</Combobox.Target>
<Combobox.Dropdown>
<Combobox.Options>
{supportedLanguages.map((languageKey) => (
<Combobox.Option value={languageKey} key={languageKey}>
<OptionItem currentLocale={currentLocale} localeKey={languageKey} showCheck />
</Combobox.Option>
))}
</Combobox.Options>
<ScrollArea h={300}>
<Combobox.Options>
{supportedLanguages.map((languageKey) => (
<Combobox.Option value={languageKey} key={languageKey}>
<OptionItem currentLocale={value} localeKey={languageKey} showCheck />
</Combobox.Option>
))}
</Combobox.Options>
</ScrollArea>
</Combobox.Dropdown>
</Combobox>
);
@@ -72,11 +80,11 @@ const OptionItem = ({
return (
<Group wrap="nowrap" justify="space-between">
<Group wrap="nowrap">
<span className={`fi fi-${localeAttributes[localeKey].flagIcon} ${classes.flagIcon}`}></span>
<span className={`fi fi-${localeConfigurations[localeKey].flagIcon} ${classes.flagIcon}`}></span>
<Group wrap="nowrap" gap="xs">
<Text>{localeAttributes[localeKey].name}</Text>
<Text>{localeConfigurations[localeKey].name}</Text>
<Text size="xs" c="dimmed" inherit>
({localeAttributes[localeKey].translatedName})
({localeConfigurations[localeKey].translatedName})
</Text>
</Group>
</Group>
@@ -1,11 +1,12 @@
import Script from "next/script";
import { UMAMI_WEBSITE_ID } from "@homarr/analytics";
import { api } from "@homarr/api/server";
import { db } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
export const Analytics = async () => {
// For static pages it will not find any analytics data so we do not include the script on them
const analytics = await api.serverSettings.getAnalytics().catch(() => null);
const analytics = await getServerSettingByKeyAsync(db, "analytics").catch(() => null);
if (analytics?.enableGeneral) {
return <Script src="https://umami.homarr.dev/script.js" data-website-id={UMAMI_WEBSITE_ID} defer />;
@@ -1,28 +1,28 @@
import SuperJSON from "superjson";
import { db, eq } from "@homarr/db";
import { serverSettings } from "@homarr/db/schema/sqlite";
import type { defaultServerSettings } from "@homarr/server-settings";
import { db } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
export const SearchEngineOptimization = async () => {
const crawlingAndIndexingSetting = await db.query.serverSettings.findFirst({
where: eq(serverSettings.settingKey, "crawlingAndIndexing"),
});
const crawlingAndIndexingSetting = await getServerSettingByKeyAsync(db, "crawlingAndIndexing");
if (!crawlingAndIndexingSetting) {
return null;
const robotsAttributes: string[] = [];
if (crawlingAndIndexingSetting.noIndex) {
robotsAttributes.push("noindex");
}
const value = SuperJSON.parse<(typeof defaultServerSettings)["crawlingAndIndexing"]>(
crawlingAndIndexingSetting.value,
);
if (crawlingAndIndexingSetting.noFollow) {
robotsAttributes.push("nofollow");
}
const robotsAttributes = [...(value.noIndex ? ["noindex"] : []), ...(value.noIndex ? ["nofollow"] : [])];
const googleAttributes: string[] = [];
const googleAttributes = [
...(value.noSiteLinksSearchBox ? ["nositelinkssearchbox"] : []),
...(value.noTranslate ? ["notranslate"] : []),
];
if (crawlingAndIndexingSetting.noSiteLinksSearchBox) {
googleAttributes.push("nositelinkssearchbox");
}
if (crawlingAndIndexingSetting.noTranslate) {
googleAttributes.push("notranslate");
}
return (
<>
@@ -15,7 +15,7 @@ export const DangerZoneRoot = async ({ children }: DangerZoneRootProps) => {
<Title c="red.8" order={2}>
{t("common.dangerZone")}
</Title>
<Card withBorder style={{ borderColor: "var(--mantine-color-red-8)" }}>
<Card withBorder style={{ borderColor: "var(--mantine-color-red-8)", borderWidth: 3 }}>
<Stack gap="sm">
{Array.isArray(children)
? children.map((child, index) => (
@@ -43,14 +43,14 @@ interface DangerZoneItemProps {
export const DangerZoneItem = ({ label, description, action }: DangerZoneItemProps) => {
return (
<Group justify="space-between" px="md">
<Group justify="space-between" px="md" w={"100%"}>
<Stack gap={0}>
<Text fw="bold" size="sm">
{label}
</Text>
<Text size="sm">{description}</Text>
</Stack>
<Group justify="end" w={{ base: "100%", xs: "auto" }}>
<Group justify="end" w={{ xs: "100%", sm: "100%", md: "auto" }}>
{action}
</Group>
</Group>
+26
View File
@@ -0,0 +1,26 @@
import { Anchor, Card, Stack, Text } from "@mantine/core";
import type { TablerIcon } from "@tabler/icons-react";
interface NoResultsProps {
icon: TablerIcon;
title: string;
action?: {
label: string;
href: string;
hidden?: boolean;
};
}
export const NoResults = ({ icon: Icon, title, action }: NoResultsProps) => {
return (
<Card withBorder bg="transparent">
<Stack align="center" gap="sm">
<Icon size="2rem" />
<Text fw={500} size="lg">
{title}
</Text>
{!action?.hidden && <Anchor href={action?.href}>{action?.label}</Anchor>}
</Stack>
</Card>
);
};
@@ -24,7 +24,7 @@ import { useScopedI18n } from "@homarr/translation/client";
import "flag-icons/css/flag-icons.min.css";
import { useAuthContext } from "~/app/[locale]/_client-providers/session";
import { LanguageCombobox } from "./language/language-combobox";
import { CurrentLanguageCombobox } from "./language/current-language-combobox";
interface UserAvatarMenuProps {
children: ReactNode;
@@ -61,7 +61,8 @@ export const UserAvatarMenu = ({ children }: UserAvatarMenuProps) => {
}, [logoutUrl, openModal, router]);
return (
<Menu width={300} withArrow withinPortal>
// We use keepMounted so we can add event listeners to prevent navigating away without saving the board
<Menu width={300} withArrow withinPortal keepMounted>
<Menu.Dropdown>
<Menu.Item onClick={toggleColorScheme} leftSection={<ColorSchemeIcon size="1rem" />}>
{colorSchemeText}
@@ -72,7 +73,7 @@ export const UserAvatarMenu = ({ children }: UserAvatarMenuProps) => {
<Menu.Divider />
<Menu.Item p={0} closeMenuOnClick={false}>
<LanguageCombobox />
<CurrentLanguageCombobox />
</Menu.Item>
<Menu.Divider />
{Boolean(session.data) && (
@@ -0,0 +1,23 @@
import "server-only";
import { notFound, redirect } from "next/navigation";
import { TRPCError } from "@trpc/server";
import { logger } from "@homarr/log";
export const catchTrpcNotFound = (err: unknown) => {
if (err instanceof TRPCError && err.code === "NOT_FOUND") {
notFound();
}
throw err;
};
export const catchTrpcUnauthorized = (err: unknown) => {
if (err instanceof TRPCError && err.code === "UNAUTHORIZED") {
logger.info("Somebody tried to access a protected route without being authenticated, redirecting to login page");
redirect("/auth/login");
}
throw err;
};
-12
View File
@@ -1,12 +0,0 @@
import "server-only";
import { notFound } from "next/navigation";
import { TRPCError } from "@trpc/server";
export const catchTrpcNotFound = (err: unknown) => {
if (err instanceof TRPCError && err.code === "NOT_FOUND") {
notFound();
}
throw err;
};
+24 -3
View File
@@ -1,11 +1,32 @@
import type { NextRequest } from "next/server";
import { createTRPCClient, httpLink } from "@trpc/client";
import SuperJSON from "superjson";
import { I18nMiddleware } from "@homarr/translation/middleware";
import type { AppRouter } from "@homarr/api";
import { createHeadersCallbackForSource } from "@homarr/api/client";
import { createI18nMiddleware } from "@homarr/translation/middleware";
export function middleware(request: NextRequest) {
return I18nMiddleware(request);
export async function middleware(request: NextRequest) {
// fetch api does not work because window is not defined and we need to construct the url from the headers
// In next 15 we will be able to use node apis and such the db directly
const culture = await serverFetchApi.serverSettings.getCulture.query();
// We don't want to fallback to accept-language header so we clear it
request.headers.set("accept-language", "");
const next = createI18nMiddleware(culture.defaultLocale);
return next(request);
}
export const config = {
matcher: ["/((?!api|static|.*\\..*|_next|favicon.ico|robots.txt).*)"],
};
export const serverFetchApi = createTRPCClient<AppRouter>({
links: [
httpLink({
url: `http://${process.env.HOSTNAME ?? "localhost"}:3000/api/trpc`,
transformer: SuperJSON,
headers: createHeadersCallbackForSource("server-fetch"),
}),
],
});
+18
View File
@@ -0,0 +1,18 @@
import { cache } from "react";
import { cookies } from "next/headers";
import { db } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import type { ColorScheme } from "@homarr/definitions";
import { colorSchemeCookieKey } from "@homarr/definitions";
export const getCurrentColorSchemeAsync = cache(async () => {
const cookieValue = cookies().get(colorSchemeCookieKey)?.value;
if (cookieValue) {
return cookieValue as ColorScheme;
}
const appearanceSettings = await getServerSettingByKeyAsync(db, "appearance");
return appearanceSettings.defaultColorScheme;
});
+1 -1
View File
@@ -29,7 +29,7 @@ export const getPackageAttributesAsync = async () => {
};
};
type PackageJsonDependencies = { [key in string]: string };
type PackageJsonDependencies = Record<string, string>;
interface PackageJson {
dependencies: PackageJsonDependencies | undefined;
}
+9 -9
View File
@@ -10,7 +10,7 @@
"main": "./src/main.ts",
"types": "./src/main.ts",
"scripts": {
"build": "esbuild src/main.ts --bundle --platform=node --outfile=tasks.cjs",
"build": "esbuild src/main.ts --bundle --platform=node --loader:.scss=text --external:@opentelemetry/api --outfile=tasks.cjs",
"clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts",
"format": "prettier --check . --ignore-path ../../.gitignore",
@@ -36,19 +36,19 @@
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"dayjs": "^1.11.13",
"dotenv": "^16.4.5",
"dotenv": "^16.4.7",
"superjson": "2.2.1",
"undici": "6.20.1"
"undici": "7.1.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^20.17.1",
"dotenv-cli": "^7.4.2",
"eslint": "^9.13.0",
"prettier": "^3.3.3",
"tsx": "4.19.1",
"typescript": "^5.6.3"
"@types/node": "^22.10.1",
"dotenv-cli": "^7.4.4",
"eslint": "^9.16.0",
"prettier": "^3.4.2",
"tsx": "4.19.2",
"typescript": "^5.7.2"
}
}
@@ -0,0 +1,99 @@
import type { Dispatcher } from "undici";
import { describe, expect, test, vi } from "vitest";
import { logger } from "@homarr/log";
import { LoggingAgent } from "~/undici-log-agent-override";
vi.mock("undici", () => {
return {
Agent: class Agent {
dispatch(_options: Dispatcher.DispatchOptions, _handler: Dispatcher.DispatchHandler): boolean {
return true;
}
},
setGlobalDispatcher: () => undefined,
};
});
const REDACTED = "REDACTED";
describe("LoggingAgent should log all requests", () => {
test("should log all requests", () => {
// Arrange
const infoLogSpy = vi.spyOn(logger, "info");
const agent = new LoggingAgent();
// Act
agent.dispatch({ origin: "https://homarr.dev", path: "/", method: "GET" }, {});
// Assert
expect(infoLogSpy).toHaveBeenCalledWith("Dispatching request https://homarr.dev/ (0 headers)");
});
test("should show amount of headers", () => {
// Arrange
const infoLogSpy = vi.spyOn(logger, "info");
const agent = new LoggingAgent();
// Act
agent.dispatch(
{
origin: "https://homarr.dev",
path: "/",
method: "GET",
headers: {
"Content-Type": "text/html",
"User-Agent": "Mozilla/5.0",
},
},
{},
);
// Assert
expect(infoLogSpy).toHaveBeenCalledWith(expect.stringContaining("(2 headers)"));
});
test.each([
["/?hex=a3815e8ada2ef9a31", `/?hex=${REDACTED}`],
["/?uuid=f7c3f65e-c511-4f90-ba9a-3fd31418bd49", `/?uuid=${REDACTED}`],
["/?password=complexPassword123", `/?password=${REDACTED}`],
[
// JWT for John Doe
"/?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
`/?jwt=${REDACTED}`,
],
["/?one=a1&two=b2&three=c3", `/?one=${REDACTED}&two=${REDACTED}&three=${REDACTED}`],
["/?numberWith13Chars=1234567890123", `/?numberWith13Chars=${REDACTED}`],
[`/?stringWith13Chars=${"a".repeat(13)}`, `/?stringWith13Chars=${REDACTED}`],
])("should redact sensitive data in url https://homarr.dev%s", (path, expected) => {
// Arrange
const infoLogSpy = vi.spyOn(logger, "info");
const agent = new LoggingAgent();
// Act
agent.dispatch({ origin: "https://homarr.dev", path, method: "GET" }, {});
// Assert
expect(infoLogSpy).toHaveBeenCalledWith(expect.stringContaining(` https://homarr.dev${expected} `));
});
test.each([
["empty", "/?empty"],
["numbers with max 12 chars", "/?number=123456789012"],
["true", "/?true=true"],
["false", "/?false=false"],
["strings with max 12 chars", `/?short=${"a".repeat(12)}`],
["dates", "/?date=2022-01-01"],
["date times", "/?datetime=2022-01-01T00:00:00.000Z"],
])("should not redact values that are %s", (_reason, path) => {
// Arrange
const infoLogSpy = vi.spyOn(logger, "info");
const agent = new LoggingAgent();
// Act
agent.dispatch({ origin: "https://homarr.dev", path, method: "GET" }, {});
// Assert
expect(infoLogSpy).toHaveBeenCalledWith(expect.stringContaining(` https://homarr.dev${path} `));
});
});
+6 -4
View File
@@ -3,27 +3,29 @@ import { Agent, setGlobalDispatcher } from "undici";
import { logger } from "@homarr/log";
class LoggingAgent extends Agent {
export class LoggingAgent extends Agent {
constructor(...props: ConstructorParameters<typeof Agent>) {
super(...props);
}
dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean {
dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean {
const url = new URL(`${options.origin as string}${options.path}`);
// The below code should prevent sensitive data from being logged as
// some integrations use query parameters for auth
url.searchParams.forEach((value, key) => {
if (value === "") return; // Skip empty values
if (/^\d{1,12}$/.test(value)) return; // Skip small numbers
if (/^-?\d{1,12}$/.test(value)) return; // Skip small numbers
if (value === "true" || value === "false") return; // Skip boolean values
if (/^[a-zA-Z]{1,12}$/.test(value)) return; // Skip short strings
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return; // Skip dates
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) return; // Skip date times
url.searchParams.set(key, "REDACTED");
});
logger.info(
`Dispatching request ${url.toString().replaceAll("=&", "&")} (${Object.keys(options.headers as object).length} headers)`,
`Dispatching request ${url.toString().replaceAll("=&", "&")} (${Object.keys(options.headers ?? {}).length} headers)`,
);
return super.dispatch(options, handler);
}
+7 -7
View File
@@ -7,7 +7,7 @@
"main": "./src/main.ts",
"types": "./src/main.ts",
"scripts": {
"build": "esbuild src/main.ts --bundle --platform=node --outfile=wssServer.cjs --external:bcrypt --external:cpu-features --loader:.html=text --loader:.node=text",
"build": "esbuild src/main.ts --bundle --platform=node --outfile=wssServer.cjs --external:bcrypt --external:@opentelemetry/api --external:cpu-features --loader:.html=text --loader:.scss=text --loader:.node=text",
"clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts",
"format": "prettier --check . --ignore-path ../../.gitignore",
@@ -25,17 +25,17 @@
"@homarr/log": "workspace:^",
"@homarr/redis": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"dotenv": "^16.4.5",
"tsx": "4.19.1",
"dotenv": "^16.4.7",
"tsx": "4.19.2",
"ws": "^8.18.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.5.12",
"eslint": "^9.13.0",
"prettier": "^3.3.3",
"typescript": "^5.6.3"
"@types/ws": "^8.5.13",
"eslint": "^9.16.0",
"prettier": "^3.4.2",
"typescript": "^5.7.2"
}
}
+10
View File
@@ -0,0 +1,10 @@
files:
- source: /packages/translation/src/lang/en.json
translation: /packages/translation/src/lang/%two_letters_code%.json
# Title of pull request and so the commit that will be used for squashed merge commit
pull_request_title: "chore(lang): updated translations from crowdin"
# Custom commit message that is not only appended
commit_message: "chore(lang): update translations %original_file_name% from crowdin [skip ci]"
append_commit_message: false
Loaded 100 of 399 files, more files were not shown because too many files have changed in this diff. Show more