Compare commits

..
249 Commits
Author SHA1 Message Date
Releases Homarr d0a20dbfc5 chore(release): 1.0.0 [skip ci] 2025-01-17 12:47:30 +00:00
homarr-releases[bot] 68329965f7 chore(release): automatic release v1.0.0 2025-01-17 12:46:22 +00:00
Meier Lukas f9fea70d39 chore: change documentation base url to homarr.dev (#1826) 2025-01-17 13:45:18 +01:00
Meier Lukas 3ed46aecbd feat(widget): add proxmox integration (#1969)
* feat(widget): add proxmox integration

* fix: broken lock file

* fix: ci issues

* fix: ci issues

* fix: ci issues

* chore: debug temporary

* fix: name is not used correctly for nodes and storage in proxmox

* fix: remove temporary debu logs

* fix: job runs for both cluster and system health and throws error

* fix: ts-expect-error is unnecessary

* fix: remove unused import
2025-01-17 13:01:04 +01:00
Meier Lukas a31c6a97e0 ci: change weekly release base to main branch (#1976) 2025-01-17 12:59:02 +01:00
homarr-crowdin[bot]andMeier Lukas d1be727eb3 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-01-17 11:54:32 +00:00
Meier Lukas a1084f91e5 fix: mysql transactions do not work with run property of sqlite (#1974)
* fix: mysql transactions do not work with run property of sqlite

* fix: ci issues
2025-01-17 12:53:01 +01:00
Meier Lukas 7622f0a4d2 fix: websockets not working extend not defined (#1975)
* fix: websockets not working extend not defined

* fix: revert dockerode to 4.0.2
2025-01-17 12:52:42 +01:00
homarr-renovate[bot] 34cacb3153 fix(deps): update dependency flag-icons to ^7.3.1 (#1970)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-17 02:57:17 +00:00
homarr-renovate[bot] 48cf5d5304 chore(deps): update dependency prettier-plugin-packagejson to ^2.5.8 (#1973)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-17 02:14:40 +00:00
Meier Lukas 8c36c3e36b feat(certificates): handle self signed certificates (#1951)
* wip: add page and loading of certificates in folder

* wip: add certificate addition and removal

* feat: add removal ui for certificates

* feat: migrate integrations to fetch or agent with trusted certificates

* fix: lock file issues

* fix: typecheck issue

* fix: inconsistent package versions

* chore: address pull request feedback

* fix: add missing navigation item and restrict access to page

* chore: address pull request feedback

* fix: inconsistent undici dependency version

* fix: inconsistent undici dependency version
2025-01-17 00:08:40 +01:00
homarr-renovate[bot] b10b2013af fix(deps): update dependency drizzle-orm to ^0.38.4 (#1967)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-16 15:33:39 +00:00
homarr-renovate[bot] ea8acc83ea fix(deps): update dependency flag-icons to ^7.3.0 (#1966)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-16 14:44:14 +00:00
homarr-renovate[bot] cb96508634 fix(deps): update dependency drizzle-kit to ^0.30.2 (#1965)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-16 14:34:48 +00:00
homarr-renovate[bot] 5e83847772 fix(deps): update dependency undici to v7.2.3 (#1963)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-16 09:32:31 +00:00
homarr-renovate[bot] 1557537763 chore(deps): update dependency @types/node to ^22.10.7 (#1962)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-16 00:54:29 +00:00
homarr-crowdin[bot] 5f993d2630 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-16 00:43:40 +00:00
homarr-renovate[bot] 44767c46c2 fix(deps): update dependency flag-icons to ^7.2.4 (#1960)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-15 22:53:09 +00:00
homarr-renovate[bot] aaddf0d987 fix(deps): update dependency undici to v7.2.2 (#1957)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-15 17:41:33 +00:00
homarr-crowdin[bot] 7f8e6e4ea1 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-15 16:13:54 +00:00
homarr-renovate[bot] 34a76c98e8 fix(deps): update dependency better-sqlite3 to ^11.8.0 (#1956)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-15 03:17:53 +00:00
homarr-renovate[bot] 0f82a6f63d chore(deps): update dependency prettier-plugin-packagejson to ^2.5.7 (#1954)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-14 20:42:28 +00:00
Meier Lukas d4bb014a9b fix(category): remove not always working and ignoring dynamic sections (#1939) 2025-01-14 19:55:21 +01:00
Meier Lukas e01d74f4f8 feat(board): add mobile home board (#1910)
* feat(board): add mobile home board

* fix: add missing translations

* fix: mysql key reference with other datatype

* fix: format issue

* fix: missing trpc context arguments in tests

* fix: missing trpc context arguments in tests
2025-01-14 19:54:55 +01:00
Meier Lukas ec3bda34e0 fix(media-request): media and media-request status can have missing values (#1911) 2025-01-14 19:45:21 +01:00
Meier Lukas 5c16fb8d6c chore: add start command to run from source (#1928)
* chore: add start command to run from source

* fix: start command does not run in parallel
2025-01-14 19:44:37 +01:00
Meier Lukas 986e013ca3 fix(app): missing noreferer attribute for anchor tag (#1915) 2025-01-14 19:31:50 +01:00
Meier Lukas 1fd3fd8dfb refactor: env validation typescript and common package (#1912) 2025-01-14 19:03:38 +01:00
homarr-renovate[bot] a03a01b964 fix(deps): update mantine monorepo to ^7.16.0 (#1948)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-14 17:53:17 +00:00
Meier Lukas 4d94fce523 test: adjust nzbget test to use append instead of scan (#1952) 2025-01-14 18:39:12 +01:00
homarr-renovate[bot] 75c791801e fix(deps): update dependency eslint-config-prettier to v10 (#1937)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-14 17:48:54 +01:00
homarr-renovate[bot] 3e67b411b6 chore(deps): update dependency @types/react to v19.0.7 (#1947)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-14 09:16:54 +00:00
homarr-renovate[bot] ff1a003037 fix(deps): update dependency sass to ^1.83.4 (#1946)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-14 06:41:43 +00:00
homarr-renovate[bot] 99c69ba65d fix(deps): update dependency sass to ^1.83.3 (#1945)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-14 05:49:13 +00:00
homarr-crowdin[bot] f5cf92c49c chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-14 00:44:35 +00:00
homarr-renovate[bot] 05dc51b38e chore(deps): update pnpm to v9.15.4 (#1943)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-13 21:24:22 +00:00
homarr-renovate[bot] a53e0f7cee fix(deps): update tanstack-query monorepo to ^5.64.1 (#1935)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-13 19:27:22 +00:00
homarr-renovate[bot] f3a91ca37f chore(deps): update dependency @types/node to ^22.10.6 (#1942)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-13 19:15:16 +00:00
homarr-renovate[bot] 1e406a8a58 fix(deps): update dependency @ianvs/prettier-plugin-sort-imports to ^4.4.1 (#1936)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-13 18:49:04 +00:00
homarr-renovate[bot] 2ea763c231 fix(deps): update dependency eslint-plugin-react to ^7.37.4 (#1934)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-13 18:46:24 +00:00
homarr-renovate[bot] 579bb2a33e chore(deps): update dependency @types/react to v19.0.6 (#1933)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-13 18:45:09 +00:00
homarr-renovate[bot] feecdd36d4 fix(deps): update dependency typescript-eslint to ^8.20.0 (#1938)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-13 18:24:00 +00:00
Meier Lukas 56755d1ab4 fix: trpc removed export anything from client in react-query (#1941) 2025-01-13 18:50:51 +01:00
homarr-crowdin[bot] 6b36ec3103 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-13 00:47:42 +00:00
homarr-renovate[bot] fc6dbc92df chore(deps): update react monorepo (#1927)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-12 10:45:02 +00:00
homarr-crowdin[bot] 467d5e21ae chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-12 00:49:05 +00:00
homarr-renovate[bot] b399cb6979 fix(deps): update tanstack-query monorepo to ^5.64.0 (#1922)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-11 19:13:10 +00:00
Meier Lukas 4d69b81676 ci: add missing git config to release pipeline 2025-01-11 11:13:48 +01:00
homarr-crowdin[bot] a1fc9902a5 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-11 10:06:50 +00:00
Meier Lukas 0649220080 Merge branch 'beta' into dev 2025-01-11 10:56:33 +01:00
homarr-renovate[bot] e502fbc533 chore(deps): update dependency eslint to ^9.18.0 (#1916)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-10 21:54:40 +00:00
Releases Homarr f8c3dab7f9 chore(release): 1.0.0-beta.12 [skip ci] 2025-01-10 19:13:50 +00:00
homarr-releases[bot] 039800d94d chore(release): automatic release v1.0.0 2025-01-10 19:12:41 +00:00
homarr-renovate[bot] 9a7e421f68 fix(deps): update tiptap monorepo to v2.11.2 (#1913)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-10 18:42:40 +00:00
Meier Lukas e695fb64d3 fix(widget): set-options from widget components should be partial (#1882) 2025-01-10 14:47:19 +01:00
Meier Lukas 62da953356 feat(items): add search to selection (#1887) 2025-01-10 14:46:39 +01:00
Meier Lukas 39171ac76a refactor(icons): move walkxcode/dashboard-icons to homarr-labs/dashboard-icons (#1883) 2025-01-10 14:46:31 +01:00
Meier Lukas 546c824888 fix(dns-hole-summary): calculation for domains and percentage for multiple integrations (#1894) 2025-01-10 14:46:21 +01:00
Meier Lukas a12dd10269 fix(security): restrict link protocols to http and https (#1888) 2025-01-10 14:45:30 +01:00
Meier Lukas 80c02ef9f2 fix(translation): query parameter in search with escaped (#1895) 2025-01-10 14:44:31 +01:00
homarr-renovate[bot] 7d1b1d8342 fix(deps): update dependency glob to ^11.0.1 (#1908)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-10 11:29:01 +00:00
homarr-renovate[bot] dc3d27e1a1 fix(deps): update tiptap monorepo to v2.11.1 (#1909)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-10 11:15:46 +00:00
homarr-renovate[bot] ec8ad483ca chore(deps): update dependency jsdom to v26 (#1903)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-09 23:59:35 +01:00
homarr-renovate[bot] 8edf922e30 fix(deps): update dependency trpc-to-openapi to ^2.1.2 (#1900)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-09 15:43:55 +00:00
homarr-renovate[bot] 86a925939c fix(deps): update dependency mantine-react-table to v2.0.0-beta.8 (#1902)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-09 15:34:35 +00:00
homarr-renovate[bot] b034dcbb11 fix(deps): update dependency undici to v7.2.1 (#1904)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-09 15:16:57 +00:00
homarr-crowdin[bot] 93d7d99e70 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-09 00:44:53 +00:00
homarr-renovate[bot] bfe5e9a8e1 chore(deps): update dependency @types/react to v19.0.4 (#1899)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-08 20:18:06 +00:00
homarr-renovate[bot] e056f620da chore(deps): update dependency typescript to ^5.7.3 (#1897)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-08 19:14:44 +00:00
homarr-renovate[bot] 3860af31b6 fix(deps): update dependency octokit to ^4.1.0 (#1898)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-08 19:14:26 +00:00
homarr-renovate[bot] a69c64d88d fix(deps): update dependency ldapts to v7.3.1 (#1896)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-08 19:12:13 +00:00
homarr-renovate[bot] 34a6c65ead chore(deps): update node.js to v22.13.0 (#1890)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-08 16:06:01 +01:00
homarr-renovate[bot] e33e0a4d2b fix(deps): update tanstack-query monorepo to ^5.63.0 (#1893)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-08 14:36:02 +00:00
homarr-renovate[bot] ffdc8d3496 fix(deps): update dependency @tabler/icons-react to ^3.28.1 (#1891)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-08 11:15:24 +00:00
homarr-crowdin[bot] 64cf3d02c4 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-08 00:44:58 +00:00
homarr-renovate[bot] f4f02ca539 fix(deps): update nextjs monorepo to ^15.1.4 (#1885)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-07 20:54:33 +00:00
homarr-renovate[bot] 6327ca073c chore(deps): update node.js to v22.13.0 (#1884)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-07 19:53:55 +00:00
homarr-renovate[bot] 7807b28d24 fix(deps): update mantine monorepo to ^7.15.3 (#1880)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-07 06:22:05 +00:00
homarr-crowdin[bot] 02386ffcd8 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-07 05:32:34 +00:00
homarr-renovate[bot] 690cc303bf chore(deps): update dependency @types/dockerode to ^3.3.34 (#1879)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-07 00:56:21 +00:00
homarr-renovate[bot] 9da50e518b chore(deps): update dependency @types/chroma-js to v3 (#1876)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-06 22:05:56 +01:00
homarr-renovate[bot] eb424d757b fix(deps): update tanstack-query monorepo to ^5.62.16 (#1877)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-06 19:27:55 +00:00
Meier Lukas 65befa22ba feat(spotlight): add default search engine (#1807) 2025-01-06 19:59:40 +01:00
Meier Lukas 6a68ccfee4 chore(config): change next-config from mjs to typescript (#1861) 2025-01-06 19:59:25 +01:00
homarr-renovate[bot] e46b1c49f9 fix(deps): update dependency @tabler/icons-react to ^3.27.1 (#1875)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-06 18:55:52 +00:00
Manuel 64cc078b09 feat: docker add to homarr (#1825) 2025-01-06 19:37:18 +01:00
homarr-renovate[bot] be4e75321a fix(deps): update dependency typescript-eslint to ^8.19.1 (#1874)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-06 18:32:37 +00:00
homarr-renovate[bot] e0eab425e6 chore(deps): update dependency @types/react to v19.0.3 (#1872)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-06 17:00:21 +00:00
homarr-renovate[bot] 2dec74e5ff chore(deps): update pnpm to v9.15.3 (#1871)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-06 16:54:37 +00:00
homarr-crowdin[bot] 39ea2baff3 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-06 00:46:45 +00:00
homarr-crowdin[bot] a160f16772 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-05 00:48:20 +00:00
Meier Lukas ccb19e06c1 feat(app): add search and pagination (#1860) 2025-01-04 23:06:34 +01:00
Meier Lukas bf12944768 fix(iframe): remove second scrollbar from item card (#1828) 2025-01-04 22:47:33 +01:00
Meier Lukas 1e9098d052 feat(icons): add upload button to icon picker (#1859) 2025-01-04 22:43:30 +01:00
Meier Lukas 1562eae4d7 refactor: remove sharp image optimization (#1862) 2025-01-04 22:38:40 +01:00
Meier Lukas c349bf8371 feat(auth): add env variable for oidc-name-attribute-overwrite (#1850) 2025-01-04 21:49:33 +01:00
homarr-renovate[bot] 1f0116d725 fix(deps): update dependency better-sqlite3 to ^11.7.2 (#1858)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-04 20:16:31 +00:00
Meier LukasandManuel 49d10f7ad0 feat(board): add board duplication (#1856)
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
2025-01-04 18:53:57 +00:00
Meier Lukas d98552540a fix(deps): update nextjs monorepo to v15 (major) (#1844) 2025-01-04 19:47:23 +01:00
homarr-renovate[bot] 92f4f9421e fix(deps): update dependency dockerode to ^4.0.3 (#1854)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-04 12:59:23 +00:00
homarr-renovate[bot] 53f07f87b8 fix(deps): update tanstack-query monorepo to ^5.62.15 (#1853)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-04 11:39:29 +00:00
homarr-renovate[bot] 812469db82 fix(deps): update dependency sass to ^1.83.1 (#1852)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-04 02:16:26 +00:00
homarr-crowdin[bot] e2b211cf34 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-04 00:43:27 +00:00
homarr-renovate[bot] 2358f6f5eb fix(deps): update tanstack-query monorepo to ^5.62.14 (#1846)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-03 20:17:03 +00:00
Releases Homarr 024ec9235d chore(release): 1.0.0-beta.11 [skip ci] 2025-01-03 19:13:36 +00:00
homarr-releases[bot] 58fdea0245 chore(release): automatic release v1.0.0 2025-01-03 19:12:38 +00:00
Meier Lukas 4ead238462 test(e2e): add for onboarding and lldap authorization (#1834)
* test(e2e): add for onboarding and lldap authorization

* ci: add playwright chrome installation to e2e test

* fix(e2e): timeout between lldap login redirect to short

* test(e2e): add oidc azure test

* fix(e2e): lldap test fails

* wip: add temporary error log for failed ldap server connection

* fix(e2e): github actions don't support host.docker.internal

* chore: address pull request feedback

* refactor(e2e): move onboarding steps to onboarding actions and assertions

* fix(e2e): increase timeout for navigating back from azure login

* fix: wait for url network changed error

* fix: revert to wait for url

* fix(e2e): remove oidc test

* refactor(e2e): remove env validation

* ci: remove azure oidc env variables
2025-01-03 16:49:30 +01:00
Meier Lukas 6305c74f90 feat: remove import beta badge (#1829) 2025-01-03 16:35:43 +01:00
homarr-renovate[bot] 0780080681 chore(deps): update dependency @types/node to ^22.10.5 (#1842)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-03 15:33:47 +00:00
homarr-crowdin[bot] aa15a705d7 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-03 15:33:38 +00:00
homarr-renovate[bot] cc70175125 chore(deps): update semantic-release monorepo (#1841)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-03 15:28:36 +00:00
homarr-renovate[bot] bc5e25e9cd fix(deps): update tanstack-query monorepo to ^5.62.12 (#1843)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-03 15:16:35 +00:00
homarr-renovate[bot] d43557e165 chore(deps): update dependency @types/dockerode to ^3.3.33 (#1839)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-02 21:39:38 +00:00
homarr-renovate[bot] b6c96f73a5 chore(deps): update dependency @types/node to ^22.10.4 (#1838)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-02 18:55:31 +00:00
Yossi Hillali ee8831affe fix: prowlarr integration indexers status changed from id to indexerId (#1837) 2025-01-02 20:53:27 +02:00
Meier Lukas 6e21863d04 fix(icons): picker does not allow multiple words (#1827) 2025-01-02 14:52:38 +01:00
homarr-crowdin[bot] ca39ca66f0 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-02 00:43:54 +00:00
Yossi Hillali cc7d53e84e fix: login page in light mode is always black (#1821) 2025-01-01 12:24:53 +02:00
homarr-renovate[bot] 99c409cbb7 chore(deps): update dependency @types/node to ^22.10.3 (#1824)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-01-01 02:26:11 +00:00
homarr-crowdin[bot] 1fcf9d0146 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-01-01 00:48:53 +00:00
homarr-renovate[bot] a234fc844e fix(deps): update dependency zod-form-data to ^2.0.5 (#1822)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-31 17:14:34 +00:00
Releases Homarr 168b587c18 chore(release): 1.0.0-beta.10 [skip ci] 2024-12-31 10:42:11 +00:00
homarr-releases[bot] dc12a82805 chore(release): automatic release v1.0.0 2024-12-31 10:41:19 +00:00
ManuelandMeier Lukas f5076454cd feat(integration): add search engine creation (#1816)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-31 11:40:37 +01:00
Meier Lukas aeb681a858 feat(docker): add support for pgid and puid env variables (#1759) 2024-12-31 11:36:28 +01:00
Meier Lukas 56b57ad171 fix(docker): replace anonymous docker volume with env variable for encrypting secrets (#1809) 2024-12-31 11:30:29 +01:00
Manuel 91e1cef611 feat: add request media (#1811) 2024-12-31 11:05:30 +01:00
homarr-renovate[bot] e53b06e8c2 chore(deps): update dependency concurrently to ^9.1.2 (#1818)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-31 08:46:41 +00:00
Meier Lukas 0ebf4bc55e feat(widget): add minecraft server status widget (#1801) 2024-12-31 09:45:48 +01:00
homarr-crowdin[bot] b8a155dd08 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-31 00:43:59 +00:00
Thomas CamlongandMeier Lukas 63786eed06 chore(lang): updated translations from crowdin (#1803)
* New translations en.json (Chinese Simplified)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Croatian)

* New translations en.json (Estonian)

* New translations en.json (Latvian)

* New translations en.json (Cree)

* New translations en.json (Hebrew)

* New translations en.json (Hebrew)

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-30 21:44:23 +01:00
homarr-renovate[bot] 598f0047eb fix(deps): update dependency typescript-eslint to ^8.19.0 (#1814)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-30 17:33:38 +00:00
homarr-renovate[bot] eacda4c397 fix(deps): update tiptap monorepo to v2.11.0 (#1813)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-30 16:54:00 +00:00
Meier Lukas 5cb726f793 ci(docker): always push image (#1812) 2024-12-30 17:38:27 +01:00
homarr-crowdin[bot] e8d29420e0 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-30 16:28:16 +00:00
Yossi Hillali 23c7d0bbf0 fix(media-server): redesign widget (#1775)
* fix: redisign media server widget

* fix: reviewed changes

* fix: reviewed chenges

* fix: add icon title

* fix: text resize
2024-12-29 19:24:38 +02:00
homarr-renovate[bot] 75aefcb0d5 fix(deps): update dependency octokit to ^4.0.3 (#1806)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-29 16:16:34 +00:00
homarr-crowdin[bot] b52a8a7935 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-29 08:27:17 +00:00
homarr-renovate[bot] 6d90209e00 chore(deps): update pnpm to v9.15.2 (#1804)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-29 08:26:48 +00:00
Thomas CamlongandMeier Lukas 8f0945f51a chore(lang): updated translations from crowdin (#1800)
* New translations en.json (German)

* New translations en.json (Hebrew)

* New translations en.json (Hebrew)

* New translations en.json (Turkish)

* New translations en.json (Chinese Simplified)

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-28 18:50:25 +01:00
homarr-crowdin[bot] 5b04b93f23 chore(lang): updated translations from crowdin (#1802)
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-28 18:49:22 +01:00
Thomas CamlongandMeier Lukas c8228b920c chore(lang): updated translations from crowdin (#1797)
* New translations en.json (Hebrew)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-28 10:25:14 +01:00
homarr-crowdin[bot] abf7942fb7 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-28 09:14:42 +00:00
homarr-crowdin[bot] 50746eb3e4 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-28 09:06:24 +00:00
Thomas CamlongandMeier Lukas 76abfca11b chore(lang): updated translations from crowdin (#1786)
* New translations en.json (Hebrew)

* New translations en.json (Dutch)

* New translations en.json (Hebrew)

* New translations en.json (Hebrew)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Cree)

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-27 20:47:14 +01:00
homarr-crowdin[bot] fcab6a347d chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-27 19:43:44 +00:00
Yossi Hillali c91aae53ac fix(health-monitoring): add months to uptime, fix status update (#1792) 2024-12-27 21:32:04 +02:00
homarr-renovate[bot]andMeier Lukas 4943e7b851 fix(deps): update dependency trpc-to-openapi to ^2.1.1 (#1788)
* fix(deps): update dependency trpc-to-openapi to ^2.1.1

* fix(deps): missing dependency lodash.clonedeep in api package

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-27 20:31:15 +01:00
Releases Homarr 88c423fca6 chore(release): 1.0.0-beta.9 [skip ci] 2024-12-27 19:13:01 +00:00
homarr-releases[bot] f238663ec7 chore(release): automatic release v1.0.0 2024-12-27 19:12:07 +00:00
homarr-renovate[bot] e9870e7ee7 fix(deps): update tanstack-query monorepo to ^5.62.11 (#1791)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-27 13:22:52 +00:00
homarr-renovate[bot] 9c0a0ced04 chore(deps): update dependency concurrently to ^9.1.1 (#1790)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-27 11:53:46 +00:00
homarr-crowdin[bot] 4c27635066 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-27 00:44:11 +00:00
homarr-renovate[bot] 3d66ff9148 fix(deps): update nextjs monorepo to ^14.2.22 (#1787)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-26 18:20:53 +00:00
Meier Lukas ec257e5f60 chore(env): remove unused vercel-url env variable (#1779)
* chore(env): remove unused vercel-url env variable

* chore: remove vercel-json file
2024-12-26 08:58:27 +01:00
Meier Lukas ef24370a8c refactor: revert assignment of oldmarr widget mapping (#1780) 2024-12-26 08:58:06 +01:00
Releases Homarr 1fa0fc50c5 chore(release): 1.0.0-beta.8 [skip ci] 2024-12-25 14:19:38 +00:00
homarr-releases[bot] e9a1dae857 chore(release): automatic release v1.0.0 2024-12-25 14:18:45 +00:00
Meier Lukas e37279fe80 fix: oldmarr import user casing wrong (#1784) 2024-12-25 15:17:51 +01:00
homarr-renovate[bot] 82c6ce7b77 fix(deps): update dependency drizzle-orm to ^0.38.3 (#1783)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-25 12:25:14 +00:00
homarr-renovate[bot] 555212e2dc fix(deps): update dependency drizzle-zod to ^0.6.1 (#1782)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-25 10:53:06 +00:00
homarr-crowdin[bot] c831539772 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-25 00:43:00 +00:00
homarr-renovate[bot] 3e2ffbd7bf fix(deps): update dependency @ctrl/transmission to ^7.2.0 (#1777)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-24 20:33:01 +00:00
Thomas Camlong 22d7f676eb chore(lang): updated translations from crowdin (#1774)
* New translations en.json (Hebrew)

* New translations en.json (Hebrew)
2024-12-24 21:26:29 +01:00
homarr-crowdin[bot] 23bf262f4e chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-24 20:26:03 +00:00
homarr-renovate[bot] ecae80d741 fix(deps): update dependency @ctrl/deluge to ^7.1.0 (#1776)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-24 20:16:58 +00:00
Releases Homarr 162e8adf4c chore(release): 1.0.0-beta.7 [skip ci] 2024-12-24 13:34:42 +00:00
homarr-releases[bot] 620ec0315f chore(release): automatic release v1.0.0 2024-12-24 13:33:47 +00:00
Meier Lukas 8df398c3c7 feat(apps): remove url variables (#1771) 2024-12-24 14:16:24 +01:00
Meier Lukas 425359be02 fix(board): item background and border overridden by mantine card styles (#1769) 2024-12-24 14:16:11 +01:00
Meier Lukas 8893b72fe3 fix(board): tiles order not respected on firefox (#1770) 2024-12-24 14:15:58 +01:00
Meier Lukas b9483379dd fix(onboard): missing admin permission assignment for external group (#1764) 2024-12-24 14:15:46 +01:00
Meier Lukas 0336803550 fix(auth): oidc redirect does not respect https protocol (#1763) 2024-12-24 14:15:34 +01:00
Meier Lukas e220087e96 fix(icons): local icon repository does not work with medias (#1765) 2024-12-24 14:15:18 +01:00
Manuel f127c9325c config: update issue templates (#1751) 2024-12-24 13:05:59 +00:00
homarr-renovate[bot] c410b06a15 fix(deps): update tanstack-query monorepo to ^5.62.10 (#1772)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-24 12:25:11 +00:00
Thomas Camlong ce755efe4d chore(lang): updated translations from crowdin (#1760)
* New translations en.json (Hungarian)

* New translations en.json (Hebrew)

* New translations en.json (Hebrew)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Hebrew)
2024-12-24 11:01:14 +01:00
homarr-renovate[bot] 8b416a6dd3 fix(deps): update dependency @ctrl/qbittorrent to ^9.2.0 (#1768)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-24 04:18:59 +00:00
homarr-renovate[bot] fe95334aa7 fix(deps): update dependency eslint-plugin-react to ^7.37.3 (#1767)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-24 03:02:21 +00:00
homarr-crowdin[bot] b0facfd77a chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-24 00:44:22 +00:00
homarr-renovate[bot] 01db27c34f fix(deps): update dependency react-error-boundary to v5 (#1748)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-23 20:17:53 +01:00
homarr-renovate[bot] 999cf58625 chore(deps): update dependency dotenv-cli to v8 (#1741)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-23 20:14:01 +01:00
homarr-renovate[bot] 9223dd06ee fix(deps): update dependency typescript-eslint to ^8.18.2 (#1762)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-23 17:34:06 +00:00
homarr-renovate[bot] 42da2b9b06 fix(deps): update tanstack-query monorepo to ^5.62.9 (#1761)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-23 17:15:12 +00:00
Thomas CamlongandMeier Lukas a73eed63eb chore(lang): updated translations from crowdin (#1754)
* New translations en.json (Italian)

* New translations en.json (Hungarian)

* New translations en.json (Hungarian)

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-23 12:01:12 +01:00
homarr-crowdin[bot] 44dcc72d43 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-23 10:51:54 +00:00
homarr-renovate[bot] d02ded5511 fix(deps): update dependency mysql2 to v3.12.0 (#1758)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-23 10:38:45 +00:00
homarr-renovate[bot] dfacb08dbd fix(deps): update mantine monorepo to ^7.15.2 (#1757)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-23 10:17:37 +00:00
homarr-renovate[bot] 4e85bbbad0 fix(deps): update dependency jotai to ^2.11.0 (#1756)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-23 06:21:05 +00:00
ManuelandMeier Lukas d78c6ed529 fix: copy media link (#1750)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-22 20:20:33 +01:00
Thomas CamlongandMeier Lukas 210ce3669f chore(lang): updated translations from crowdin (#1747)
* New translations en.json (German)

* New translations en.json (German)

* New translations en.json (German)

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-22 18:39:19 +01:00
homarr-crowdin[bot] 44e5346d75 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-22 17:25:57 +00:00
Meier Lukas d34d665229 docs: update crowdin badge to new translation project (#1745) 2024-12-22 15:40:57 +01:00
Thomas CamlongandMeier Lukas b6dc38483d chore(lang): updated translations from crowdin (#1738)
* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations en.json (German)

* New translations en.json (Turkish)

* New translations en.json (German)

* New translations en.json (Hebrew)

* New translations en.json (Chinese Simplified)

* New translations en.json (Hebrew)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-21 12:13:42 +01:00
homarr-crowdin[bot] a7e387e54c chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-21 11:10:45 +00:00
homarr-crowdin[bot] 189f35f3cb chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-21 11:05:13 +00:00
Releases Homarr 3c8d4f5018 chore(release): 1.0.0-beta.6 [skip ci] 2024-12-20 19:13:19 +00:00
homarr-releases[bot] 2b59f19a7f chore(release): automatic release v1.0.0 2024-12-20 19:12:27 +00:00
homarr-renovate[bot] fff1da88bd fix(deps): update dependency next-intl to v3.26.3 (#1740)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-20 13:23:18 +00:00
homarr-renovate[bot] 720fdb772c fix(deps): update dependency ioredis to v5.4.2 (#1739)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-20 12:25:58 +00:00
homarr-renovate[bot] a225bdd70c chore(deps): update pnpm to v9.15.1 (#1737)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-20 00:55:05 +00:00
homarr-crowdin[bot] e40c348352 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-20 00:44:25 +00:00
Thomas Camlong 28e1175565 chore(lang): updated translations from crowdin (#1734)
* New translations en.json (Slovak)

* New translations en.json (Slovak)
2024-12-19 23:28:56 +01:00
homarr-crowdin[bot] b450b00934 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-19 22:28:10 +00:00
Thomas CamlongandMeier Lukas 8e0bf87c56 chore(lang): updated translations from crowdin (#1722)
* New translations en.json (Hungarian)

* New translations en.json (Russian)

* New translations en.json (Turkish)

* New translations en.json (Hungarian)

* New translations en.json (Turkish)

* New translations en.json (Hungarian)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations en.json (Chinese Traditional)

* New translations en.json (Czech)

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2024-12-19 19:45:58 +01:00
homarr-crowdin[bot] 9e061f8fc7 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2024-12-19 18:34:13 +00:00
Releases Homarr 10855bd84c chore(release): 1.0.0-beta.5 [skip ci] 2024-12-19 16:52:00 +00:00
homarr-releases[bot] 93d5ca683b chore(release): automatic release v1.0.0 2024-12-19 16:51:02 +00:00
Meier Lukas d2584b63c5 fix: always require db-user and password for mysql (#1730) 2024-12-19 17:48:10 +01:00
Meier Lukas 8c1156240c fix: about version is one behind because of missing package-json update in deploy (#1725) 2024-12-19 16:11:12 +01:00
Meier Lukas bb7f751481 fix: update of dev branch has no credentials (#1724) 2024-12-19 16:10:55 +01:00
Meier Lukas 1e8a5359b7 fix: allow additional file-type for oldmarr import (#1723) 2024-12-19 16:10:33 +01:00
Meier Lukas 550bca6dee fix: mysql operations not working (#1728) 2024-12-19 16:10:22 +01:00
homarr-renovate[bot] 2ae3d938ee fix(deps): update tiptap monorepo to v2.10.4 (#1729)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-19 13:05:35 +00:00
homarr-renovate[bot] 2b4bc8352e fix(deps): update nextjs monorepo to ^14.2.21 (#1727)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-19 11:16:50 +00:00
homarr-renovate[bot] c0f681637d fix(deps): update dependency tldts to ^6.1.69 (#1726)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-19 11:15:50 +00:00
Thomas Camlong 92cd1c5bd0 chore(lang): updated translations from crowdin (#1721)
* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Croatian)

* New translations en.json (Latvian)

* New translations en.json (Cree)

* New translations en.json (Turkish)

* New translations en.json (Hebrew)

* New translations en.json (Russian)

* New translations en.json (Turkish)
2024-12-19 07:59:10 +01:00
homarr-renovate[bot] 3b7e6cc083 fix(deps): update dependency zod-form-data to ^2.0.4 (#1719)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-18 18:46:07 +00:00
homarr-renovate[bot] 61afab518f fix(deps): update dependency undici to v7.2.0 (#1718)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-18 18:20:57 +00:00
homarr-renovate[bot] a7bd664c5c fix(deps): update dependency zod-form-data to ^2.0.3 (#1717)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-18 17:34:58 +00:00
homarr-renovate[bot] a2c3bee66c chore(deps): update dependency @types/react to ^19.0.2 (#1716)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2024-12-18 16:21:01 +00:00
SeDemal b6dad438c2 feat: Add quick filters for integration and status + minor UI improvement from feedback (#1641) 2024-12-18 15:16:54 +01:00
homarr-releases[bot] fac522adb9 chore(release): automatic release v0.1.0 2024-12-13 19:13:52 +00:00
homarr-releases[bot] 9945425eff chore(release): automatic release v0.1.0 2024-12-06 19:14:14 +00:00
homarr-releases[bot] 8cd5833281 chore(release): automatic release v0.1.0 2024-11-29 19:15:06 +00:00
homarr-releases[bot] ef7174aa36 chore(release): automatic release v0.1.0 2024-11-22 19:14:01 +00:00
homarr-releases[bot] 4ce76f8a73 chore(release): automatic release v0.1.0 2024-11-15 19:13:45 +00:00
homarr-releases[bot] 3e65af9ebe chore(release): automatic release v0.1.0 2024-11-08 19:12:25 +00:00
homarr-releases[bot] c6e5bec348 chore(release): automatic release v0.1.0 2024-11-04 19:34:22 +00:00
homarr-releases[bot] 3e74c065f5 chore(release): automatic release v0.1.0 2024-11-04 18:21:54 +00:00
homarr-releases[bot] 14ae732d0c chore(release): automatic release v0.1.0 2024-11-01 19:13:45 +00:00
homarr-releases[bot] b97883f44d chore(release): automatic release v0.1.0 2024-10-25 19:12:51 +00:00
homarr-releases[bot] 741753fa74 chore(release): automatic release v0.1.0 2024-10-18 19:13:17 +00:00
homarr-releases[bot] 1f0f8912b6 chore(release): automatic release v0.1.0 2024-10-11 21:37:04 +00:00
homarr-releases[bot] 49d6932218 chore(release): automatic release v0.1.0 2024-10-11 19:12:56 +00:00
homarr-releases[bot] f13fefa64d chore(release): automatic release v0.1.0 2024-10-04 19:33:01 +00:00
homarr-releases[bot] cef567efdf chore(release): automatic release v0.1.0 2024-10-04 19:12:57 +00:00
homarr-releases[bot] db2b79ee7f chore(release): automatic release v0.1.0 2024-09-27 19:13:34 +00:00
Meier Lukas b46d30b04c chore(release): release v0.1.0 2024-09-21 11:42:43 +02:00
homarr-releases[bot] de4283b5fa chore(release): automatic release v0.1.0 2024-09-20 19:13:13 +00:00
homarr-releases[bot] a08d3e387f chore(release): automatic release v0.1.0 2024-09-13 19:12:49 +00:00
homarr-releases[bot] bd1ff01109 chore(release): automatic release v0.1.0 2024-09-06 19:13:34 +00:00
homarr-releases[bot] b24b1cc8a9 chore(release): automatic release v0.1.0 2024-08-30 19:11:57 +00:00
homarr-releases[bot] e81edc898f chore(release): automatic release v0.1.0 2024-08-23 19:11:47 +00:00
homarr-releases[bot] 4d864878e3 chore(release): automatic release v0.1.0 2024-08-16 19:13:19 +00:00
homarr-releases[bot] 5776ead3e3 chore(release): automatic release v0.1.0 2024-08-09 19:12:44 +00:00
homarr-releases[bot] baf32c3fe6 chore(release): automatic release v0.1.0 2024-08-02 19:52:15 +00:00
Meier Lukas ea408a0fdd Merge pull request #570 from homarr-labs/dev
chore: merge to main
2024-05-28 21:50:27 +02:00
Manuel 17f93097f6 Merge pull request #547 from homarr-labs/dev
chore: merge to main
2024-05-24 22:16:13 +02:00
388 changed files with 43430 additions and 13680 deletions

No files matched your search

+2 -1
View File
@@ -6,4 +6,5 @@ README.md
.next
.git
dev
.build
.build
e2e
+11 -4
View File
@@ -4,6 +4,14 @@
# This file will be committed to version control, so make sure not to have any secrets in it.
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
# The below secret is not used anywhere but required for Auth.js (Would encrypt JWTs and Mail hashes, both not used)
AUTH_SECRET="supersecret"
# The below secret is used to encrypt integration secrets in the database.
# It should be a 32-byte string, generated by running `openssl rand -hex 32` on Unix
# or starting the project without any (which will show a randomly generated one).
SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
# This is how you can use the sqlite driver:
DB_DRIVER='better-sqlite3'
DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
@@ -20,10 +28,9 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
# DB_PASSWORD='password'
# DB_NAME='name-of-database'
# You can generate the secret via 'openssl rand -base64 32' on Unix
# @see https://next-auth.js.org/configuration/options#secret
AUTH_SECRET='supersecret'
# The below path can be used to store trusted certificates during development, it is not required and can be left empty.
# If it is used, please use the full path to the directory where the certificates are stored.
# LOCAL_CERTIFICATE_PATH='FULL_PATH_TO_CERTIFICATES'
TURBO_TELEMETRY_DISABLED=1
+58 -17
View File
@@ -1,37 +1,78 @@
name: 🐞 Bug Report
description: Create a bug report to help us improve
description: Report that something is broken, not working as intended or causes side-effects
title: "bug: "
labels: ["🐞❔ unconfirmed bug"]
labels: ["needs triage"]
body:
- type: textarea
attributes:
label: Provide environment information
description: |
Run this command in your project root and paste the results in a code block:
```bash
npx envinfo --system --binaries
```
validations:
required: true
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of the bug, as well as what you expected to happen when encountering it.
validations:
required: true
- type: input
- type: textarea
attributes:
label: Link to reproduction
description: Please provide a link to a reproduction of the bug. Issues without a reproduction repo may be ignored.
label: Steps to reproduce
description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc.
validations:
required: true
- type: textarea
attributes:
label: To reproduce
description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc.
label: Impact
description: How big is the impact of this bug? Does it make Homarr unusable? Is there any workaround that you're aware of?
validations:
required: true
- type: textarea
attributes:
label: Additional information
description: Add any other information related to the bug here, screenshots if applicable.
- type: dropdown
id: version
attributes:
label: Version
description: What version of Homarr are you running?
options:
- 1.0.0-beta
- Other (describe in "additional information")
default: 0
validations:
required: true
- type: dropdown
id: installationMethod
attributes:
label: Installation method
description: How do you run Homarr? Post docker-compose, configs or screenshots if applicable.
options:
- Docker Run
- Docker Compose
- Portainer
- Helm
- QNAP
- Saltbox
- EasyPanel
- Unraid Apps
- TrueNAS Apps
- Synology
- HomeAssistant Addon
- From Source
- Other (describe in "additional information")
default: 0
validations:
required: true
- type: dropdown
id: browser
attributes:
label: Browser
description: If relevant, what browser do you use?
options:
- Firefox
- Edge (Chromium)
- Edge (Proprietary)
- Chrome
- Safari
- Vivaldi
- Brave
- Samsung Internet
- Other (describe in "additional information")
default: 0
validations:
required: false
+2 -12
View File
@@ -2,26 +2,16 @@
# See here: https://github.com/vercel/next.js/blob/canary/.github/ISSUE_TEMPLATE/3.feature_request.yml
name: 🛠 Feature Request
description: Create a feature request for the core packages
description: Request a new feature that you would like to have implemented
title: "feat: "
labels: ["✨ enhancement"]
labels: ["needs triage"]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to file a feature request. Please fill out this form as completely as possible.
- type: textarea
attributes:
label: Describe the feature you'd like to request
description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed.
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like to see
description: Please describe the solution you would like to see. Adding example usage is a good way to provide context.
validations:
required: true
- type: textarea
attributes:
label: Additional information
+40
View File
@@ -0,0 +1,40 @@
name: 🛠 Integration request
description: Request support for a new integration (eg. Sonarr, Radarr)
title: "feat: "
labels: ["needs triage"]
body:
- type: input
attributes:
label: Project Website
description: Post the link to the website of the application. Paste the official link.
placeholder: ex. https://sonarr.tv/
validations:
required: true
- type: textarea
attributes:
label: Describe what data should be consumed by Homarr
description: Please describe what data Homarr should fetch from the integration. Specify in what interval data should be fetched and whether the user can also perform write operations (eg. deleting a movie or adding a user).
validations:
required: true
- type: textarea
attributes:
label: Additional information
description: Add any other information related to the integration.
- type: dropdown
attributes:
label: Public API available?
description: Is there a public API available, that we can consume in Homarr?
options:
- Yes, available on a website
- Yes, available in the application itself
- No
validations:
required: true
- type: dropdown
attributes:
label: Are you willing to contribute this yourself?
options:
- Yes
- No
validations:
required: true
+30
View File
@@ -0,0 +1,30 @@
name: 🛠 Widget request
description: Request a new widget (eg. Clock, Calendar, ...)
title: "feat: "
labels: ["needs triage"]
body:
- type: input
attributes:
label: Compatible integrations
description: Post a list of the integrations that should be compatible with this widget. Divide using comma. Leave empty if no integration is needed.
placeholder: ex. Sonarr, Radarr, Lidarr, Readarr, Nextcloud
validations:
required: false
- type: textarea
attributes:
label: Describe what data should be displayed
description: Please describe what data Homarr should display. Describe how elements should be intractable and what actions the user can perform.
validations:
required: true
- type: textarea
attributes:
label: Additional information
description: Add any other information related to the widget.
- type: dropdown
attributes:
label: Are you willing to contribute this yourself?
options:
- Yes
- No
validations:
required: true
+5
View File
@@ -6,6 +6,11 @@
matchPackagePatterns: ["^@homarr/"],
enabled: false,
},
// Disable Dockerode updates see https://github.com/apocas/dockerode/issues/787
{
matchPackagePatterns: ["^dockerode$"],
enabled: false,
},
{
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
automerge: true,
+2
View File
@@ -91,6 +91,8 @@ jobs:
network: host
env:
SKIP_ENV_VALIDATION: true
- name: Install playwright browsers
run: pnpm exec playwright install chromium
- name: Run E2E Tests
shell: bash
run: pnpm test:e2e
+15 -9
View File
@@ -13,11 +13,6 @@ on:
required: false
default: true
description: Send notifications
push-image:
type: boolean
required: false
default: true
description: Push Docker Image
permissions:
contents: write
@@ -40,6 +35,7 @@ jobs:
SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' }}
outputs:
version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }}
git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }}
steps:
- run: echo "Skipping release for workflow_dispatch event"
@@ -87,10 +83,19 @@ jobs:
run: |
git fetch --tags
echo "version=$(git describe --tags --abbrev=0)" >> "$GITHUB_OUTPUT"
- name: Read git ref
if: env.SKIP_RELEASE == 'false'
id: read-git-ref
run: |
echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Update dev branch
if: env.SKIP_RELEASE == 'false'
continue-on-error: true # Prevent pipeline from failing when merge fails
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
run: |
git config user.name "Releases Homarr"
git config user.email "175486441+homarr-releases[bot]@users.noreply.github.com"
git fetch origin dev
git checkout dev
git pull origin dev
@@ -104,9 +109,10 @@ jobs:
NEXT_VERSION: ${{ needs.release.outputs.version }}
DEPLOY_LATEST: ${{ github.ref_name == 'main' }}
DEPLOY_BETA: ${{ github.ref_name == 'beta' }}
PUSH_IMAGE: ${{ github.event_name != 'workflow_dispatch' || github.events.inputs.push-image == true }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.release.outputs.git_ref }}
- name: Discord notification
if: ${{ github.events.inputs.send-notifications != false }}
env:
@@ -133,13 +139,13 @@ jobs:
${{ env.DEPLOY_LATEST == 'true' && 'type=raw,value=latest' || null }}
${{ env.DEPLOY_BETA == 'true' && 'type=raw,value=beta' || null }}
type=raw,value=${{ env.NEXT_VERSION }}
- name: Build and maybe push
- name: Build and push
id: buildPushAction
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
context: .
push: ${{ env.PUSH_IMAGE}}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
network: host
@@ -150,4 +156,4 @@ jobs:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: "Deployment of image has completed for branch ${{ github.ref_name }}. Image ID is '${{ steps.buildPushAction.outputs.imageid }}'. ${{ env.PUSH_IMAGE == 'true' && '' || 'This was a dry run' }}"
args: "Deployment of image has completed for branch ${{ github.ref_name }}. Image ID is '${{ steps.buildPushAction.outputs.imageid }}'."
@@ -37,7 +37,7 @@ jobs:
token: ${{ github.token }}
branch: dev
- name: Create pull request
run: 'gh pr create --title "chore(release): automatic release ${{ steps.semver.outputs.next }}" --body "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}``" --base beta --head dev --label automerge'
run: 'gh pr create --title "chore(release): automatic release ${{ steps.semver.outputs.next }}" --body "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}``" --base main --head dev --label automerge'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Discord notification
+5 -1
View File
@@ -54,10 +54,14 @@ yarn-error.log*
*.log
apps/tasks/tasks.cjs
apps/tasks/tasks.css
apps/websocket/wssServer.cjs
apps/websocket/wssServer.css
apps/nextjs/.million/
packages/cli/cli.cjs
# e2e mounts
e2e/shared/tmp
#personal backgrounds
apps/nextjs/public/images/background.png
apps/nextjs/public/images/background.png
+1 -1
View File
@@ -1 +1 @@
22.12.0
22.13.0
+1 -1
View File
@@ -39,5 +39,5 @@
"i18n-ally.localesPaths": [
"packages/translation/src/lang",
],
"i18n-ally.keystyle": "auto",
"i18n-ally.keystyle": "nested",
}
+1330
View File
File diff suppressed because it is too large. Load diff
+22 -35
View File
@@ -1,4 +1,4 @@
FROM node:22.12.0-alpine AS base
FROM node:22.13.0-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
@@ -12,9 +12,6 @@ COPY . .
RUN corepack enable pnpm && pnpm install --recursive --frozen-lockfile
# Install sharp for image optimization
RUN corepack enable pnpm && pnpm install sharp -w
# Copy static data as it is not part of the build
COPY static-data ./static-data
ARG SKIP_ENV_VALIDATION='true'
@@ -25,51 +22,41 @@ RUN corepack enable pnpm && pnpm build
FROM base AS runner
WORKDIR /app
# gettext is required for envsubst
RUN apk add --no-cache redis nginx bash gettext su-exec
# gettext is required for envsubst, openssl for generating AUTH_SECRET, su-exec for running application as non-root
RUN apk add --no-cache redis nginx bash gettext su-exec openssl
RUN mkdir /appdata
VOLUME /appdata
RUN mkdir /secrets
VOLUME /secrets
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Enable homarr cli
COPY --from=builder --chown=nextjs:nodejs /app/packages/cli/cli.cjs /app/apps/cli/cli.cjs
COPY --from=builder /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
# Don't run production as root
RUN chown -R nextjs:nodejs /secrets
RUN mkdir -p /var/cache/nginx && chown -R nextjs:nodejs /var/cache/nginx && \
mkdir -p /var/log/nginx && chown -R nextjs:nodejs /var/log/nginx && \
mkdir -p /var/lib/nginx && chown -R nextjs:nodejs /var/lib/nginx && \
touch /run/nginx/nginx.pid && chown -R nextjs:nodejs /run/nginx/nginx.pid && \
mkdir -p /etc/nginx/templates /etc/nginx/ssl/certs && chown -R nextjs:nodejs /etc/nginx
RUN mkdir -p /var/cache/nginx && \
mkdir -p /var/log/nginx && \
mkdir -p /var/lib/nginx && \
touch /run/nginx/nginx.pid && \
mkdir -p /etc/nginx/templates /etc/nginx/ssl/certs
COPY --from=builder /app/apps/nextjs/next.config.mjs .
COPY --from=builder /app/apps/nextjs/next.config.ts .
COPY --from=builder /app/apps/nextjs/package.json .
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=builder /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
COPY --from=builder /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
COPY --from=builder /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
COPY --from=builder --chown=nextjs:nodejs /app/packages/db/migrations ./db/migrations
COPY --from=builder /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=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 scripts/entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
COPY --chown=nextjs:nodejs scripts/generateRandomSecureKey.js ./generateRandomSecureKey.js
COPY --chown=nextjs:nodejs packages/redis/redis.conf /app/redis.conf
COPY --chown=nextjs:nodejs nginx.conf /etc/nginx/templates/nginx.conf
COPY --from=builder /app/apps/nextjs/.next/standalone ./
COPY --from=builder /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
COPY --from=builder /app/apps/nextjs/public ./apps/nextjs/public
COPY scripts/run.sh ./run.sh
COPY --chmod=777 scripts/entrypoint.sh ./entrypoint.sh
COPY packages/redis/redis.conf /app/redis.conf
COPY nginx.conf /etc/nginx/templates/nginx.conf
ENV DB_URL='/appdata/db/db.sqlite'
@@ -78,4 +65,4 @@ ENV DB_DRIVER='better-sqlite3'
ENV AUTH_PROVIDERS='credentials'
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD ["sh", "run.sh"]
CMD ["sh", "run.sh"]
@@ -1,22 +1,33 @@
// Importing env files here to validate on build
import "@homarr/auth/env.mjs";
import "@homarr/auth/env";
import "@homarr/db/env";
import "@homarr/common/env";
import type { NextConfig } from "next";
import MillionLint from "@million/lint";
import createNextIntlPlugin from "next-intl/plugin";
import "./src/env.mjs";
import "./src/env.ts";
// 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 nextConfig = {
interface WebpackConfig {
module: {
rules: {
test: RegExp;
loader: string;
}[];
};
}
const nextConfig: NextConfig = {
output: "standalone",
reactStrictMode: true,
/** We already do linting and typechecking as separate tasks in CI */
eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
webpack: (config, { isServer }) => {
webpack: (config: WebpackConfig, { isServer }) => {
if (isServer) {
config.module.rules.push({
test: /\.node$/,
@@ -36,6 +47,7 @@ const nextConfig = {
};
// Skip transform is used because of webpack loader, without it for example 'Tooltip.Floating' will not work and show an error
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const withMillionLint = MillionLint.next({ rsc: true, skipTransform: true, telemetry: false });
export default withNextIntl(nextConfig);
+29 -28
View File
@@ -6,7 +6,7 @@
"scripts": {
"build": "pnpm with-env next build",
"clean": "git clean -xdf .next .turbo node_modules",
"dev": "pnpm with-env next dev",
"dev": "pnpm with-env next dev --turbopack",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"start": "pnpm with-env next start",
@@ -18,12 +18,13 @@
"@homarr/analytics": "workspace:^0.1.0",
"@homarr/api": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0",
"@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/cron-job-status": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/form": "workspace:^0.1.0",
"@homarr/gridstack": "^1.11.2",
"@homarr/gridstack": "^1.11.3",
"@homarr/integrations": "workspace:^0.1.0",
"@homarr/log": "workspace:^",
"@homarr/modals": "workspace:^0.1.0",
@@ -38,18 +39,18 @@
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "^7.15.1",
"@mantine/core": "^7.15.1",
"@mantine/dropzone": "^7.15.1",
"@mantine/hooks": "^7.15.1",
"@mantine/modals": "^7.15.1",
"@mantine/tiptap": "^7.15.1",
"@mantine/colors-generator": "^7.16.0",
"@mantine/core": "^7.16.0",
"@mantine/dropzone": "^7.16.0",
"@mantine/hooks": "^7.16.0",
"@mantine/modals": "^7.16.0",
"@mantine/tiptap": "^7.16.0",
"@million/lint": "1.0.14",
"@t3-oss/env-nextjs": "^0.11.1",
"@tabler/icons-react": "^3.26.0",
"@tanstack/react-query": "^5.62.8",
"@tanstack/react-query-devtools": "^5.62.8",
"@tanstack/react-query-next-experimental": "5.62.8",
"@tabler/icons-react": "^3.28.1",
"@tanstack/react-query": "^5.64.1",
"@tanstack/react-query-devtools": "^5.64.1",
"@tanstack/react-query-next-experimental": "5.64.1",
"@trpc/client": "next",
"@trpc/next": "next",
"@trpc/react-query": "next",
@@ -61,18 +62,18 @@
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"dotenv": "^16.4.7",
"flag-icons": "^7.2.3",
"glob": "^11.0.0",
"jotai": "^2.10.4",
"mantine-react-table": "2.0.0-beta.7",
"next": "^14.2.20",
"flag-icons": "^7.3.1",
"glob": "^11.0.1",
"jotai": "^2.11.0",
"mantine-react-table": "2.0.0-beta.8",
"next": "15.1.4",
"postcss-preset-mantine": "^1.17.0",
"prismjs": "^1.29.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-error-boundary": "^4.1.2",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-error-boundary": "^5.0.0",
"react-simple-code-editor": "^0.14.1",
"sass": "^1.83.0",
"sass": "^1.83.4",
"superjson": "2.2.2",
"swagger-ui-react": "^5.18.2",
"use-deep-compare-effect": "^1.8.1"
@@ -81,16 +82,16 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "2.4.5",
"@types/node": "^22.10.2",
"@types/chroma-js": "3.1.0",
"@types/node": "^22.10.7",
"@types/prismjs": "^1.26.5",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"@types/react": "19.0.7",
"@types/react-dom": "19.0.3",
"@types/swagger-ui-react": "^4.18.3",
"concurrently": "^9.1.0",
"eslint": "^9.17.0",
"concurrently": "^9.1.2",
"eslint": "^9.18.0",
"node-loader": "^2.1.0",
"prettier": "^3.4.2",
"typescript": "^5.7.2"
"typescript": "^5.7.3"
}
}
@@ -18,9 +18,10 @@ import superjson from "superjson";
import type { SuperJSONResult } from "superjson";
import type { AppRouter } from "@homarr/api";
import { clientApi, createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/client";
import { clientApi, getTrpcUrl } from "@homarr/api/client";
import { createHeadersCallbackForSource } from "@homarr/api/shared";
import { env } from "~/env.mjs";
import { env } from "~/env";
const getWebSocketProtocol = () => {
// window is not defined on server side
@@ -4,22 +4,24 @@ import { Card, Center, Stack, Text, Title } from "@mantine/core";
import { auth } from "@homarr/auth/next";
import { isProviderEnabled } from "@homarr/auth/server";
import { and, db, eq } from "@homarr/db";
import { invites } from "@homarr/db/schema/sqlite";
import { invites } from "@homarr/db/schema";
import { getScopedI18n } from "@homarr/translation/server";
import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
import { RegistrationForm } from "./_registration-form";
interface InviteUsagePageProps {
params: {
params: Promise<{
id: string;
};
searchParams: {
}>;
searchParams: Promise<{
token: string;
};
}>;
}
export default async function InviteUsagePage({ params, searchParams }: InviteUsagePageProps) {
export default async function InviteUsagePage(props: InviteUsagePageProps) {
const searchParams = await props.searchParams;
const params = await props.params;
if (!isProviderEnabled("credentials")) notFound();
const session = await auth();
@@ -57,7 +59,7 @@ export default async function InviteUsagePage({ params, searchParams }: InviteUs
{t("subtitle")}
</Text>
</Stack>
<Card bg="dark.8" w={64 * 6} maw="90vw">
<Card withBorder w={64 * 6} maw="90vw">
<RegistrationForm invite={invite} />
</Card>
<Text size="xs" c="gray.5" ta="center">
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation";
import { Card, Center, Stack, Text, Title } from "@mantine/core";
import { env } from "@homarr/auth/env.mjs";
import { env } from "@homarr/auth/env";
import { auth } from "@homarr/auth/next";
import { getScopedI18n } from "@homarr/translation/server";
@@ -9,12 +9,13 @@ import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
import { LoginForm } from "./_login-form";
interface LoginProps {
searchParams: {
searchParams: Promise<{
callbackUrl?: string;
};
}>;
}
export default async function Login({ searchParams }: LoginProps) {
export default async function Login(props: LoginProps) {
const searchParams = await props.searchParams;
const session = await auth();
if (session) {
@@ -35,7 +36,7 @@ export default async function Login({ searchParams }: LoginProps) {
{t("subtitle")}
</Text>
</Stack>
<Card bg="dark.8" w={64 * 6} maw="90vw">
<Card withBorder w={64 * 6} maw="90vw">
<LoginForm
providers={env.AUTH_PROVIDERS}
oidcClientName={env.AUTH_OIDC_CLIENT_NAME}
@@ -28,7 +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 { env } from "~/env";
import { useEditMode, useRequiredBoard } from "./_context";
export const BoardContentHeaderActions = () => {
@@ -2,7 +2,7 @@ import { IconHomeOff } from "@tabler/icons-react";
import { auth } from "@homarr/auth/next";
import { db } from "@homarr/db";
import { boards } from "@homarr/db/schema/sqlite";
import { boards } from "@homarr/db/schema";
import { getI18n } from "@homarr/translation/server";
import type { BoardNotFoundProps } from "~/components/board/not-found";
@@ -31,15 +31,15 @@ import { GeneralSettingsContent } from "./_general";
import { LayoutSettingsContent } from "./_layout";
interface Props {
params: {
params: Promise<{
name: string;
};
searchParams: {
}>;
searchParams: Promise<{
tab?: keyof TranslationObject["board"]["setting"]["section"];
};
}>;
}
const getBoardAndPermissionsAsync = async (params: Props["params"]) => {
const getBoardAndPermissionsAsync = async (params: Awaited<Props["params"]>) => {
try {
const board = await api.board.getBoardByName({ name: params.name });
const { hasFullAccess } = await getBoardPermissionsAsync(board);
@@ -63,7 +63,9 @@ const getBoardAndPermissionsAsync = async (params: Props["params"]) => {
}
};
export default async function BoardSettingsPage({ params, searchParams }: Props) {
export default async function BoardSettingsPage(props: Props) {
const searchParams = await props.searchParams;
const params = await props.params;
const { board, permissions } = await getBoardAndPermissionsAsync(params);
const boardSettings = await getServerSettingByKeyAsync(db, "board");
const { hasFullAccess, hasChangeAccess } = await getBoardPermissionsAsync(board);
@@ -99,7 +101,11 @@ export default async function BoardSettingsPage({ params, searchParams }: Props)
<BoardAccessSettings board={board} initialPermissions={permissions} />
</AccordionItemFor>
<AccordionItemFor value="dangerZone" icon={IconAlertTriangle} danger noPadding>
<DangerZoneSettingsContent hideVisibility={boardSettings.homeBoardId === board.id} />
<DangerZoneSettingsContent
hideVisibility={
boardSettings.homeBoardId === board.id || boardSettings.mobileHomeBoardId === board.id
}
/>
</AccordionItemFor>
</>
)}
@@ -28,9 +28,9 @@ export const createBoardLayout = <TParams extends Params>({
params,
children,
}: PropsWithChildren<{
params: TParams;
params: Promise<TParams>;
}>) => {
const initialBoard = await getInitialBoard(params).catch((error) => {
const initialBoard = await getInitialBoard(await params).catch((error) => {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
logger.warn(error);
notFound();
@@ -24,10 +24,19 @@ export const ImportDropZone = ({ loading, updateFile }: ImportDropZoneProps) =>
}}
acceptColor="blue.6"
rejectColor="red.6"
accept={[MIME_TYPES.zip]}
accept={[MIME_TYPES.zip, "application/x-zip-compressed"]}
loading={loading}
multiple={false}
maxSize={1024 * 1024 * 1024 * 64} // 64 MB
onReject={(rejections) => {
console.error(
"Rejected files",
rejections.map(
(rejection) =>
`File: ${rejection.file.name} size=${rejection.file.size} fileType=${rejection.file.type}\n - ${rejection.errors.map((error) => error.message).join("\n - ")}`,
),
);
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: "none" }}>
<Dropzone.Accept>
+9 -5
View File
@@ -9,11 +9,12 @@ import "~/styles/scroll-area.scss";
import { notFound } from "next/navigation";
import { NextIntlClientProvider } from "next-intl";
import { env } from "@homarr/auth/env.mjs";
import { env } from "@homarr/auth/env";
import { auth } from "@homarr/auth/next";
import { ModalProvider } from "@homarr/modals";
import { Notifications } from "@homarr/notifications";
import { SpotlightProvider } from "@homarr/spotlight";
import type { SupportedLanguage } from "@homarr/translation";
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
import { getI18nMessages } from "@homarr/translation/server";
@@ -63,14 +64,17 @@ export const viewport: Viewport = {
],
};
export default async function Layout(props: { children: React.ReactNode; params: { locale: string } }) {
if (!isLocaleSupported(props.params.locale)) {
export default async function Layout(props: {
children: React.ReactNode;
params: Promise<{ locale: SupportedLanguage }>;
}) {
if (!isLocaleSupported((await props.params).locale)) {
notFound();
}
const session = await auth();
const colorScheme = await getCurrentColorSchemeAsync();
const direction = isLocaleRTL(props.params.locale) ? "rtl" : "ltr";
const direction = isLocaleRTL((await props.params).locale) ? "rtl" : "ltr";
const i18nMessages = await getI18nMessages();
const StackedProvider = composeWrappers([
@@ -89,7 +93,7 @@ export default async function Layout(props: { children: React.ReactNode; params:
return (
// Instead of ColorSchemScript we use data-mantine-color-scheme to prevent flickering
<html
lang={props.params.locale}
lang={(await props.params).locale}
dir={direction}
data-mantine-color-scheme={colorScheme}
style={{
@@ -5,29 +5,29 @@ import { splitToNChunks } from "@homarr/common";
import classes from "./hero-banner.module.css";
const icons = [
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/homarr.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/sabnzbd.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/deluge.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/radarr.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/sonarr.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/lidarr.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/sabnzbd.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/deluge.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/radarr.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/sonarr.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/lidarr.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/pihole.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/dashdot.png",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/overseerr.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/plex.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/jellyfin.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/dashdot.png",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/overseerr.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plex.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/jellyfin.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/homeassistant.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/freshrss.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/readarr.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/transmission.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/qbittorrent.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nzbget.png",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/openmediavault.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/readarr.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/transmission.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/qbittorrent.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/nzbget.png",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/openmediavault.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/docker.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/jellyseerr.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/jellyseerr.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/adguardhome.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/tdarr.png",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/prowlarr.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/tdarr.png",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/prowlarr.svg",
];
const countIconGroups = 3;
@@ -37,16 +37,16 @@ export async function generateMetadata() {
};
}
const getHost = () => {
const getHostAsync = async () => {
if (process.env.HOSTNAME) {
return `${process.env.HOSTNAME}:3000`;
}
return headers().get("host");
return (await headers()).get("host");
};
export default async function AboutPage() {
const baseServerUrl = `http://${getHost()}`;
const baseServerUrl = `http://${await getHostAsync()}`;
const t = await getScopedI18n("management.page.about");
const attributes = await getPackageAttributesAsync();
const githubContributors = (await fetch(`${baseServerUrl}/api/about/contributors/github`).then((res) =>
@@ -9,10 +9,11 @@ import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { AppEditForm } from "./_app-edit-form";
interface AppEditPageProps {
params: { id: string };
params: Promise<{ id: string }>;
}
export default async function AppEditPage({ params }: AppEditPageProps) {
export default async function AppEditPage(props: AppEditPageProps) {
const params = await props.params;
const session = await auth();
if (!session?.user.permissions.includes("app-modify-all")) {
@@ -6,8 +6,10 @@ 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 type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { SearchInput, TablePagination } from "@homarr/ui";
import { z } from "@homarr/validation";
import { ManageContainer } from "~/components/manage/manage-container";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
@@ -15,22 +17,35 @@ 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 searchParamsSchema = z.object({
search: z.string().optional(),
pageSize: z.string().regex(/\d+/).transform(Number).catch(10),
page: z.string().regex(/\d+/).transform(Number).catch(1),
});
interface AppsPageProps {
searchParams: Promise<inferSearchParamsFromSchema<typeof searchParamsSchema>>;
}
export default async function AppsPage(props: AppsPageProps) {
const session = await auth();
if (!session) {
redirect("/auth/login");
}
const apps = await api.app.all();
const searchParams = searchParamsSchema.parse(await props.searchParams);
const { items: apps, totalCount } = await api.app.getPaginated(searchParams);
const t = await getScopedI18n("app");
return (
<ManageContainer>
<DynamicBreadcrumb />
<Stack>
<Title>{t("page.list.title")}</Title>
<Group justify="space-between" align="center">
<Title>{t("page.list.title")}</Title>
<SearchInput placeholder={`${t("search")}...`} defaultValue={searchParams.search} />
{session.user.permissions.includes("app-create") && (
<MobileAffixButton component={Link} href="/manage/apps/new">
{t("page.create.title")}
@@ -45,6 +60,10 @@ export default async function AppsPage() {
))}
</Stack>
)}
<Group justify="end">
<TablePagination total={Math.ceil(totalCount / searchParams.pageSize)} />
</Group>
</Stack>
</ManageContainer>
);
@@ -82,8 +101,8 @@ const AppCard = async ({ app }: AppCardProps) => {
</Text>
)}
{app.href && (
<Anchor href={parseAppHrefWithVariablesServer(app.href)} lineClamp={1} size="sm" w="min-content">
{parseAppHrefWithVariablesServer(app.href)}
<Anchor href={app.href} lineClamp={1} size="sm" w="min-content">
{app.href}
</Anchor>
)}
</Stack>
@@ -3,12 +3,14 @@
import { useCallback } from "react";
import Link from "next/link";
import { Menu } from "@mantine/core";
import { IconHome, IconSettings, IconTrash } from "@tabler/icons-react";
import { IconCopy, IconDeviceMobile, IconHome, IconSettings, IconTrash } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useConfirmModal } from "@homarr/modals";
import { useConfirmModal, useModalAction } from "@homarr/modals";
import { DuplicateBoardModal } from "@homarr/modals-collection";
import { useScopedI18n } from "@homarr/translation/client";
import { useBoardPermissions } from "~/components/board/permissions/client";
@@ -30,8 +32,10 @@ export const BoardCardMenuDropdown = ({ board }: BoardCardMenuDropdownProps) =>
const tCommon = useScopedI18n("common");
const { hasFullAccess, hasChangeAccess } = useBoardPermissions(board);
const { data: session } = useSession();
const { openConfirmModal } = useConfirmModal();
const { openModal: openDuplicateModal } = useModalAction(DuplicateBoardModal);
const setHomeBoardMutation = clientApi.board.setHomeBoard.useMutation({
onSettled: async () => {
@@ -39,6 +43,12 @@ export const BoardCardMenuDropdown = ({ board }: BoardCardMenuDropdownProps) =>
await revalidatePathActionAsync("/");
},
});
const setMobileHomeBoardMutation = clientApi.board.setMobileHomeBoard.useMutation({
onSettled: async () => {
// Revalidate all as it's part of the user settings, /boards page and board manage page
await revalidatePathActionAsync("/");
},
});
const deleteBoardMutation = clientApi.board.deleteBoard.useMutation({
onSettled: async () => {
await revalidatePathActionAsync("/manage/boards");
@@ -64,11 +74,35 @@ export const BoardCardMenuDropdown = ({ board }: BoardCardMenuDropdownProps) =>
await setHomeBoardMutation.mutateAsync({ id: board.id });
}, [board.id, setHomeBoardMutation]);
const handleSetMobileHomeBoard = useCallback(async () => {
await setMobileHomeBoardMutation.mutateAsync({ id: board.id });
}, [board.id, setMobileHomeBoardMutation]);
const handleDuplicateBoard = useCallback(() => {
openDuplicateModal({
board: {
id: board.id,
name: board.name,
},
onSuccess: async () => {
await revalidatePathActionAsync("/manage/boards");
},
});
}, [board.id, board.name, openDuplicateModal]);
return (
<Menu.Dropdown>
<Menu.Item onClick={handleSetHomeBoard} leftSection={<IconHome {...iconProps} />}>
{t("setHomeBoard.label")}
</Menu.Item>
<Menu.Item onClick={handleSetMobileHomeBoard} leftSection={<IconDeviceMobile {...iconProps} />}>
{t("setMobileHomeBoard.label")}
</Menu.Item>
{session?.user.permissions.includes("board-create") && (
<Menu.Item onClick={handleDuplicateBoard} leftSection={<IconCopy {...iconProps} />}>
{t("duplicate.label")}
</Menu.Item>
)}
{hasChangeAccess && (
<>
<Menu.Divider />
@@ -1,12 +1,11 @@
"use client";
import { Affix, Button, Group, Menu } from "@mantine/core";
import { Affix, Button, Menu } from "@mantine/core";
import { IconCategoryPlus, IconChevronDown, IconFileImport } from "@tabler/icons-react";
import { useModalAction } from "@homarr/modals";
import { AddBoardModal, ImportBoardModal } from "@homarr/modals-collection";
import { useI18n } from "@homarr/translation/client";
import { BetaBadge } from "@homarr/ui";
export const CreateBoardButton = () => {
const t = useI18n();
@@ -26,10 +25,7 @@ export const CreateBoardButton = () => {
</Menu.Target>
<Menu.Dropdown>
<Menu.Item onClick={openImportModal} leftSection={<IconFileImport size="1rem" />}>
<Group>
{t("board.action.oldImport.label")}
<BetaBadge size="xs" />
</Group>
{t("board.action.oldImport.label")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
@@ -15,7 +15,7 @@ import {
Title,
Tooltip,
} from "@mantine/core";
import { IconDotsVertical, IconHomeFilled, IconLock, IconWorld } from "@tabler/icons-react";
import { IconDeviceMobile, IconDotsVertical, IconHomeFilled, IconLock, IconWorld } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
@@ -88,6 +88,14 @@ const BoardCard = async ({ board }: BoardCardProps) => {
</Tooltip>
)}
{board.isMobileHome && (
<Tooltip label={t("action.setMobileHomeBoard.badge.tooltip")}>
<Badge tt="none" color="yellow" variant="light" leftSection={<IconDeviceMobile size=".7rem" />}>
{t("action.setMobileHomeBoard.badge.label")}
</Badge>
</Tooltip>
)}
{board.creator && (
<Group gap="xs">
<UserAvatar user={board.creator} size="sm" />
@@ -1,4 +1,4 @@
import { IconKey, IconPassword, IconUser } from "@tabler/icons-react";
import { IconGrid3x3, IconKey, IconPassword, IconServer, IconUser } from "@tabler/icons-react";
import type { IntegrationSecretKind } from "@homarr/definitions";
import type { TablerIcon } from "@homarr/ui";
@@ -7,4 +7,6 @@ export const integrationSecretIcons = {
username: IconUser,
apiKey: IconKey,
password: IconPassword,
realm: IconServer,
tokenId: IconGrid3x3,
} satisfies Record<IntegrationSecretKind, TablerIcon>;
@@ -11,10 +11,11 @@ import { IntegrationAccessSettings } from "../../_components/integration-access-
import { EditIntegrationForm } from "./_integration-edit-form";
interface EditIntegrationPageProps {
params: { id: string };
params: Promise<{ id: string }>;
}
export default async function EditIntegrationPage({ params }: EditIntegrationPageProps) {
export default async function EditIntegrationPage(props: EditIntegrationPageProps) {
const params = await props.params;
const editT = await getScopedI18n("integration.page.edit");
const t = await getI18n();
const integration = await api.integration.byId({ id: params.id }).catch(catchTrpcNotFound);
@@ -3,13 +3,13 @@
import { useCallback } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Alert, Button, Fieldset, Group, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
import { Alert, Button, Checkbox, Fieldset, Group, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
import { getAllSecretKindOptions, getIntegrationName } from "@homarr/definitions";
import { getAllSecretKindOptions, getIntegrationName, integrationDefs } from "@homarr/definitions";
import type { UseFormReturnType } from "@homarr/form";
import { useZodForm } from "@homarr/form";
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
@@ -38,6 +38,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
kind,
value: "",
})),
attemptSearchEngineCreation: true,
},
});
const { mutateAsync, isPending } = clientApi.integration.create.useMutation();
@@ -78,6 +79,8 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
);
};
const supportsSearchEngine = integrationDefs[searchParams.kind].category.flat().includes("search");
return (
<form onSubmit={form.onSubmit((value) => void handleSubmitAsync(value))}>
<Stack>
@@ -104,6 +107,16 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
</Stack>
</Fieldset>
{supportsSearchEngine && (
<Checkbox
label={t("integration.field.attemptSearchEngineCreation.label")}
description={t("integration.field.attemptSearchEngineCreation.description", {
kind: getIntegrationName(searchParams.kind),
})}
{...form.getInputProps("attemptSearchEngineCreation", { type: "checkbox" })}
/>
)}
<Group justify="end" align="center">
<Button variant="default" component={Link} href="/manage/integrations">
{t("common.action.backToOverview")}
@@ -13,12 +13,15 @@ import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NewIntegrationForm } from "./_integration-new-form";
interface NewIntegrationPageProps {
searchParams: Partial<z.infer<typeof validation.integration.create>> & {
kind: IntegrationKind;
};
searchParams: Promise<
Partial<z.infer<typeof validation.integration.create>> & {
kind: IntegrationKind;
}
>;
}
export default async function IntegrationsNewPage({ searchParams }: NewIntegrationPageProps) {
export default async function IntegrationsNewPage(props: NewIntegrationPageProps) {
const searchParams = await props.searchParams;
const session = await auth();
if (!session?.user.permissions.includes("integration-create")) {
notFound();
@@ -46,12 +46,13 @@ import { DeleteIntegrationActionButton } from "./_integration-buttons";
import { IntegrationCreateDropdownContent } from "./new/_integration-new-dropdown";
interface IntegrationsPageProps {
searchParams: {
searchParams: Promise<{
tab?: IntegrationKind;
};
}>;
}
export default async function IntegrationsPage({ searchParams }: IntegrationsPageProps) {
export default async function IntegrationsPage(props: IntegrationsPageProps) {
const searchParams = await props.searchParams;
const session = await auth();
if (!session) {
@@ -6,6 +6,7 @@ import {
IconBrandDiscord,
IconBrandDocker,
IconBrandGithub,
IconCertificate,
IconGitFork,
IconHome,
IconInfoSmall,
@@ -119,6 +120,12 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
href: "/manage/tools/logs",
hidden: !session?.user.permissions.includes("other-view-logs"),
},
{
label: t("items.tools.items.certificates"),
icon: IconCertificate,
href: "/manage/tools/certificates",
hidden: !session?.user.permissions.includes("admin"),
},
{
label: t("items.tools.items.tasks"),
icon: IconReport,
@@ -13,10 +13,7 @@ interface CopyMediaProps {
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}`
: "";
const url = typeof window !== "undefined" ? `${window.location.origin}/api/user-medias/${media.id}` : "";
return (
<CopyButton value={url}>
@@ -1,15 +1,40 @@
"use client";
import type { JSX } from "react";
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 type { MaybePromise } from "@homarr/common/types";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { supportedMediaUploadFormats } from "@homarr/validation";
export const UploadMedia = () => {
export const UploadMediaButton = () => {
const t = useI18n();
const onSettledAsync = async () => {
await revalidatePathActionAsync("/manage/medias");
};
return (
<UploadMedia onSettled={onSettledAsync}>
{({ onClick, loading }) => (
<Button onClick={onClick} loading={loading} rightSection={<IconUpload size={16} stroke={1.5} />}>
{t("media.action.upload.label")}
</Button>
)}
</UploadMedia>
);
};
interface UploadMediaProps {
children: (props: { onClick: () => void; loading: boolean }) => JSX.Element;
onSettled?: () => MaybePromise<void>;
onSuccess?: (media: { id: string; url: string }) => MaybePromise<void>;
}
export const UploadMedia = ({ children, onSettled, onSuccess }: UploadMediaProps) => {
const t = useI18n();
const { mutateAsync, isPending } = clientApi.media.uploadMedia.useMutation();
@@ -18,10 +43,14 @@ export const UploadMedia = () => {
const formData = new FormData();
formData.append("file", file);
await mutateAsync(formData, {
onSuccess() {
async onSuccess(mediaId) {
showSuccessNotification({
message: t("media.action.upload.notification.success.message"),
});
await onSuccess?.({
id: mediaId,
url: `/api/user-medias/${mediaId}`,
});
},
onError() {
showErrorNotification({
@@ -29,18 +58,14 @@ export const UploadMedia = () => {
});
},
async onSettled() {
await revalidatePathActionAsync("/manage/medias");
await onSettled?.();
},
});
};
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>
)}
{({ onClick }) => children({ onClick, loading: isPending })}
</FileButton>
);
};
@@ -7,6 +7,7 @@ import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { humanFileSize } from "@homarr/common";
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { getI18n } from "@homarr/translation/server";
import { SearchInput, TablePagination, UserAvatar } from "@homarr/ui";
import { z } from "@homarr/validation";
@@ -16,7 +17,7 @@ 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";
import { UploadMediaButton } from "./_actions/upload-media";
const searchParamsSchema = z.object({
search: z.string().optional(),
@@ -29,12 +30,8 @@ const searchParamsSchema = z.object({
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>>;
searchParams: Promise<inferSearchParamsFromSchema<typeof searchParamsSchema>>;
}
export default async function GroupsListPage(props: MediaListPageProps) {
@@ -45,7 +42,7 @@ export default async function GroupsListPage(props: MediaListPageProps) {
}
const t = await getI18n();
const searchParams = searchParamsSchema.parse(props.searchParams);
const searchParams = searchParamsSchema.parse(await props.searchParams);
const { items: medias, totalCount } = await api.media.getPaginated(searchParams);
return (
@@ -61,7 +58,7 @@ export default async function GroupsListPage(props: MediaListPageProps) {
)}
</Group>
{session.user.permissions.includes("media-upload") && <UploadMedia />}
{session.user.permissions.includes("media-upload") && <UploadMediaButton />}
</Group>
<Table striped highlightOnHover>
<TableThead>
@@ -10,10 +10,11 @@ import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { SearchEngineEditForm } from "./_search-engine-edit-form";
interface SearchEngineEditPageProps {
params: { id: string };
params: Promise<{ id: string }>;
}
export default async function SearchEngineEditPage({ params }: SearchEngineEditPageProps) {
export default async function SearchEngineEditPage(props: SearchEngineEditPageProps) {
const params = await props.params;
const session = await auth();
if (!session?.user.permissions.includes("search-engine-modify-all")) {
@@ -6,6 +6,7 @@ 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 type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { SearchInput, TablePagination } from "@homarr/ui";
import { z } from "@homarr/validation";
@@ -22,12 +23,8 @@ const searchParamsSchema = z.object({
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 SearchEnginesPageProps {
searchParams: SearchParamsSchemaInputFromSchema<z.infer<typeof searchParamsSchema>>;
searchParams: Promise<inferSearchParamsFromSchema<typeof searchParamsSchema>>;
}
export default async function SearchEnginesPage(props: SearchEnginesPageProps) {
@@ -37,7 +34,7 @@ export default async function SearchEnginesPage(props: SearchEnginesPageProps) {
redirect("/auth/login");
}
const searchParams = searchParamsSchema.parse(props.searchParams);
const searchParams = searchParamsSchema.parse(await props.searchParams);
const { items: searchEngines, totalCount } = await api.searchEngine.getPaginated(searchParams);
const tEngine = await getScopedI18n("search.engine");
@@ -37,6 +37,25 @@ export const BoardSettingsForm = ({ defaultValues }: { defaultValues: ServerSett
)}
{...form.getInputProps("homeBoardId")}
/>
<SelectWithCustomItems
label={tBoard("homeBoard.mobileLabel")}
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("mobileHomeBoardId")}
/>
</>
)}
</CommonSettingsForm>
@@ -0,0 +1,29 @@
"use client";
import { Select } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import type { ServerSettings } from "@homarr/server-settings";
import { useScopedI18n } from "@homarr/translation/client";
import { CommonSettingsForm } from "./common-form";
export const SearchSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["search"] }) => {
const tSearch = useScopedI18n("management.page.settings.section.search");
const [selectableSearchEngines] = clientApi.searchEngine.getSelectable.useSuspenseQuery({ withIntegrations: false });
return (
<CommonSettingsForm settingKey="search" defaultValues={defaultValues}>
{(form) => (
<>
<Select
label={tSearch("defaultSearchEngine.label")}
description={tSearch("defaultSearchEngine.description")}
data={selectableSearchEngines}
{...form.getInputProps("defaultSearchEngineId")}
/>
</>
)}
</CommonSettingsForm>
);
};
@@ -11,6 +11,7 @@ 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";
import { SearchSettingsForm } from "./_components/search-settings-form";
export async function generateMetadata() {
const t = await getScopedI18n("management");
@@ -41,6 +42,10 @@ export default async function SettingsPage() {
<Title order={2}>{tSettings("section.board.title")}</Title>
<BoardSettingsForm defaultValues={serverSettings.board} />
</Stack>
<Stack>
<Title order={2}>{tSettings("section.search.title")}</Title>
<SearchSettingsForm defaultValues={serverSettings.search} />
</Stack>
<Stack>
<Title order={2}>{tSettings("section.appearance.title")}</Title>
<AppearanceSettingsForm defaultValues={serverSettings.appearance} />
@@ -30,7 +30,7 @@ export default async function ApiPage() {
if (!session?.user || !session.user.permissions.includes("admin")) {
notFound();
}
const document = openApiDocument(extractBaseUrlFromHeaders(headers()));
const document = openApiDocument(extractBaseUrlFromHeaders(await headers()));
const apiKeys = await api.apiKeys.getAll();
const t = await getScopedI18n("management.page.tool.api.tab");
@@ -0,0 +1,81 @@
"use client";
import { Button, FileInput, Group, Stack } from "@mantine/core";
import { IconCertificate } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { createModal, useModalAction } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { superRefineCertificateFile, z } from "@homarr/validation";
export const AddCertificateButton = () => {
const { openModal } = useModalAction(AddCertificateModal);
const t = useI18n();
const handleClick = () => {
openModal({});
};
return <Button onClick={handleClick}>{t("certificate.action.create.label")}</Button>;
};
const AddCertificateModal = createModal(({ actions }) => {
const t = useI18n();
const form = useZodForm(
z.object({
file: z.instanceof(File).nullable().superRefine(superRefineCertificateFile),
}),
{
initialValues: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
file: null!,
},
},
);
const { mutateAsync } = clientApi.certificates.addCertificate.useMutation();
return (
<form
onSubmit={form.onSubmit(async (values) => {
const formData = new FormData();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
formData.set("file", values.file!);
await mutateAsync(formData, {
async onSuccess() {
showSuccessNotification({
title: t("certificate.action.create.notification.success.title"),
message: t("certificate.action.create.notification.success.message"),
});
await revalidatePathActionAsync("/manage/tools/certificates");
actions.closeModal();
},
onError() {
showErrorNotification({
title: t("certificate.action.create.notification.error.title"),
message: t("certificate.action.create.notification.error.message"),
});
},
});
})}
>
<Stack>
<FileInput leftSection={<IconCertificate size={16} />} {...form.getInputProps("file")} />
<Group justify="end">
<Button onClick={actions.closeModal} variant="subtle" color="gray">
{t("common.action.cancel")}
</Button>
<Button type="submit" loading={form.submitting}>
{t("common.action.add")}
</Button>
</Group>
</Stack>
</form>
);
}).withOptions({
defaultTitle(t) {
return t("certificate.action.create.label");
},
});
@@ -0,0 +1,54 @@
"use client";
import { ActionIcon } from "@mantine/core";
import { IconTrash } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useConfirmModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
interface RemoveCertificateProps {
fileName: string;
}
export const RemoveCertificate = ({ fileName }: RemoveCertificateProps) => {
const { openConfirmModal } = useConfirmModal();
const { mutateAsync } = clientApi.certificates.removeCertificate.useMutation();
const t = useI18n();
const handleClick = () => {
openConfirmModal({
title: t("certificate.action.remove.label"),
children: t("certificate.action.remove.confirm"),
// eslint-disable-next-line no-restricted-syntax
async onConfirm() {
await mutateAsync(
{ fileName },
{
async onSuccess() {
showSuccessNotification({
title: t("certificate.action.remove.notification.success.title"),
message: t("certificate.action.remove.notification.success.message"),
});
await revalidatePathActionAsync("/manage/tools/certificates");
},
onError() {
showErrorNotification({
title: t("certificate.action.remove.notification.error.title"),
message: t("certificate.action.remove.notification.error.message"),
});
},
},
);
},
});
};
return (
<ActionIcon onClick={handleClick} color="red" variant="subtle">
<IconTrash color="red" size={16} />
</ActionIcon>
);
};
@@ -0,0 +1,97 @@
import { X509Certificate } from "node:crypto";
import { notFound } from "next/navigation";
import { Card, Group, SimpleGrid, Stack, Text, Title } from "@mantine/core";
import { IconCertificate, IconCertificateOff } from "@tabler/icons-react";
import dayjs from "dayjs";
import { auth } from "@homarr/auth/next";
import { loadCustomRootCertificatesAsync } from "@homarr/certificates/server";
import { getMantineColor } from "@homarr/common";
import type { SupportedLanguage } from "@homarr/translation";
import { getI18n } from "@homarr/translation/server";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NoResults } from "~/components/no-results";
import { AddCertificateButton } from "./_components/add-certificate";
import { RemoveCertificate } from "./_components/remove-certificate";
interface CertificatesPageProps {
params: Promise<{
locale: SupportedLanguage;
}>;
}
export default async function CertificatesPage({ params }: CertificatesPageProps) {
const session = await auth();
if (!session?.user.permissions.includes("admin")) {
notFound();
}
const { locale } = await params;
const t = await getI18n();
const certificates = await loadCustomRootCertificatesAsync();
const x509Certificates = certificates
.map((cert) => ({
...cert,
x509: new X509Certificate(cert.content),
}))
.sort((certA, certB) => certA.x509.validToDate.getTime() - certB.x509.validToDate.getTime());
return (
<>
<DynamicBreadcrumb />
<Stack>
<Group justify="space-between">
<Stack gap={4}>
<Title>{t("certificate.page.list.title")}</Title>
<Text>{t("certificate.page.list.description")}</Text>
</Stack>
<AddCertificateButton />
</Group>
{x509Certificates.length === 0 && (
<NoResults icon={IconCertificateOff} title={t("certificate.page.list.noResults.title")} />
)}
<SimpleGrid cols={{ sm: 1, lg: 2, xl: 3 }} spacing="lg">
{x509Certificates.map((cert) => (
<Card key={cert.x509.fingerprint} withBorder>
<Group wrap="nowrap">
<IconCertificate color={getMantineColor(iconColor(cert.x509.validToDate), 6)} size={32} stroke={1.5} />
<Stack flex={1} gap="xs">
<Group justify="space-between">
<Text fw={500}>{cert.x509.subject}</Text>
<Text c="gray.6" ta="end" size="sm">
{cert.fileName}
</Text>
</Group>
<Group justify="space-between">
<Text size="sm" c="gray.6" title={cert.x509.validToDate.toISOString()}>
{t("certificate.page.list.expires", {
when: new Intl.RelativeTimeFormat(locale).format(
dayjs(cert.x509.validToDate).diff(dayjs(), "days"),
"days",
),
})}
</Text>
<RemoveCertificate fileName={cert.fileName} />
</Group>
</Stack>
</Group>
</Card>
))}
</SimpleGrid>
</Stack>
</>
);
}
const iconColor = (validTo: Date) => {
const daysUntilInvalid = dayjs(validTo).diff(new Date(), "days");
if (daysUntilInvalid < 1) return "red";
if (daysUntilInvalid < 7) return "orange";
if (daysUntilInvalid < 30) return "yellow";
return "green";
};
@@ -2,7 +2,14 @@
import type { MantineColor } from "@mantine/core";
import { Avatar, Badge, Box, Button, Group, Text } from "@mantine/core";
import { IconPlayerPlay, IconPlayerStop, IconRefresh, IconRotateClockwise, IconTrash } from "@tabler/icons-react";
import {
IconCategoryPlus,
IconPlayerPlay,
IconPlayerStop,
IconRefresh,
IconRotateClockwise,
IconTrash,
} from "@tabler/icons-react";
import type { MRT_ColumnDef } from "mantine-react-table";
import { MantineReactTable } from "mantine-react-table";
@@ -10,6 +17,8 @@ import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useTimeAgo } from "@homarr/common";
import type { DockerContainerState } from "@homarr/definitions";
import { useModalAction } from "@homarr/modals";
import { AddDockerAppToHomarr } from "@homarr/modals-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { TranslationFunction } from "@homarr/translation";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
@@ -125,6 +134,7 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers"
);
},
renderToolbarAlertBannerContent: ({ groupedAlert, table }) => {
const dockerContainers = table.getSelectedRowModel().rows.map((row) => row.original);
return (
<Group gap={"sm"}>
{groupedAlert}
@@ -134,7 +144,7 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers"
totalCount: table.getRowCount(),
})}
</Text>
<ContainerActionBar selectedIds={table.getSelectedRowModel().rows.map((row) => row.original.id)} />
<ContainerActionBar selectedContainers={dockerContainers} />
</Group>
);
},
@@ -151,16 +161,29 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers"
}
interface ContainerActionBarProps {
selectedIds: string[];
selectedContainers: RouterOutputs["docker"]["getContainers"]["containers"];
}
const ContainerActionBar = ({ selectedIds }: ContainerActionBarProps) => {
const ContainerActionBar = ({ selectedContainers }: ContainerActionBarProps) => {
const t = useScopedI18n("docker.action");
const { openModal } = useModalAction(AddDockerAppToHomarr);
const handleClick = () => {
openModal({
selectedContainers,
});
};
const selectedIds = selectedContainers.map((container) => container.id);
return (
<Group gap="xs">
<ContainerActionBarButton icon={IconPlayerPlay} color="green" action="start" selectedIds={selectedIds} />
<ContainerActionBarButton icon={IconPlayerStop} color="red" action="stop" selectedIds={selectedIds} />
<ContainerActionBarButton icon={IconRotateClockwise} color="orange" action="restart" selectedIds={selectedIds} />
<ContainerActionBarButton icon={IconTrash} color="red" action="remove" selectedIds={selectedIds} />
<Button leftSection={<IconCategoryPlus />} color={"red"} onClick={handleClick} variant="light" radius="md">
{t("addToHomarr.label")}
</Button>
</Group>
);
};
@@ -174,9 +197,10 @@ interface ContainerActionBarButtonProps {
const ContainerActionBarButton = (props: ContainerActionBarButtonProps) => {
const t = useScopedI18n("docker.action");
const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation();
const utils = clientApi.useUtils();
const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation();
const handleClickAsync = async () => {
await mutateAsync(
{ ids: props.selectedIds },
@@ -0,0 +1,67 @@
"use client";
import { Button, Group, Select, Stack } from "@mantine/core";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
interface ChangeDefaultSearchEngineFormProps {
user: RouterOutputs["user"]["getById"];
searchEnginesData: { value: string; label: string }[];
}
export const ChangeDefaultSearchEngineForm = ({ user, searchEnginesData }: ChangeDefaultSearchEngineFormProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changeDefaultSearchEngine.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
defaultSearchEngineId: variables.defaultSearchEngineId,
});
showSuccessNotification({
message: t("user.action.changeDefaultSearchEngine.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("user.action.changeDefaultSearchEngine.notification.error.message"),
});
},
});
const form = useZodForm(validation.user.changeDefaultSearchEngine, {
initialValues: {
defaultSearchEngineId: user.defaultSearchEngineId ?? "",
},
});
const handleSubmit = (values: FormType) => {
mutate({
userId: user.id,
...values,
});
};
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Select w="100%" data={searchEnginesData} {...form.getInputProps("defaultSearchEngineId")} />
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};
type FormType = z.infer<typeof validation.user.changeDefaultSearchEngine>;
@@ -18,13 +18,14 @@ interface ChangeHomeBoardFormProps {
export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changeHomeBoardId.useMutation({
const { mutate, isPending } = clientApi.user.changeHomeBoards.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
homeBoardId: variables.homeBoardId,
mobileHomeBoardId: variables.mobileHomeBoardId,
});
showSuccessNotification({
message: t("user.action.changeHomeBoard.notification.success.message"),
@@ -36,9 +37,10 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
});
},
});
const form = useZodForm(validation.user.changeHomeBoard, {
const form = useZodForm(validation.user.changeHomeBoards, {
initialValues: {
homeBoardId: user.homeBoardId ?? "",
mobileHomeBoardId: user.mobileHomeBoardId ?? "",
},
});
@@ -52,7 +54,18 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Select w="100%" data={boardsData} {...form.getInputProps("homeBoardId")} />
<Select
label={t("management.page.user.setting.general.item.board.type.general")}
w="100%"
data={boardsData}
{...form.getInputProps("homeBoardId")}
/>
<Select
label={t("management.page.user.setting.general.item.board.type.mobile")}
w="100%"
data={boardsData}
{...form.getInputProps("mobileHomeBoardId")}
/>
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
@@ -64,4 +77,4 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
);
};
type FormType = z.infer<typeof validation.user.changeHomeBoard>;
type FormType = z.infer<typeof validation.user.changeHomeBoards>;
@@ -11,6 +11,7 @@ import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone"
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
import { createMetaTitle } from "~/metadata";
import { canAccessUserEditPage } from "../access";
import { ChangeDefaultSearchEngineForm } from "./_components/_change-default-search-engine";
import { ChangeHomeBoardForm } from "./_components/_change-home-board";
import { DeleteUserButton } from "./_components/_delete-user-button";
import { FirstDayOfWeek } from "./_components/_first-day-of-week";
@@ -19,12 +20,13 @@ import { UserProfileAvatarForm } from "./_components/_profile-avatar-form";
import { UserProfileForm } from "./_components/_profile-form";
interface Props {
params: {
params: Promise<{
userId: string;
};
}>;
}
export async function generateMetadata({ params }: Props) {
export async function generateMetadata(props: Props) {
const params = await props.params;
const session = await auth();
const user = await api.user
.getById({
@@ -43,7 +45,8 @@ export async function generateMetadata({ params }: Props) {
};
}
export default async function EditUserPage({ params }: Props) {
export default async function EditUserPage(props: Props) {
const params = await props.params;
const t = await getI18n();
const tGeneral = await getScopedI18n("management.page.user.setting.general");
const session = await auth();
@@ -58,6 +61,7 @@ export default async function EditUserPage({ params }: Props) {
}
const boards = await api.board.getAllBoards();
const searchEngines = await api.searchEngine.getSelectable();
const isCredentialsUser = user.provider === "credentials";
@@ -85,7 +89,7 @@ export default async function EditUserPage({ params }: Props) {
</Stack>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.board")}</Title>
<Title order={2}>{tGeneral("item.board.title")}</Title>
<ChangeHomeBoardForm
user={user}
boardsData={boards.map((board) => ({
@@ -95,6 +99,11 @@ export default async function EditUserPage({ params }: Props) {
/>
</Stack>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.defaultSearchEngine")}</Title>
<ChangeDefaultSearchEngineForm user={user} searchEnginesData={searchEngines} />
</Stack>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.firstDayOfWeek")}</Title>
<FirstDayOfWeek user={user} />
@@ -15,10 +15,14 @@ import { NavigationLink } from "../groups/[id]/_navigation";
import { canAccessUserEditPage } from "./access";
interface LayoutProps {
params: { userId: string };
params: Promise<{ userId: string }>;
}
export default async function Layout({ children, params }: PropsWithChildren<LayoutProps>) {
export default async function Layout(props: PropsWithChildren<LayoutProps>) {
const params = await props.params;
const { children } = props;
const session = await auth();
const t = await getI18n();
const tUser = await getScopedI18n("management.page.user");
@@ -10,12 +10,13 @@ import { canAccessUserEditPage } from "../access";
import { ChangePasswordForm } from "./_components/_change-password-form";
interface Props {
params: {
params: Promise<{
userId: string;
};
}>;
}
export default async function UserSecurityPage({ params }: Props) {
export default async function UserSecurityPage(props: Props) {
const params = await props.params;
const session = await auth();
const tSecurity = await getScopedI18n("management.page.user.setting.security");
const user = await api.user
@@ -3,7 +3,7 @@ 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 { groups } from "@homarr/db/schema";
import { everyoneGroup } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
@@ -10,10 +10,14 @@ import { ManageContainer } from "~/components/manage/manage-container";
import { NavigationLink } from "./_navigation";
interface LayoutProps {
params: { id: string };
params: Promise<{ id: string }>;
}
export default async function Layout({ children, params }: PropsWithChildren<LayoutProps>) {
export default async function Layout(props: PropsWithChildren<LayoutProps>) {
const params = await props.params;
const { children } = props;
const t = await getI18n();
const tGroup = await getScopedI18n("management.page.group");
const group = await api.group.getById({ id: params.id });
@@ -5,7 +5,7 @@ import { IconExclamationCircle } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
import { env } from "@homarr/auth/env.mjs";
import { env } from "@homarr/auth/env";
import { auth } from "@homarr/auth/next";
import { isProviderEnabled } from "@homarr/auth/server";
import { everyoneGroup } from "@homarr/definitions";
@@ -17,15 +17,17 @@ import { AddGroupMember } from "./_add-group-member";
import { RemoveGroupMember } from "./_remove-group-member";
interface GroupsDetailPageProps {
params: {
params: Promise<{
id: string;
};
searchParams: {
}>;
searchParams: Promise<{
search: string | undefined;
};
}>;
}
export default async function GroupsDetailPage({ params, searchParams }: GroupsDetailPageProps) {
export default async function GroupsDetailPage(props: GroupsDetailPageProps) {
const searchParams = await props.searchParams;
const params = await props.params;
const session = await auth();
if (!session?.user.permissions.includes("admin")) {
@@ -14,12 +14,13 @@ import { ReservedGroupAlert } from "./_reserved-group-alert";
import { TransferGroupOwnership } from "./_transfer-group-ownership";
interface GroupsDetailPageProps {
params: {
params: Promise<{
id: string;
};
}>;
}
export default async function GroupsDetailPage({ params }: GroupsDetailPageProps) {
export default async function GroupsDetailPage(props: GroupsDetailPageProps) {
const params = await props.params;
const session = await auth();
if (!session?.user.permissions.includes("admin")) {
@@ -12,12 +12,13 @@ import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { PermissionForm, PermissionSwitch, SaveAffix } from "./_group-permission-form";
interface GroupPermissionsPageProps {
params: {
params: Promise<{
id: string;
};
}>;
}
export default async function GroupPermissionsPage({ params }: GroupPermissionsPageProps) {
export default async function GroupPermissionsPage(props: GroupPermissionsPageProps) {
const params = await props.params;
const session = await auth();
if (!session?.user.permissions.includes("admin")) {
@@ -5,6 +5,7 @@ import { Anchor, Group, Stack, Table, TableTbody, TableTd, TableTh, TableThead,
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { getI18n } from "@homarr/translation/server";
import { SearchInput, TablePagination, UserAvatarGroup } from "@homarr/ui";
import { z } from "@homarr/validation";
@@ -19,12 +20,8 @@ const searchParamsSchema = z.object({
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 GroupsListPageProps {
searchParams: SearchParamsSchemaInputFromSchema<z.infer<typeof searchParamsSchema>>;
searchParams: Promise<inferSearchParamsFromSchema<typeof searchParamsSchema>>;
}
export default async function GroupsListPage(props: GroupsListPageProps) {
@@ -35,7 +32,7 @@ export default async function GroupsListPage(props: GroupsListPageProps) {
}
const t = await getI18n();
const searchParams = searchParamsSchema.parse(props.searchParams);
const searchParams = searchParamsSchema.parse(await props.searchParams);
const { items: groups, totalCount } = await api.group.getPaginated(searchParams);
return (
@@ -5,15 +5,15 @@ import { db } from "@homarr/db";
import type { WidgetKind } from "@homarr/definitions";
import { widgetImports } from "@homarr/widgets";
import { env } from "~/env.mjs";
import { env } from "~/env";
import { WidgetPreviewPageContent } from "./_content";
interface Props {
params: { kind: string };
params: Promise<{ kind: string }>;
}
export default async function WidgetPreview(props: Props) {
if (!(props.params.kind in widgetImports || env.NODE_ENV !== "development")) {
if (!((await props.params).kind in widgetImports || env.NODE_ENV !== "development")) {
notFound();
}
@@ -26,7 +26,7 @@ export default async function WidgetPreview(props: Props) {
},
});
const sort = props.params.kind as WidgetKind;
const sort = (await props.params).kind as WidgetKind;
return (
<Center h="100vh">
+6 -7
View File
@@ -1,19 +1,18 @@
import { headers } from "next/headers";
import { userAgent } from "next/server";
import type { NextRequest } from "next/server";
import { userAgent } 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 { hashPasswordAsync } from "@homarr/auth";
import { createSessionAsync } from "@homarr/auth/server";
import { db, eq } from "@homarr/db";
import { apiKeys } from "@homarr/db/schema/sqlite";
import { apiKeys } from "@homarr/db/schema";
import { logger } from "@homarr/log";
const handlerAsync = async (req: NextRequest) => {
const apiKeyHeaderValue = req.headers.get("ApiKey");
const ipAddress = req.ip ?? headers().get("x-forwarded-for");
const ipAddress = req.headers.get("x-forwarded-for");
const { ua } = userAgent(req);
const session: Session | null = await getSessionOrDefaultFromHeadersAsync(apiKeyHeaderValue, ipAddress, ua);
@@ -88,9 +87,9 @@ const getSessionOrDefaultFromHeadersAsync = async (
};
export {
handlerAsync as DELETE,
handlerAsync as GET,
handlerAsync as PATCH,
handlerAsync as POST,
handlerAsync as PUT,
handlerAsync as DELETE,
handlerAsync as PATCH,
};
@@ -1,16 +1,17 @@
import { NextRequest } from "next/server";
import { createHandlers } from "@homarr/auth";
import { createHandlersAsync } from "@homarr/auth";
import type { SupportedAuthProvider } from "@homarr/definitions";
import { logger } from "@homarr/log";
export const GET = async (req: NextRequest) => {
return await createHandlers(extractProvider(req), isSecureCookieEnabled(req)).handlers.GET(reqWithTrustedOrigin(req));
const { handlers } = await createHandlersAsync(extractProvider(req), isSecureCookieEnabled(req));
return await handlers.GET(reqWithTrustedOrigin(req));
};
export const POST = async (req: NextRequest) => {
return await createHandlers(extractProvider(req), isSecureCookieEnabled(req)).handlers.POST(
reqWithTrustedOrigin(req),
);
const { handlers } = await createHandlersAsync(extractProvider(req), isSecureCookieEnabled(req));
return await handlers.POST(reqWithTrustedOrigin(req));
};
/**
@@ -3,9 +3,10 @@ import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { db, eq } from "@homarr/db";
import { medias } from "@homarr/db/schema/sqlite";
import { medias } from "@homarr/db/schema";
export async function GET(_req: NextRequest, { params }: { params: { id: string } }) {
export async function GET(_req: NextRequest, props: { params: Promise<{ id: string }> }) {
const params = await props.params;
const image = await db.query.medias.findFirst({
where: eq(medias.id, params.id),
columns: {
@@ -36,6 +36,7 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
root: {
"--opacity": board.opacity / 100,
containerType: "size",
overflow: item.kind === "iframe" ? "hidden" : undefined,
},
}}
p={0}
@@ -87,7 +88,14 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
isEditMode={isEditMode}
boardId={board.id}
itemId={item.id}
setOptions={updateOptions}
setOptions={(partialNewOptions) =>
updateOptions({
newOptions: {
...partialNewOptions.newOptions,
...options,
},
})
}
{...dimensions}
/>
</ErrorBoundary>
@@ -1,21 +1,67 @@
import { Button, Card, Center, Grid, Stack, Text } from "@mantine/core";
import { useMemo, useState } from "react";
import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
import { objectEntries } from "@homarr/common";
import type { WidgetKind } from "@homarr/definitions";
import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui";
import { widgetImports } from "@homarr/widgets";
import type { WidgetDefinition } from "@homarr/widgets";
import { useItemActions } from "./item-actions";
export const ItemSelectModal = createModal<void>(({ actions }) => {
const [search, setSearch] = useState("");
const t = useI18n();
const { createItem } = useItemActions();
const items = useMemo(
() =>
objectEntries(widgetImports)
.map(([kind, value]) => ({
kind,
icon: value.definition.icon,
name: t(`widget.${kind}.name`),
description: t(`widget.${kind}.description`),
}))
.sort((itemA, itemB) => itemA.name.localeCompare(itemB.name)),
[t],
);
const filteredItems = useMemo(
() => items.filter((item) => item.name.toLowerCase().includes(search.toLowerCase())),
[items, search],
);
const handleAdd = (kind: WidgetKind) => {
createItem({ kind });
actions.closeModal();
};
return (
<Grid>
{objectEntries(widgetImports).map(([key, value]) => {
return <WidgetItem key={key} kind={key} definition={value.definition} closeModal={actions.closeModal} />;
})}
</Grid>
<Stack>
<Input
value={search}
onChange={(event) => setSearch(event.currentTarget.value)}
leftSection={<IconSearch />}
placeholder={`${t("item.create.search")}...`}
data-autofocus
onKeyDown={(event) => {
// Add item if there is only one item in the list and user presses Enter
if (event.key === "Enter" && filteredItems.length === 1) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
handleAdd(filteredItems[0]!.kind);
}
}}
/>
<Grid>
{filteredItems.map((item) => (
<WidgetItem key={item.kind} item={item} onSelect={() => handleAdd(item.kind)} />
))}
</Grid>
</Stack>
);
}).withOptions({
defaultTitle: (t) => t("item.create.title"),
@@ -23,20 +69,18 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
});
const WidgetItem = ({
kind,
definition,
closeModal,
item,
onSelect,
}: {
kind: WidgetKind;
definition: WidgetDefinition;
closeModal: () => void;
item: {
kind: WidgetKind;
name: string;
description: string;
icon: TablerIcon;
};
onSelect: () => void;
}) => {
const t = useI18n();
const { createItem } = useItemActions();
const handleAdd = (kind: WidgetKind) => {
createItem({ kind });
closeModal();
};
return (
<Grid.Col span={{ xs: 12, sm: 4, md: 3 }}>
@@ -44,25 +88,16 @@ const WidgetItem = ({
<Stack justify="space-between" h="100%">
<Stack gap="xs">
<Center>
<definition.icon />
<item.icon />
</Center>
<Text lh={1.2} style={{ whiteSpace: "normal" }} ta="center">
{t(`widget.${kind}.name`)}
{item.name}
</Text>
<Text lh={1.2} style={{ whiteSpace: "normal" }} size="xs" ta="center" c="dimmed">
{t(`widget.${kind}.description`)}
{item.description}
</Text>
</Stack>
<Button
onClick={() => {
handleAdd(kind);
}}
variant="light"
size="xs"
mt="auto"
radius="md"
fullWidth
>
<Button onClick={onSelect} variant="light" size="xs" mt="auto" radius="md" fullWidth>
{t(`item.create.addToBoard`)}
</Button>
</Stack>
@@ -0,0 +1,111 @@
import type { Board, CategorySection, DynamicSection, EmptySection, Section } from "~/app/[locale]/boards/_types";
export interface RemoveCategoryInput {
id: string;
}
export const removeCategoryCallback =
(input: RemoveCategoryInput) =>
(previous: Board): Board => {
const currentCategory = previous.sections.find(
(section): section is CategorySection => section.kind === "category" && section.id === input.id,
);
if (!currentCategory) {
return previous;
}
const emptySectionsAbove = previous.sections.filter(
(section): section is EmptySection => section.kind === "empty" && section.yOffset < currentCategory.yOffset,
);
const aboveSection = emptySectionsAbove.sort((sectionA, sectionB) => sectionB.yOffset - sectionA.yOffset).at(0);
const emptySectionsBelow = previous.sections.filter(
(section): section is EmptySection => section.kind === "empty" && section.yOffset > currentCategory.yOffset,
);
const removedSection = emptySectionsBelow.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset).at(0);
if (!aboveSection || !removedSection) {
return previous;
}
// Calculate the yOffset for the items in the currentCategory and removedWrapper to add them with the same offset to the aboveWrapper
const aboveYOffset = Math.max(
calculateYHeightWithOffsetForItems(aboveSection),
calculateYHeightWithOffsetForDynamicSections(previous.sections, aboveSection.id),
);
const categoryYOffset = Math.max(
calculateYHeightWithOffsetForItems(currentCategory),
calculateYHeightWithOffsetForDynamicSections(previous.sections, currentCategory.id),
);
const previousCategoryItems = currentCategory.items.map((item) => ({
...item,
yOffset: item.yOffset + aboveYOffset,
}));
const previousBelowWrapperItems = removedSection.items.map((item) => ({
...item,
yOffset: item.yOffset + aboveYOffset + categoryYOffset,
}));
return {
...previous,
sections: [
...previous.sections.filter((section) => section.yOffset < aboveSection.yOffset && section.kind !== "dynamic"),
{
...aboveSection,
items: [...aboveSection.items, ...previousCategoryItems, ...previousBelowWrapperItems],
},
...previous.sections
.filter(
(section): section is CategorySection | EmptySection =>
section.yOffset > removedSection.yOffset && section.kind !== "dynamic",
)
.map((section) => ({
...section,
position: section.yOffset - 2,
})),
...previous.sections
.filter((section): section is DynamicSection => section.kind === "dynamic")
.map((dynamicSection) => {
// Move dynamic sections from removed section to above section with required yOffset
if (dynamicSection.parentSectionId === removedSection.id) {
return {
...dynamicSection,
yOffset: dynamicSection.yOffset + aboveYOffset + categoryYOffset,
parentSectionId: aboveSection.id,
};
}
// Move dynamic sections from category to above section with required yOffset
if (dynamicSection.parentSectionId === currentCategory.id) {
return {
...dynamicSection,
yOffset: dynamicSection.yOffset + aboveYOffset,
parentSectionId: aboveSection.id,
};
}
return dynamicSection;
}),
],
};
};
const calculateYHeightWithOffsetForDynamicSections = (sections: Section[], sectionId: string) => {
return sections.reduce((acc, section) => {
if (section.kind !== "dynamic" || section.parentSectionId !== sectionId) {
return acc;
}
const yHeightWithOffset = section.yOffset + section.height;
if (yHeightWithOffset > acc) return yHeightWithOffset;
return acc;
}, 0);
};
const calculateYHeightWithOffsetForItems = (section: Section) =>
section.items.reduce((acc, item) => {
const yHeightWithOffset = item.yOffset + item.height;
if (yHeightWithOffset > acc) return yHeightWithOffset;
return acc;
}, 0);
@@ -0,0 +1,125 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { describe, expect, test } from "vitest";
import type { DynamicSection, Item, Section } from "~/app/[locale]/boards/_types";
import { removeCategoryCallback } from "../remove-category";
describe("Remove Category", () => {
test.each([
[3, [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 5, 6], [3, 4], 2],
[5, [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4], [5, 6], 4],
[1, [0, 1, 2, 3, 4, 5, 6], [0, 3, 4, 5, 6], [1, 2], 0],
[3, [0, 3, 6, 7, 8], [0, 7, 8], [3, 6], 0],
])(
"should remove category",
(removeId, initialYOffsets, expectedYOffsets, expectedRemovals, expectedLocationOfItems) => {
const sections = createSections(initialYOffsets);
const input = removeId.toString();
const result = removeCategoryCallback({ id: input })({ sections } as never);
expect(result.sections.map((section) => parseInt(section.id, 10))).toEqual(expectedYOffsets);
expectedRemovals.forEach((expectedRemoval) => {
expect(result.sections.find((section) => section.id === expectedRemoval.toString())).toBeUndefined();
});
const aboveSection = result.sections.find((section) => section.id === expectedLocationOfItems.toString());
expect(aboveSection?.items.map((item) => parseInt(item.id, 10))).toEqual(
expect.arrayContaining(expectedRemovals),
);
},
);
test("should correctly move items to above empty section", () => {
const initialYOffsets = [0, 1, 2, 3, 4, 5, 6];
const sections: Section[] = createSections(initialYOffsets);
const aboveSection = sections.find((section) => section.yOffset === 2)!;
aboveSection.items = [
createItem({ id: "above-1" }),
createItem({ id: "above-2", yOffset: 3, xOffset: 2, height: 2 }),
];
const removedCategory = sections.find((section) => section.yOffset === 3)!;
removedCategory.items = [
createItem({ id: "category-1" }),
createItem({ id: "category-2", yOffset: 2, xOffset: 4, width: 4 }),
];
const removedEmptySection = sections.find((section) => section.yOffset === 4)!;
removedEmptySection.items = [
createItem({ id: "below-1", xOffset: 5 }),
createItem({ id: "below-2", yOffset: 1, xOffset: 1, height: 2 }),
];
sections.push(
createDynamicSection({
id: "7",
parentSectionId: "3",
yOffset: 7,
height: 3,
items: [createItem({ id: "dynamic-1" })],
}),
);
const input = "3";
const result = removeCategoryCallback({ id: input })({ sections } as never);
expect(result.sections.map((section) => parseInt(section.id, 10))).toEqual([0, 1, 2, 5, 6, 7]);
const aboveSectionResult = result.sections.find((section) => section.id === "2")!;
expect(aboveSectionResult.items).toEqual(
expect.arrayContaining([
createItem({ id: "above-1" }),
createItem({ id: "above-2", yOffset: 3, xOffset: 2, height: 2 }),
createItem({ id: "category-1", yOffset: 5 }),
createItem({ id: "category-2", yOffset: 7, xOffset: 4, width: 4 }),
createItem({ id: "below-1", yOffset: 15, xOffset: 5 }),
createItem({ id: "below-2", yOffset: 16, xOffset: 1, height: 2 }),
]),
);
const dynamicSection = result.sections.find((section): section is DynamicSection => section.id === "7")!;
expect(dynamicSection.yOffset).toBe(12);
expect(dynamicSection.parentSectionId).toBe("2");
});
});
const createItem = (item: Partial<{ id: string; width: number; height: number; yOffset: number; xOffset: number }>) => {
return {
id: item.id ?? "0",
kind: "app",
options: {},
advancedOptions: {
customCssClasses: [],
},
height: item.height ?? 1,
width: item.width ?? 1,
yOffset: item.yOffset ?? 0,
xOffset: item.xOffset ?? 0,
integrationIds: [],
} satisfies Item;
};
const createDynamicSection = (
section: Partial<
Pick<DynamicSection, "id" | "height" | "width" | "yOffset" | "xOffset" | "parentSectionId" | "items">
>,
) => {
return {
id: section.id ?? "0",
kind: "dynamic",
height: section.height ?? 1,
width: section.width ?? 1,
yOffset: section.yOffset ?? 0,
xOffset: section.xOffset ?? 0,
parentSectionId: section.parentSectionId ?? "0",
items: section.items ?? [],
} satisfies DynamicSection;
};
const createSections = (initialYOffsets: number[]) => {
return initialYOffsets.map((yOffset, index) => ({
id: yOffset.toString(),
kind: index % 2 === 0 ? "empty" : "category",
name: "Category",
yOffset,
xOffset: 0,
items: [createItem({ id: yOffset.toString() })],
})) satisfies Section[];
};
@@ -2,10 +2,12 @@ import { useCallback } from "react";
import { createId } from "@homarr/db/client";
import type { CategorySection, EmptySection, Section } from "~/app/[locale]/boards/_types";
import type { CategorySection, EmptySection } 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";
import type { RemoveCategoryInput } from "./actions/remove-category";
import { removeCategoryCallback } from "./actions/remove-category";
interface AddCategory {
name: string;
@@ -17,10 +19,6 @@ interface RenameCategory {
name: string;
}
interface RemoveCategory {
id: string;
}
export const useCategoryActions = () => {
const { updateBoard } = useUpdateBoard();
@@ -132,57 +130,8 @@ export const useCategoryActions = () => {
);
const removeCategory = useCallback(
({ id: categoryId }: RemoveCategory) => {
updateBoard((previous) => {
const currentCategory = previous.sections.find(
(section): section is CategorySection => section.kind === "category" && section.id === categoryId,
);
if (!currentCategory) return previous;
const aboveWrapper = previous.sections.find(
(section): section is EmptySection =>
section.kind === "empty" && section.yOffset === currentCategory.yOffset - 1,
);
const removedWrapper = previous.sections.find(
(section): section is EmptySection =>
section.kind === "empty" && section.yOffset === currentCategory.yOffset + 1,
);
if (!aboveWrapper || !removedWrapper) return previous;
// Calculate the yOffset for the items in the currentCategory and removedWrapper to add them with the same offset to the aboveWrapper
const aboveYOffset = calculateYHeightWithOffset(aboveWrapper);
const categoryYOffset = calculateYHeightWithOffset(currentCategory);
const previousCategoryItems = currentCategory.items.map((item) => ({
...item,
yOffset: item.yOffset + aboveYOffset,
}));
const previousBelowWrapperItems = removedWrapper.items.map((item) => ({
...item,
yOffset: item.yOffset + aboveYOffset + categoryYOffset,
}));
return {
...previous,
sections: [
...previous.sections.filter((section) => section.yOffset < currentCategory.yOffset - 1),
{
...aboveWrapper,
items: [...aboveWrapper.items, ...previousCategoryItems, ...previousBelowWrapperItems],
},
...previous.sections
.filter(
(section): section is CategorySection | EmptySection => section.yOffset >= currentCategory.yOffset + 2,
)
.map((section) => ({
...section,
position: section.yOffset - 2,
})),
],
};
});
(input: RemoveCategoryInput) => {
updateBoard(removeCategoryCallback(input));
},
[updateBoard],
);
@@ -195,10 +144,3 @@ export const useCategoryActions = () => {
removeCategory,
};
};
const calculateYHeightWithOffset = (section: Section) =>
section.items.reduce((acc, item) => {
const yHeightWithOffset = item.yOffset + item.height;
if (yHeightWithOffset > acc) return yHeightWithOffset;
return acc;
}, 0);
@@ -10,6 +10,12 @@ import { useSectionContext } from "./section-context";
export const SectionContent = () => {
const { section, innerSections, refs } = useSectionContext();
const board = useRequiredBoard();
/**
* IMPORTANT: THE ORDER OF THE BELOW ITEMS HAS TO MATCH THE ORDER OF
* THE ITEMS RENDERED WITH GRIDSTACK, OTHERWISE THE ITEMS WILL BE MIXED UP
* @see https://github.com/homarr-labs/homarr/pull/1770
*/
const sortedItems = useMemo(() => {
return [
...section.items.map((item) => ({ ...item, type: "item" as const })),
@@ -19,7 +25,7 @@ export const SectionContent = () => {
return itemA.xOffset - itemB.xOffset;
}
return itemA.yOffset - itemB.xOffset;
return itemA.yOffset - itemB.yOffset;
});
}, [section.items, innerSections]);
@@ -7,6 +7,6 @@
--background-color: rgb(from var(--mantine-color-white) r g b / var(--opacity));
--border-color: rgb(from var(--mantine-color-gray-3) r g b / var(--opacity));
}
background-color: var(--background-color);
border-color: var(--border-color);
background-color: var(--background-color) !important;
border-color: var(--border-color) !important;
}
@@ -1,10 +1,12 @@
import type { FocusEventHandler } from "react";
import { startTransition, useState } from "react";
import {
ActionIcon,
Box,
Card,
Combobox,
Flex,
Group,
Image,
Indicator,
InputBase,
@@ -16,10 +18,13 @@ import {
useCombobox,
} from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import { IconUpload } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { useScopedI18n } from "@homarr/translation/client";
import { UploadMedia } from "~/app/[locale]/manage/medias/_actions/upload-media";
import classes from "./icon-picker.module.css";
interface IconPickerProps {
@@ -34,6 +39,7 @@ export const IconPicker = ({ initialValue, onChange, error, onFocus, onBlur }: I
const [value, setValue] = useState<string>(initialValue ?? "");
const [search, setSearch] = useState(initialValue ?? "");
const [previewUrl, setPreviewUrl] = useState<string | null>(initialValue ?? null);
const { data: session } = useSession();
const tCommon = useScopedI18n("common");
@@ -105,40 +111,61 @@ export const IconPicker = ({ initialValue, onChange, error, onFocus, onBlur }: I
return (
<Combobox store={combobox} withinPortal>
<Combobox.Target>
<InputBase
rightSection={<Combobox.Chevron />}
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();
combobox.updateSelectedOptionIndex();
setSearch(event.currentTarget.value);
setValue(event.currentTarget.value);
setPreviewUrl(null);
onChange(event.currentTarget.value);
}}
onClick={() => combobox.openDropdown()}
onFocus={(event) => {
onFocus?.(event);
combobox.openDropdown();
}}
onBlur={(event) => {
onBlur?.(event);
combobox.closeDropdown();
setPreviewUrl(value);
setSearch(value || "");
}}
rightSectionPointerEvents="none"
withAsterisk
error={error}
label={tCommon("iconPicker.label")}
placeholder={tCommon("iconPicker.header", { countIcons: data?.countIcons ?? 0 })}
/>
<Group wrap="nowrap" gap="xs" w="100%" align="start">
<InputBase
flex={1}
rightSection={<Combobox.Chevron />}
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();
combobox.updateSelectedOptionIndex();
setSearch(event.currentTarget.value);
setValue(event.currentTarget.value);
setPreviewUrl(null);
onChange(event.currentTarget.value);
}}
onClick={() => combobox.openDropdown()}
onFocus={(event) => {
onFocus?.(event);
combobox.openDropdown();
}}
onBlur={(event) => {
onBlur?.(event);
combobox.closeDropdown();
setPreviewUrl(value);
setSearch(value || "");
}}
rightSectionPointerEvents="none"
withAsterisk
error={error}
label={tCommon("iconPicker.label")}
placeholder={tCommon("iconPicker.header", { countIcons: data?.countIcons ?? 0 })}
/>
{session?.user.permissions.includes("media-upload") && (
<UploadMedia
onSuccess={({ url }) => {
startTransition(() => {
setValue(url);
setPreviewUrl(url);
setSearch(url);
onChange(url);
});
}}
>
{({ onClick, loading }) => (
<ActionIcon onClick={onClick} loading={loading} mt={24} size={36} variant="default">
<IconUpload size={16} stroke={1.5} />
</ActionIcon>
)}
</UploadMedia>
)}
</Group>
</Combobox.Target>
<Combobox.Dropdown>
@@ -88,7 +88,7 @@ export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuPro
</Menu.Item>
<Menu.Divider />
<Menu.Item p={0} closeMenuOnClick={false}>
<Menu.Item p={0} closeMenuOnClick={false} component="div">
<CurrentLanguageCombobox />
</Menu.Item>
<Menu.Divider />
@@ -113,7 +113,7 @@ export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuPro
{t("logout")}
</Menu.Item>
) : (
<Menu.Item onClick={() => router.push("/auth/login")} leftSection={<IconLogin size="1rem" />}>
<Menu.Item component={Link} href="/auth/login" leftSection={<IconLogin size="1rem" />}>
{t("login")}
</Menu.Item>
)}
-68
View File
@@ -1,68 +0,0 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
const isUsingDbUrl = Boolean(process.env.DB_URL);
const isUsingDbHost = Boolean(process.env.DB_HOST);
const isUsingDbCredentials = process.env.DB_DRIVER === "mysql2";
export const env = createEnv({
shared: {
VERCEL_URL: z
.string()
.optional()
.transform((url) => (url ? `https://${url}` : undefined)),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
},
/**
* Specify your server-side environment variables schema here. This way you can ensure the app isn't
* built with invalid env vars.
*/
server: {
DB_DRIVER: z.enum(["better-sqlite3", "mysql2"]).default("better-sqlite3"),
// If the DB_HOST is set, the DB_URL is optional
DB_URL: isUsingDbHost ? z.string().optional() : z.string(),
DB_HOST: isUsingDbUrl ? z.string().optional() : z.string(),
DB_PORT: isUsingDbUrl
? z.string().regex(/\d+/).transform(Number).optional()
: z
.string()
.regex(/\d+/)
.transform(Number)
.refine((number) => number >= 1)
.default("3306"),
DB_USER: isUsingDbCredentials ? z.string() : z.string().optional(),
DB_PASSWORD: isUsingDbCredentials ? z.string() : z.string().optional(),
DB_NAME: isUsingDbUrl ? z.string().optional() : z.string(),
// Comma separated list of docker hostnames that can be used to connect to query the docker endpoints (localhost:2375,host.docker.internal:2375, ...)
DOCKER_HOSTNAMES: z.string().optional(),
DOCKER_PORTS: z.number().optional(),
},
/**
* Specify your client-side environment variables schema here.
* For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
*/
client: {
// NEXT_PUBLIC_CLIENTVAR: z.string(),
},
/**
* Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
*/
runtimeEnv: {
VERCEL_URL: process.env.VERCEL_URL,
PORT: process.env.PORT,
DB_URL: process.env.DB_URL,
DB_HOST: process.env.DB_HOST,
DB_USER: process.env.DB_USER,
DB_PASSWORD: process.env.DB_PASSWORD,
DB_NAME: process.env.DB_NAME,
DB_PORT: process.env.DB_PORT,
DB_DRIVER: process.env.DB_DRIVER,
NODE_ENV: process.env.NODE_ENV,
DOCKER_HOSTNAMES: process.env.DOCKER_HOSTNAMES,
DOCKER_PORTS: process.env.DOCKER_PORTS,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
},
skipValidation:
Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION) || process.env.npm_lifecycle_event === "lint",
});
+38
View File
@@ -0,0 +1,38 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
import { shouldSkipEnvValidation } from "@homarr/common/env-validation";
export const env = createEnv({
shared: {
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
},
/**
* Specify your server-side environment variables schema here. This way you can ensure the app isn't
* built with invalid env vars.
*/
server: {
// Comma separated list of docker hostnames that can be used to connect to query the docker endpoints (localhost:2375,host.docker.internal:2375, ...)
DOCKER_HOSTNAMES: z.string().optional(),
DOCKER_PORTS: z.string().optional(),
},
/**
* Specify your client-side environment variables schema here.
* For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
*/
client: {
// NEXT_PUBLIC_CLIENTVAR: z.string(),
},
/**
* Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
*/
runtimeEnv: {
PORT: process.env.PORT,
NODE_ENV: process.env.NODE_ENV,
DOCKER_HOSTNAMES: process.env.DOCKER_HOSTNAMES,
DOCKER_PORTS: process.env.DOCKER_PORTS,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
},
skipValidation: shouldSkipEnvValidation(),
});
+1 -1
View File
@@ -4,7 +4,7 @@ import { createTRPCClient, httpLink } from "@trpc/client";
import SuperJSON from "superjson";
import type { AppRouter } from "@homarr/api";
import { createHeadersCallbackForSource } from "@homarr/api/client";
import { createHeadersCallbackForSource } from "@homarr/api/shared";
import { createI18nMiddleware } from "@homarr/translation/middleware";
export async function middleware(request: NextRequest) {
+1 -1
View File
@@ -7,7 +7,7 @@ import type { ColorScheme } from "@homarr/definitions";
import { colorSchemeCookieKey } from "@homarr/definitions";
export const getCurrentColorSchemeAsync = cache(async () => {
const cookieValue = cookies().get(colorSchemeCookieKey)?.value;
const cookieValue = (await cookies()).get(colorSchemeCookieKey)?.value;
if (cookieValue) {
return cookieValue as ColorScheme;
+5 -5
View File
@@ -38,17 +38,17 @@
"dayjs": "^1.11.13",
"dotenv": "^16.4.7",
"superjson": "2.2.2",
"undici": "7.1.1"
"undici": "7.2.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.10.2",
"dotenv-cli": "^7.4.4",
"eslint": "^9.17.0",
"@types/node": "^22.10.7",
"dotenv-cli": "^8.0.0",
"eslint": "^9.18.0",
"prettier": "^3.4.2",
"tsx": "4.19.2",
"typescript": "^5.7.2"
"typescript": "^5.7.3"
}
}
+2 -31
View File
@@ -1,35 +1,6 @@
import type { Dispatcher } from "undici";
import { Agent, setGlobalDispatcher } from "undici";
import { setGlobalDispatcher } from "undici";
import { logger } from "@homarr/log";
export class LoggingAgent extends Agent {
constructor(...props: ConstructorParameters<typeof Agent>) {
super(...props);
}
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 (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 ?? {}).length} headers)`,
);
return super.dispatch(options, handler);
}
}
import { LoggingAgent } from "@homarr/common/server";
const agent = new LoggingAgent();
setGlobalDispatcher(agent);
+2 -2
View File
@@ -34,8 +34,8 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.5.13",
"eslint": "^9.17.0",
"eslint": "^9.18.0",
"prettier": "^3.4.2",
"typescript": "^5.7.2"
"typescript": "^5.7.3"
}
}
+2 -2
View File
@@ -10,8 +10,8 @@
<a href="https://github.com/homarr-labs/homarr/actions/workflows/deployment-docker-image.yml">
  <img title="Docker CI Status" src="https://github.com/homarr-labs/homarr/actions/workflows/deployment-docker-image.yml/badge.svg" alt="CI Status">
</a>
<a href=https://translate.homarr.dev/>
<img title="Translations" src="https://badges.crowdin.net/homarr/localized.svg" />
<a href="https://crowdin.com/project/homarr_labs">
<img title="Translations" src="https://badges.crowdin.net/homarr_labs/localized.svg" />
</a>
<a href="https://discord.gg/aCsmEV5RgA">
  <img title="Discord" src="https://discordapp.com/api/guilds/972958686051962910/widget.png?style=shield">
+92
View File
@@ -0,0 +1,92 @@
import { chromium } from "playwright";
import { GenericContainer } from "testcontainers";
import { describe, expect, test } from "vitest";
import { OnboardingActions } from "./shared/actions/onboarding-actions";
import { createHomarrContainer, withLogs } from "./shared/create-homarr-container";
import { createSqliteDbFileAsync } from "./shared/e2e-db";
const defaultCredentials = {
username: "admin",
password: "password",
email: "admin@homarr.dev",
group: "lldap_admin",
};
const ldapBase = "dc=example,dc=com";
describe("LLDAP authorization", () => {
test("Authorize with LLDAP successfully", async () => {
// Arrange
const lldapContainer = await createLldapContainer().start();
const { db, localMountPath } = await createSqliteDbFileAsync();
const homarrContainer = await createHomarrContainer({
environment: {
AUTH_PROVIDERS: "ldap",
AUTH_LDAP_URI: `ldap://host.docker.internal:${lldapContainer.getMappedPort(3890)}`,
AUTH_LDAP_BASE: ldapBase,
AUTH_LDAP_BIND_DN: `uid=${defaultCredentials.username},ou=People,${ldapBase}`,
AUTH_LDAP_BIND_PASSWORD: defaultCredentials.password,
},
mounts: {
"/appdata": localMountPath,
},
}).start();
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
const onboardingActions = new OnboardingActions(page, db);
await onboardingActions.skipOnboardingAsync({
group: defaultCredentials.group,
});
// Act
await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}/auth/login`);
await page.getByLabel("Username").fill(defaultCredentials.username);
await page.getByLabel("Password").fill(defaultCredentials.password);
await page.locator("css=button[type='submit']").click();
// Assert
await page.waitForURL(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`);
const users = await db.query.users.findMany({
with: {
groups: {
with: {
group: true,
},
},
},
});
expect(users).toHaveLength(1);
const user = users[0]!;
expect(user).toEqual(
expect.objectContaining({
name: defaultCredentials.username,
email: defaultCredentials.email,
provider: "ldap",
}),
);
const groups = user.groups.map((g) => g.group.name);
expect(groups).toContain(defaultCredentials.group);
// Cleanup
await browser.close();
await homarrContainer.stop();
await lldapContainer.stop();
}, 120_000);
});
const createLldapContainer = () => {
return withLogs(
new GenericContainer("lldap/lldap:stable").withExposedPorts(3890).withEnvironment({
LLDAP_JWT_SECRET: "REPLACE_WITH_RANDOM",
LLDAP_KEY_SEED: "REPLACE_WITH_RANDOM",
LLDAP_LDAP_BASE_DN: ldapBase,
LLDAP_LDAP_USER_PASS: defaultCredentials.password,
LLDAP_LDAP_USER_EMAIL: defaultCredentials.email,
}),
);
};
+85
View File
@@ -0,0 +1,85 @@
import { chromium } from "playwright";
import { describe, test } from "vitest";
import { OnboardingActions } from "./shared/actions/onboarding-actions";
import { OnboardingAssertions } from "./shared/assertions/onboarding-assertions";
import { createHomarrContainer } from "./shared/create-homarr-container";
import { createSqliteDbFileAsync } from "./shared/e2e-db";
describe("Onboarding", () => {
test("Credentials onboarding should be successful", async () => {
// Arrange
const { db, localMountPath } = await createSqliteDbFileAsync();
const homarrContainer = await createHomarrContainer({
mounts: {
"/appdata": localMountPath,
},
}).start();
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
const actions = new OnboardingActions(page, db);
const assertions = new OnboardingAssertions(page, db);
// Act
await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`);
await actions.startOnboardingAsync("scratch");
await actions.processUserStepAsync({
username: "admin",
password: "Comp(exP4sswOrd",
confirmPassword: "Comp(exP4sswOrd",
});
await actions.processSettingsStepAsync();
// Assert
await assertions.assertFinishStepVisibleAsync();
await assertions.assertUserAndAdminGroupInsertedAsync("admin");
await assertions.assertDbOnboardingStepAsync("finish");
// Cleanup
await browser.close();
await homarrContainer.stop();
}, 60_000);
test("External provider onboarding setup should be successful", async () => {
// Arrange
const { db, localMountPath } = await createSqliteDbFileAsync();
const homarrContainer = await createHomarrContainer({
environment: {
AUTH_PROVIDERS: "ldap",
AUTH_LDAP_URI: "ldap://host.docker.internal:3890",
AUTH_LDAP_BASE: "",
AUTH_LDAP_BIND_DN: "",
AUTH_LDAP_BIND_PASSWORD: "",
},
mounts: {
"/appdata": localMountPath,
},
}).start();
const externalGroupName = "oidc-admins";
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
const actions = new OnboardingActions(page, db);
const assertions = new OnboardingAssertions(page, db);
// Act
await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`);
await actions.startOnboardingAsync("scratch");
await actions.processExternalGroupStepAsync({
name: externalGroupName,
});
await actions.processSettingsStepAsync();
// Assert
await assertions.assertFinishStepVisibleAsync();
await assertions.assertExternalGroupInsertedAsync(externalGroupName);
await assertions.assertDbOnboardingStepAsync("finish");
// Cleanup
await browser.close();
await homarrContainer.stop();
}, 60_000);
});
+53
View File
@@ -0,0 +1,53 @@
import { createId } from "@paralleldrive/cuid2";
import type { Page } from "playwright";
import * as sqliteSchema from "../../../packages/db/schema/sqlite";
import type { SqliteDatabase } from "../e2e-db";
export class OnboardingActions {
private readonly page: Page;
private readonly db: SqliteDatabase;
constructor(page: Page, db: SqliteDatabase) {
this.page = page;
this.db = db;
}
public async skipOnboardingAsync(input?: { group?: string }) {
await this.db.update(sqliteSchema.onboarding).set({
step: "finish",
});
if (input?.group) {
await this.db.insert(sqliteSchema.groups).values({
id: createId(),
name: input.group,
});
}
}
public async startOnboardingAsync(type: "scratch" | "before 1.0") {
await this.page.locator("button", { hasText: type }).click();
}
public async processUserStepAsync(input: { username: string; password: string; confirmPassword: string }) {
await this.page.waitForSelector("text=administrator user");
await this.page.getByLabel("Username").fill(input.username);
await this.page.getByLabel("Password", { exact: true }).fill(input.password);
await this.page.getByLabel("Confirm password").fill(input.confirmPassword);
await this.page.locator("css=button[type='submit']").click();
}
public async processExternalGroupStepAsync(input: { name: string }) {
await this.page.waitForSelector("text=external provider");
await this.page.locator("input").fill(input.name);
await this.page.locator("css=button[type='submit']").click();
}
public async processSettingsStepAsync() {
await this.page.waitForSelector("text=Analytics");
await this.page.locator("css=button[type='submit']").click();
}
}
@@ -0,0 +1,62 @@
import { eq } from "drizzle-orm";
import type { Page } from "playwright";
import { expect } from "vitest";
import * as sqliteSchema from "../../../packages/db/schema/sqlite";
import { OnboardingStep } from "../../../packages/definitions/src";
import { credentialsAdminGroup } from "../../../packages/definitions/src/group";
import type { SqliteDatabase } from "../e2e-db";
export class OnboardingAssertions {
private readonly page: Page;
private readonly db: SqliteDatabase;
constructor(page: Page, db: SqliteDatabase) {
this.page = page;
this.db = db;
}
public async assertDbOnboardingStepAsync(expectedStep: OnboardingStep) {
const onboarding = await this.db.query.onboarding.findFirst();
expect(onboarding?.step).toEqual(expectedStep);
}
public async assertUserAndAdminGroupInsertedAsync(expectedUsername: string) {
const users = await this.db.query.users.findMany({
with: {
groups: {
with: {
group: {
with: {
permissions: true,
},
},
},
},
},
});
const user = users.find((u) => u.name === expectedUsername);
expect(user).toBeDefined();
const adminGroup = user!.groups.find((g) => g.group.name === credentialsAdminGroup);
expect(adminGroup).toBeDefined();
expect(adminGroup!.group.permissions).toEqual([expect.objectContaining({ permission: "admin" })]);
}
public async assertExternalGroupInsertedAsync(expectedGroupName: string) {
const group = await this.db.query.groups.findFirst({
where: eq(sqliteSchema.groups.name, expectedGroupName),
with: {
permissions: true,
},
});
expect(group).toBeDefined();
expect(group!.permissions).toEqual([expect.objectContaining({ permission: "admin" })]);
}
public async assertFinishStepVisibleAsync() {
await this.page.waitForSelector("text=completed the setup", { timeout: 5000 });
}
}
+43 -3
View File
@@ -1,11 +1,51 @@
import { GenericContainer, Wait } from "testcontainers";
import { Environment } from "testcontainers/build/types";
export const createHomarrContainer = () => {
export const createHomarrContainer = (
options: {
environment?: Environment;
mounts?: {
"/appdata"?: string;
"/var/run/docker.sock"?: string;
};
} = {},
) => {
if (!process.env.CI) {
throw new Error("This test should only be run in CI or with a homarr image named 'homarr-e2e'");
}
return new GenericContainer("homarr-e2e")
const container = new GenericContainer("homarr-e2e")
.withExposedPorts(7575)
.withWaitStrategy(Wait.forHttp("/api/health/ready", 7575));
.withEnvironment({
...options.environment,
SECRET_ENCRYPTION_KEY: "0".repeat(64),
})
.withBindMounts(
Object.entries(options.mounts ?? {})
.filter((item) => item?.[0] !== undefined)
.map(([container, local]) => ({
source: local,
target: container,
})),
)
.withWaitStrategy(Wait.forHttp("/api/health/ready", 7575))
.withExtraHosts([
{
// This enabled the usage of host.docker.internal as hostname in the container
host: "host.docker.internal",
ipAddress: "host-gateway",
},
]);
return withLogs(container);
};
export const withLogs = (container: GenericContainer) => {
container.withLogConsumer((stream) =>
stream
.on("data", (line) => console.log(line))
.on("err", (line) => console.error(line))
.on("end", () => console.log("Stream closed")),
);
return container;
};
+32
View File
@@ -0,0 +1,32 @@
import { mkdir } from "fs/promises";
import path from "path";
import { createId } from "@paralleldrive/cuid2";
import Database from "better-sqlite3";
import { BetterSQLite3Database, drizzle } from "drizzle-orm/better-sqlite3";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import * as sqliteSchema from "../../packages/db/schema/sqlite";
export const createSqliteDbFileAsync = async () => {
const localMountPath = path.join(__dirname, "tmp", createId());
await mkdir(path.join(localMountPath, "db"), { recursive: true });
const localDbUrl = path.join(localMountPath, "db", "db.sqlite");
const connection = new Database(localDbUrl);
const db = drizzle(connection, {
schema: sqliteSchema,
casing: "snake_case",
});
await migrate(db, {
migrationsFolder: path.join(__dirname, "..", "..", "packages", "db", "migrations", "sqlite"),
});
return {
db,
localMountPath,
};
};
export type SqliteDatabase = BetterSQLite3Database<typeof sqliteSchema>;
+1 -1
View File
@@ -21,7 +21,7 @@ http {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
}
}
}
+15 -11
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.0.0-beta.4",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -22,9 +22,10 @@
"lint:ws": "pnpm dlx sherif@latest",
"package:new": "turbo gen init",
"release": "semantic-release",
"test": "cross-env NODE_ENV=development vitest run --exclude e2e --coverage.enabled ",
"test:e2e": "cross-env NODE_ENV=development vitest e2e",
"test:ui": "cross-env NODE_ENV=development vitest --exclude e2e --ui --coverage.enabled",
"start": "concurrently \"pnpm with-env node apps/tasks/tasks.cjs\" \"pnpm with-env node apps/websocket/wssServer.cjs\" \"pnpm -F nextjs start\"",
"test": "cross-env NODE_ENV=development CI=true vitest run --exclude e2e --coverage.enabled ",
"test:e2e": "cross-env NODE_ENV=development CI=true vitest e2e",
"test:ui": "cross-env NODE_ENV=development CI=true vitest --exclude e2e --ui --coverage.enabled",
"typecheck": "turbo typecheck",
"with-env": "dotenv -e .env --"
},
@@ -32,32 +33,35 @@
"devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.0.0",
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^11.0.1",
"@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.2",
"@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.3.3",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "^2.1.8",
"conventional-changelog-conventionalcommits": "^8.0.0",
"cross-env": "^7.0.3",
"jsdom": "^25.0.1",
"jsdom": "^26.0.0",
"prettier": "^3.4.2",
"semantic-release": "^24.2.0",
"semantic-release": "^24.2.1",
"testcontainers": "^10.16.0",
"turbo": "^2.3.3",
"typescript": "^5.7.2",
"typescript": "^5.7.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^2.1.8"
},
"packageManager": "pnpm@9.15.0",
"packageManager": "pnpm@9.15.4",
"engines": {
"node": ">=22.12.0"
"node": ">=22.13.0"
},
"pnpm": {
"allowNonAppliedPatches": true,
"overrides": {
"proxmox-api>undici": "7.2.3"
},
"patchedDependencies": {
"pretty-print-error": "patches/pretty-print-error.patch"
}
+2 -2
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.17.0",
"typescript": "^5.7.2"
"eslint": "^9.18.0",
"typescript": "^5.7.3"
}
}
@@ -3,7 +3,7 @@ import { Umami } from "@umami/node";
import { count, db } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import { integrations, items, users } from "@homarr/db/schema/sqlite";
import { integrations, items, users } from "@homarr/db/schema";
import { logger } from "@homarr/log";
import type { defaultServerSettings } from "@homarr/server-settings";
+13 -8
View File
@@ -8,7 +8,8 @@
".": "./src/index.ts",
"./client": "./src/client.ts",
"./server": "./src/server.ts",
"./websocket": "./src/websocket.ts"
"./websocket": "./src/websocket.ts",
"./shared": "./src/shared.ts"
},
"main": "./index.ts",
"types": "./index.ts",
@@ -21,12 +22,14 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/auth": "workspace:^0.1.0",
"@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/cron-job-runner": "workspace:^0.1.0",
"@homarr/cron-job-status": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/icons": "workspace:^0.1.0",
"@homarr/integrations": "workspace:^0.1.0",
"@homarr/log": "workspace:^",
"@homarr/old-import": "workspace:^0.1.0",
@@ -39,19 +42,21 @@
"@trpc/client": "next",
"@trpc/react-query": "next",
"@trpc/server": "next",
"dockerode": "^4.0.2",
"next": "^14.2.20",
"react": "^19.0.0",
"dockerode": "4.0.2",
"lodash.clonedeep": "^4.5.0",
"next": "15.1.4",
"react": "19.0.0",
"react-dom": "19.0.0",
"superjson": "2.2.2",
"trpc-to-openapi": "^2.1.0"
"trpc-to-openapi": "^2.1.2"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.32",
"eslint": "^9.17.0",
"@types/dockerode": "^3.3.34",
"eslint": "^9.18.0",
"prettier": "^3.4.2",
"typescript": "^5.7.2"
"typescript": "^5.7.3"
}
}
+5 -41
View File
@@ -1,7 +1,11 @@
import { createTRPCClient, createTRPCReact, httpLink } from "@trpc/react-query";
"use client";
import { createTRPCClient, httpLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import SuperJSON from "superjson";
import type { AppRouter } from ".";
import { createHeadersCallbackForSource } from "./shared";
export const clientApi = createTRPCReact<AppRouter>();
export const fetchApi = createTRPCClient<AppRouter>({
@@ -16,7 +20,6 @@ export const fetchApi = createTRPCClient<AppRouter>({
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}`;
}
@@ -27,42 +30,3 @@ function getBaseUrl() {
export function getTrpcUrl() {
return `${getBaseUrl()}/api/trpc`;
}
/**
* Creates a headers callback for a given source
* It will set the x-trpc-source header and cookies if needed
* @param source trpc source request comes from
* @returns headers callback
*/
export function createHeadersCallbackForSource(source: string) {
return async () => {
const headers = new Headers();
headers.set("x-trpc-source", source);
const cookies = await importCookiesAsync();
// We need to set cookie for ssr requests (for example with useSuspenseQuery or middleware)
if (cookies) {
headers.set("cookie", cookies);
}
return headers;
};
}
/**
* This is a workarround as cookies are not passed to the server
* when using useSuspenseQuery or middleware
* @returns cookie string on server or null on client
*/
async function importCookiesAsync() {
if (typeof window === "undefined") {
return await import("next/headers").then(({ cookies }) =>
cookies()
.getAll()
.map(({ name, value }) => `${name}=${value}`)
.join(";"),
);
}
return null;
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { decryptSecret } from "@homarr/common/server";
import type { AtLeastOneOf } from "@homarr/common/types";
import type { Database } from "@homarr/db";
import { and, eq, inArray } from "@homarr/db";
import { integrations } from "@homarr/db/schema/sqlite";
import { integrations } from "@homarr/db/schema";
import type { IntegrationKind } from "@homarr/definitions";
import { z } from "@homarr/validation";
+1 -1
View File
@@ -1,7 +1,7 @@
import { TRPCError } from "@trpc/server";
import { and, eq } from "@homarr/db";
import { items } from "@homarr/db/schema/sqlite";
import { items } from "@homarr/db/schema";
import type { WidgetKind } from "@homarr/definitions";
import { z } from "@homarr/validation";
+2
View File
@@ -1,6 +1,7 @@
import { apiKeysRouter } from "./router/apiKeys";
import { appRouter as innerAppRouter } from "./router/app";
import { boardRouter } from "./router/board";
import { certificateRouter } from "./router/certificates/certificate-router";
import { cronJobsRouter } from "./router/cron-jobs";
import { dockerRouter } from "./router/docker/docker-router";
import { groupRouter } from "./router/group";
@@ -41,6 +42,7 @@ export const appRouter = createTRPCRouter({
apiKeys: apiKeysRouter,
media: mediaRouter,
updateChecker: updateCheckerRouter,
certificates: certificateRouter,
});
// export type definition of API
+1 -1
View File
@@ -1,7 +1,7 @@
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
import { generateSecureRandomToken } from "@homarr/common/server";
import { createId, db } from "@homarr/db";
import { apiKeys } from "@homarr/db/schema/sqlite";
import { apiKeys } from "@homarr/db/schema";
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
Loaded 100 of 388 files, more files were not shown because too many files have changed in this diff. Show more