Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bba7be8d5 | ||
|
|
96b556f558 | ||
|
|
aaa23f3732 | ||
|
|
d33cfe211a | ||
|
|
e7071bf2bf | ||
|
|
8a87dbad59 | ||
|
|
6a5f069383 | ||
|
|
d8e4255733 | ||
|
|
1916eacb4d | ||
|
|
c30c98b153 | ||
|
|
89aed57645 | ||
|
|
251d1d0da3 | ||
|
|
51ce5da45b | ||
|
|
eff2202e92 | ||
|
|
bb25410a40 | ||
|
|
1a56a56989 | ||
|
|
ddfed18acf | ||
|
|
7bc2f87f31 | ||
|
|
05a9d26138 | ||
|
|
4f96cdf9f7 | ||
|
|
1c1f8a48d2 | ||
|
|
f648af0e2b | ||
|
|
35ca0571ad | ||
|
|
92a91329c9 | ||
|
|
23edb55c67 | ||
|
|
8449d606d7 | ||
|
|
d3edfcf3dc | ||
|
|
65f2e825fd | ||
|
|
286f4e764a | ||
|
|
def87f36fd | ||
|
|
5adc2049cc | ||
|
|
5f9054b162 | ||
|
|
b450ff6877 | ||
|
|
97052b8bff | ||
|
|
07af9d8673 | ||
|
|
4516f27fa6 | ||
|
|
f2164272a5 | ||
|
|
50caa1fcf9 | ||
|
|
742ef66e3c | ||
|
|
0d10b2b99a | ||
|
|
2ff32627f4 | ||
|
|
d9b14db6a1 | ||
|
|
186e3e445d | ||
|
|
160c20a9a6 | ||
|
|
97cbade713 | ||
|
|
816c6a0329 | ||
|
|
018a33b078 | ||
|
|
168458e9e1 | ||
|
|
ebc64f3090 | ||
|
|
d255492bb7 | ||
|
|
dcf960fa97 | ||
|
|
6ce23a6e97 | ||
|
|
a43ac18bef | ||
|
|
881d0d1219 | ||
|
|
3fff8b88b3 | ||
|
|
57d1a91866 | ||
|
|
d34f9e5b3e | ||
|
|
cf89a03f0c | ||
|
|
f4caddfd56 | ||
|
|
9e2bbb8a3f | ||
|
|
6dbd2802c9 | ||
|
|
19e25a095f | ||
|
|
4c4e901e7d | ||
|
|
5aca5d1b0a | ||
|
|
59434ee33a | ||
|
|
b59ee77c28 | ||
|
|
5effee8148 | ||
|
|
ba82853f5c | ||
|
|
1491b0fe0e | ||
|
|
b51e285c92 | ||
|
|
6d9726e19e | ||
|
|
145bea727f | ||
|
|
5b7de36642 | ||
|
|
6f3b7ec491 | ||
|
|
bbfc6e48fe | ||
|
|
2fe36564c2 | ||
|
|
747bb922cc | ||
|
|
24def99827 | ||
|
|
e0526c5ea7 | ||
|
|
9e4b93abcf | ||
|
|
c544d6ec2e | ||
|
|
2c0c589bb5 | ||
|
|
8242bfc95c | ||
|
|
3a06ce7553 | ||
|
|
c18d04616e | ||
|
|
6e5fbde23b | ||
|
|
136ed8e8b4 | ||
|
|
920a33861f | ||
|
|
7329135fd8 | ||
|
|
18cc04e7f2 | ||
|
|
ce6338840c | ||
|
|
6e6c912225 | ||
|
|
554d19a458 | ||
|
|
0491598d59 | ||
|
|
3c4c26ee21 | ||
|
|
29da5cc905 | ||
|
|
f96cbe0652 | ||
|
|
fc46d88071 | ||
|
|
1abece05ae | ||
|
|
945d2a1244 | ||
|
|
d9d9afd5e3 | ||
|
|
ddb8a99631 | ||
|
|
ef18c58f5b | ||
|
|
94eafddc72 | ||
|
|
074d27e37e | ||
|
|
b030c128c1 | ||
|
|
0ca225d790 | ||
|
|
cd391c6b08 | ||
|
|
7281b4ae32 | ||
|
|
4cab12b80a | ||
|
|
172db0e3f9 | ||
|
|
6f0b5d7e04 | ||
|
|
6d5153e5dc | ||
|
|
ee8d49c7e4 | ||
|
|
c9637021f0 | ||
|
|
bd983701b7 | ||
|
|
e7598ee0e1 | ||
|
|
26a11e5d62 | ||
|
|
0df6e8fb20 | ||
|
|
c2453b6c96 | ||
|
|
fb4b8f7ca3 | ||
|
|
592412d240 | ||
|
|
eb717da792 | ||
|
|
ed5127de64 | ||
|
|
e1b4186aa2 | ||
|
|
63c5c9ca98 | ||
|
|
b0e44943a3 | ||
|
|
448272091b | ||
|
|
ceec6d2789 | ||
|
|
e216ae3a70 | ||
|
|
ce26cdf2b2 | ||
|
|
d66be5ff82 | ||
|
|
d6e463f6e4 | ||
|
|
f643a185f6 | ||
|
|
9aa2d5ccd8 | ||
|
|
f78eaa533e | ||
|
|
857b46bc33 | ||
|
|
3f87bbd42b | ||
|
|
9c1059afe6 | ||
|
|
4e1f7c5785 | ||
|
|
6e37416e76 | ||
|
|
27de0ce2a2 | ||
|
|
90784116e8 | ||
|
|
d8a2ab74d2 | ||
|
|
4dabc98bd4 | ||
|
|
84909b2535 | ||
|
|
f2540c7d8e | ||
|
|
0e63ffe611 | ||
|
|
0b120455fd | ||
|
|
fde68f8bcc | ||
|
|
38e368fdbc | ||
|
|
94533c667c | ||
|
|
cd25bdfc0e | ||
|
|
3a7c8d5e19 | ||
|
|
1afbdd0511 | ||
|
|
1fa60cbea6 | ||
|
|
3cbb664bc0 | ||
|
|
a80c5d40e0 | ||
|
|
cccec5e195 | ||
|
|
afca8007f1 | ||
|
|
5c86930220 | ||
|
|
35a8ac820f | ||
|
|
cd5d9c66ca | ||
|
|
60370a7e33 | ||
|
|
65ccfdfb13 | ||
|
|
f792b2a553 | ||
|
|
5d402f88d2 | ||
|
|
7a7b3a32f8 | ||
|
|
d80f070ba3 | ||
|
|
d7182b9cf5 | ||
|
|
6a199561b6 | ||
|
|
7e5f4e59a9 | ||
|
|
1b7e67dd97 | ||
|
|
0aede0dec3 | ||
|
|
e8877169f8 | ||
|
|
4ffbea3b52 | ||
|
|
fab57e6fbd | ||
|
|
c2b2b098a1 | ||
|
|
4f74395000 | ||
|
|
073ede3aad | ||
|
|
40f9ce0530 | ||
|
|
92802a37db | ||
|
|
4c2e352aca | ||
|
|
3b708c5ebd | ||
|
|
50a23d76e3 | ||
|
|
6d0a1452a6 | ||
|
|
a9f73bc279 | ||
|
|
76bc24b06a | ||
|
|
196a9b4b6d | ||
|
|
eb39eea87f | ||
|
|
e0fe6d5b81 | ||
|
|
530335eecc | ||
|
|
c35c15c635 | ||
|
|
e7267548fe | ||
|
|
ec09fb70a9 | ||
|
|
7e500f772d | ||
|
|
c9841dfb47 | ||
|
|
6aa127e57a | ||
|
|
bd6761cb75 | ||
|
|
982a0c96a5 | ||
|
|
0ff0b17c28 | ||
|
|
055770c5e8 | ||
|
|
3934252053 | ||
|
|
35172fa910 | ||
|
|
9e059a9632 | ||
|
|
82cd072753 | ||
|
|
ee05137351 | ||
|
|
d9de9694c0 | ||
|
|
915a759d7e | ||
|
|
cf2977f821 | ||
|
|
c072e19150 | ||
|
|
4da24060c9 | ||
|
|
84ace42aa5 | ||
|
|
040786353d | ||
|
|
e1162c7907 | ||
|
|
e827f6653f | ||
|
|
67184890ec | ||
|
|
340f8f98e4 | ||
|
|
5955ae6f8b | ||
|
|
af448f0085 | ||
|
|
b4cfc3bf8b | ||
|
|
a21fbd26da | ||
|
|
eb7a3a31f0 | ||
|
|
6ce712794a | ||
|
|
aa67e109dd | ||
|
|
24c8dde969 | ||
|
|
fdf48d6184 | ||
|
|
6e93c3b608 | ||
|
|
dcb845b609 | ||
|
|
f82f343631 | ||
|
|
b76003de9b | ||
|
|
514034d184 | ||
|
|
878f8f3d60 | ||
|
|
2540c94697 | ||
|
|
8a552b81d8 | ||
|
|
0dd076e6c9 | ||
|
|
b81a62127e | ||
|
|
2ec0ffec5e | ||
|
|
82c31efc02 | ||
|
|
b7b79d8722 | ||
|
|
88651171ad | ||
|
|
026d9b8418 | ||
|
|
89a1cc84bc | ||
|
|
bd6655b585 | ||
|
|
faacdc03d4 | ||
|
|
6d3005fb70 | ||
|
|
82ce369c04 | ||
|
|
6932f3198a | ||
|
|
333ef48351 | ||
|
|
ca1b3383e8 | ||
|
|
bb47f163d3 | ||
|
|
5f4734ef6b | ||
|
|
1346370ce3 | ||
|
|
4da3aa43f3 | ||
|
|
319aafe35d | ||
|
|
16dff3f982 | ||
|
|
2049b90c5b | ||
|
|
4ebfb33f45 | ||
|
|
d42d81d03c | ||
|
|
e5dddc8444 | ||
|
|
0b797c1fd0 | ||
|
|
2e56d9a6c7 | ||
|
|
499bdf110a | ||
|
|
993a0d78ef | ||
|
|
5b32f1eb79 | ||
|
|
0b15561f20 | ||
|
|
2443d7300b | ||
|
|
0a908de1e7 | ||
|
|
e1a232b37a | ||
|
|
c8a6cdae81 | ||
|
|
f0a0b9bca1 | ||
|
|
3aea77a64f | ||
|
|
b7d1d4ceef | ||
|
|
aced8c77bb | ||
|
|
717d4ccff2 | ||
|
|
441e1ceba2 | ||
|
|
9ad8c6dbf1 | ||
|
|
c17e5e8774 | ||
|
|
0c577a6f11 | ||
|
|
a5ec293dda | ||
|
|
484f0bdca4 | ||
|
|
7e4bde03e4 | ||
|
|
2c3f7c02d6 | ||
|
|
1892987976 | ||
|
|
bd5065ba88 | ||
|
|
9b63fb6df6 | ||
|
|
588f8a4faa | ||
|
|
cf892f517f | ||
|
|
3eb2b3795b | ||
|
|
2c3856a2a9 | ||
|
|
83df2ec58b | ||
|
|
303067b0c8 | ||
|
|
9b8ec975e4 | ||
|
|
92811db400 | ||
|
|
eb85804cff | ||
|
|
25e32e0010 | ||
|
|
4af13a8d32 | ||
|
|
11f29d8def | ||
|
|
c0ed041bdf | ||
|
|
2eb82108a8 | ||
|
|
034dcacb08 | ||
|
|
966e1ac81e | ||
|
|
bcc71e6eaa | ||
|
|
61ae703014 | ||
|
|
7ea746bed1 | ||
|
|
c0197d0e96 | ||
|
|
75f9ead209 |
No files matched your search
@@ -33,6 +33,14 @@ body:
|
||||
options:
|
||||
# The below comment is used to insert a new version with on-release.yml
|
||||
#NEXT_VERSION#
|
||||
- 1.43.1
|
||||
- 1.43.0
|
||||
- 1.42.1
|
||||
- 1.42.0
|
||||
- 1.41.0
|
||||
- 1.40.0
|
||||
- 1.39.0
|
||||
- 1.38.0
|
||||
- 1.37.0
|
||||
- 1.36.1
|
||||
- 1.36.0
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
extends: ["config:recommended"],
|
||||
packageRules: [
|
||||
{
|
||||
matchPackagePatterns: ["^@homarr/"],
|
||||
matchPackageNames: ["^@homarr/"],
|
||||
enabled: false,
|
||||
},
|
||||
// Reenable once https://github.com/privatenumber/tsx/issues/737 is fixed
|
||||
{
|
||||
matchPackagePatterns: ["tsx"],
|
||||
matchPackageNames: ["tsx"],
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
@@ -21,4 +21,6 @@
|
||||
automerge: false,
|
||||
baseBranches: ["dev"],
|
||||
dependencyDashboard: false,
|
||||
// prevent special characters in branch names (otherwise git errors occur on windows)
|
||||
branchNameStrict: true,
|
||||
}
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- cron: "0 0 * * *" # every day at midnight
|
||||
|
||||
permissions:
|
||||
contents: read # required for code checkout
|
||||
contents: write # required to push changes
|
||||
|
||||
jobs:
|
||||
download-crowdin-translations:
|
||||
|
||||
@@ -61,10 +61,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@v4
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
- uses: actions/setup-node@v5
|
||||
- uses: actions/setup-node@v6
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
with:
|
||||
node-version: 22.19.0
|
||||
node-version: 24.11.0
|
||||
cache: "pnpm"
|
||||
- run: npm i -g pnpm
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
name: "[Renovate] Validate configuration"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["*"]
|
||||
paths: [".github/renovate.json5"]
|
||||
|
||||
jobs:
|
||||
renovate-validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: |
|
||||
npx --yes --package renovate -- \
|
||||
renovate-config-validator --strict .github/renovate.json5
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "pnpm"
|
||||
|
||||
+208
@@ -1,3 +1,211 @@
|
||||
## [1.43.3](https://github.com/homarr-labs/homarr/compare/v1.43.2...v1.43.3) (2025-11-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency @ctrl/qbittorrent to ^9.11.0 ([#4456](https://github.com/homarr-labs/homarr/issues/4456)) ([ddfed18](https://github.com/homarr-labs/homarr/commit/ddfed18acf44596dba15ea0ef56233c4936f9775))
|
||||
* **deps:** update dependency @tanstack/react-query to ^5.90.7 ([#4451](https://github.com/homarr-labs/homarr/issues/4451)) ([7bc2f87](https://github.com/homarr-labs/homarr/commit/7bc2f87f31f1babe691fa4a086a3897977f2d871))
|
||||
* **deps:** update dependency fast-xml-parser to ^5.3.1 ([#4435](https://github.com/homarr-labs/homarr/issues/4435)) ([65f2e82](https://github.com/homarr-labs/homarr/commit/65f2e825fdb9e5e79b814c9b49f663d1afac784d))
|
||||
* **deps:** update dependency fastify to ^5.6.2 ([#4471](https://github.com/homarr-labs/homarr/issues/4471)) ([e7071bf](https://github.com/homarr-labs/homarr/commit/e7071bf2bf6a45ff760cd3e7bd06382d2ed66cdc))
|
||||
* **deps:** update dependency next-intl to v4.5.0 ([#4462](https://github.com/homarr-labs/homarr/issues/4462)) ([51ce5da](https://github.com/homarr-labs/homarr/commit/51ce5da45bdccf061e593097ed751cf09d663437))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.30.2 ([#4442](https://github.com/homarr-labs/homarr/issues/4442)) ([d3edfcf](https://github.com/homarr-labs/homarr/commit/d3edfcf3dca3d34afd5ef8ec21599cd08fde5092))
|
||||
* **deps:** update mantine monorepo to ^8.3.7 ([#4472](https://github.com/homarr-labs/homarr/issues/4472)) ([8a87dba](https://github.com/homarr-labs/homarr/commit/8a87dbad59786fa9789dd3efb8d9424bb950e977))
|
||||
* **deps:** update testcontainers-node monorepo to ^11.8.0 ([#4450](https://github.com/homarr-labs/homarr/issues/4450)) ([f648af0](https://github.com/homarr-labs/homarr/commit/f648af0e2b2b5d5f8180402c2734ea0c04559daa))
|
||||
* **stock-price:** ignore missing data points in price history ([#4429](https://github.com/homarr-labs/homarr/issues/4429)) ([c30c98b](https://github.com/homarr-labs/homarr/commit/c30c98b153fbec21551e45aedf83e0ae29d6e773))
|
||||
* **users:** next button in create form always disabled ([#4443](https://github.com/homarr-labs/homarr/issues/4443)) ([1c1f8a4](https://github.com/homarr-labs/homarr/commit/1c1f8a48d259c65ebeeec6856e1fe339defb4aaf))
|
||||
|
||||
## [1.43.2](https://github.com/homarr-labs/homarr/compare/v1.43.1...v1.43.2) (2025-11-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency @ctrl/deluge to ^7.5.0 ([#4407](https://github.com/homarr-labs/homarr/issues/4407)) ([57d1a91](https://github.com/homarr-labs/homarr/commit/57d1a9186653dd4c327b360a3bdaa78fd8e1187b))
|
||||
* **deps:** update dependency @gitbeaker/rest to ^43.7.0 ([#4386](https://github.com/homarr-labs/homarr/issues/4386)) ([dcf960f](https://github.com/homarr-labs/homarr/commit/dcf960fa97011c24475f303d587152b42eabec45))
|
||||
* **deps:** update dependency @gitbeaker/rest to ^43.8.0 ([#4425](https://github.com/homarr-labs/homarr/issues/4425)) ([4516f27](https://github.com/homarr-labs/homarr/commit/4516f27fa69445e5aec16760e1f6ed8080bc8fff))
|
||||
* **deps:** update dependency @jellyfin/sdk to ^0.13.0 ([#4399](https://github.com/homarr-labs/homarr/issues/4399)) ([4c4e901](https://github.com/homarr-labs/homarr/commit/4c4e901e7d38faf83a4725b7cee5526b0b223dc3))
|
||||
* **deps:** update dependency @octokit/auth-app to ^8.1.2 ([#4412](https://github.com/homarr-labs/homarr/issues/4412)) ([018a33b](https://github.com/homarr-labs/homarr/commit/018a33b07812fa59f1469ce8b4baa7b1d55c6103))
|
||||
* **deps:** update dependency @tanstack/react-query to ^5.90.6 ([#4427](https://github.com/homarr-labs/homarr/issues/4427)) ([5f9054b](https://github.com/homarr-labs/homarr/commit/5f9054b162daf974baafdbee9f844b3a860cbdd0))
|
||||
* **deps:** update dependency dayjs to ^1.11.19 ([#4414](https://github.com/homarr-labs/homarr/issues/4414)) ([97cbade](https://github.com/homarr-labs/homarr/commit/97cbade7135cb06e0780dd1b448c78c8802794ea))
|
||||
* **deps:** update dependency drizzle-kit to ^0.31.6 ([#4397](https://github.com/homarr-labs/homarr/issues/4397)) ([59434ee](https://github.com/homarr-labs/homarr/commit/59434ee33ae16e06f96e2f07cf85337c313c73f6))
|
||||
* **deps:** update dependency jotai to ^2.15.1 ([#4426](https://github.com/homarr-labs/homarr/issues/4426)) ([07af9d8](https://github.com/homarr-labs/homarr/commit/07af9d8673ea9573bc436626cd84d11980d023bc))
|
||||
* **deps:** update dependency octokit to ^5.0.5 ([#4413](https://github.com/homarr-labs/homarr/issues/4413)) ([186e3e4](https://github.com/homarr-labs/homarr/commit/186e3e445dfe6b4fb1aeda3830ae1f260b605882))
|
||||
* **deps:** update dependency sass to ^1.93.3 ([#4421](https://github.com/homarr-labs/homarr/issues/4421)) ([f216427](https://github.com/homarr-labs/homarr/commit/f2164272a5ca9b51bdf9110664ed5a40b87b60f6))
|
||||
* **deps:** update dependency superjson to v2.2.5 ([#4398](https://github.com/homarr-labs/homarr/issues/4398)) ([19e25a0](https://github.com/homarr-labs/homarr/commit/19e25a095fb0252e8f2a459e0a97d44a37d2188a))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.30.0 ([#4391](https://github.com/homarr-labs/homarr/issues/4391)) ([b51e285](https://github.com/homarr-labs/homarr/commit/b51e285c92ef707076f321f0ce1a15d5cbd5de7d))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.30.1 ([#4418](https://github.com/homarr-labs/homarr/issues/4418)) ([d9b14db](https://github.com/homarr-labs/homarr/commit/d9b14db6a1af515796951a1db670431d10f81eaf))
|
||||
* **deps:** update dependency tsdav to ^2.1.6 ([#4417](https://github.com/homarr-labs/homarr/issues/4417)) ([160c20a](https://github.com/homarr-labs/homarr/commit/160c20a9a6f850f75537e246f42e93f9a2ed79a0))
|
||||
* **deps:** update mantine monorepo to ^8.3.6 ([#4403](https://github.com/homarr-labs/homarr/issues/4403)) ([9e2bbb8](https://github.com/homarr-labs/homarr/commit/9e2bbb8a3f2db95c5598e5dca993bf6cd4ebc127))
|
||||
* **deps:** update nextjs monorepo to v16 (major) ([#4363](https://github.com/homarr-labs/homarr/issues/4363)) ([6ce23a6](https://github.com/homarr-labs/homarr/commit/6ce23a6e971b064292970fc4df90cc69cdb59a95))
|
||||
* **deps:** update tiptap monorepo to v2.27.0 ([#4404](https://github.com/homarr-labs/homarr/issues/4404)) ([f4caddf](https://github.com/homarr-labs/homarr/commit/f4caddfd564553014854347eb6dd14cc800eb027))
|
||||
* **deps:** update tiptap monorepo to v2.27.1 ([#4405](https://github.com/homarr-labs/homarr/issues/4405)) ([d34f9e5](https://github.com/homarr-labs/homarr/commit/d34f9e5b3ef999559a4c5f1165488fe4f8828a83))
|
||||
* **deps:** update trpc monorepo to ^11.7.1 ([#4396](https://github.com/homarr-labs/homarr/issues/4396)) ([5aca5d1](https://github.com/homarr-labs/homarr/commit/5aca5d1b0a76ab4cf0ed27dac62183c766ad398a))
|
||||
* **deps:** update turbo monorepo to ^2.6.0 ([#4422](https://github.com/homarr-labs/homarr/issues/4422)) ([742ef66](https://github.com/homarr-labs/homarr/commit/742ef66e3cf56fd351c8dee2d3dd05cd639e51fa))
|
||||
|
||||
## [1.43.1](https://github.com/homarr-labs/homarr/compare/v1.43.0...v1.43.1) (2025-10-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency @auth/core to ^0.41.1 ([#4387](https://github.com/homarr-labs/homarr/issues/4387)) ([747bb92](https://github.com/homarr-labs/homarr/commit/747bb922ccf0508bd00c37faeab29e28876577b1))
|
||||
* **deps:** update dependency @auth/drizzle-adapter to ^1.11.1 ([#4388](https://github.com/homarr-labs/homarr/issues/4388)) ([2fe3656](https://github.com/homarr-labs/homarr/commit/2fe36564c26c33ae2625eb22ef4503c6dd8ebcf9))
|
||||
* **deps:** update dependency @jellyfin/sdk to ^0.12.0 ([#4359](https://github.com/homarr-labs/homarr/issues/4359)) ([554d19a](https://github.com/homarr-labs/homarr/commit/554d19a458e856141f90e795ecebabf205e945e9))
|
||||
* **deps:** update dependency @paralleldrive/cuid2 to v3 ([#4343](https://github.com/homarr-labs/homarr/issues/4343)) ([074d27e](https://github.com/homarr-labs/homarr/commit/074d27e37e3430e7e98cf28e2876afe009811027))
|
||||
* **deps:** update dependency drizzle-orm to ^0.44.7 ([#4372](https://github.com/homarr-labs/homarr/issues/4372)) ([8242bfc](https://github.com/homarr-labs/homarr/commit/8242bfc95cd0e67397b1bfc46d65aec3f568c304))
|
||||
* **deps:** update dependency ioredis to v5.8.2 ([#4357](https://github.com/homarr-labs/homarr/issues/4357)) ([3c4c26e](https://github.com/homarr-labs/homarr/commit/3c4c26ee214a37193aea31b69ce75322862fc534))
|
||||
* **deps:** update dependency mysql2 to v3.15.3 ([#4360](https://github.com/homarr-labs/homarr/issues/4360)) ([6e6c912](https://github.com/homarr-labs/homarr/commit/6e6c9122257934402848d6fdc51e6ccdfc2dcca0))
|
||||
* **deps:** update dependency next-auth to v5.0.0-beta.30 [security] ([#4379](https://github.com/homarr-labs/homarr/issues/4379)) ([c544d6e](https://github.com/homarr-labs/homarr/commit/c544d6ec2e426bc4c82db547d7c4a5dc4d6e2a27))
|
||||
* **deps:** update dependency next-intl to v4.4.0 ([#4365](https://github.com/homarr-labs/homarr/issues/4365)) ([ce63388](https://github.com/homarr-labs/homarr/commit/ce6338840c20fc46048687ed5566ff12fa4b2502))
|
||||
* **deps:** update dependency node-ical to ^0.22.1 ([#4346](https://github.com/homarr-labs/homarr/issues/4346)) ([ef18c58](https://github.com/homarr-labs/homarr/commit/ef18c58f5b09bafd697365a0182ab7bc8719db2b))
|
||||
* **deps:** update dependency superjson to v2.2.3 ([#4364](https://github.com/homarr-labs/homarr/issues/4364)) ([18cc04e](https://github.com/homarr-labs/homarr/commit/18cc04e7f2740226f7bc7c75c0b0cdaec7cda6f8))
|
||||
* **deps:** update dependency typescript-eslint to ^8.46.2 ([#4352](https://github.com/homarr-labs/homarr/issues/4352)) ([f96cbe0](https://github.com/homarr-labs/homarr/commit/f96cbe06525515112fad466cb8a5f33d74517aba))
|
||||
* **deps:** update testcontainers-node monorepo to ^11.7.2 ([#4366](https://github.com/homarr-labs/homarr/issues/4366)) ([7329135](https://github.com/homarr-labs/homarr/commit/7329135fd85906602a16a9bdc16a1ef1cc4607c5))
|
||||
* **deps:** update tiptap monorepo to v2.26.4 ([#4371](https://github.com/homarr-labs/homarr/issues/4371)) ([136ed8e](https://github.com/homarr-labs/homarr/commit/136ed8e8b401b801ba16f2079bdfb5aeebe9f57f))
|
||||
* **deps:** update trpc monorepo to ^11.7.0 ([#4385](https://github.com/homarr-labs/homarr/issues/4385)) ([24def99](https://github.com/homarr-labs/homarr/commit/24def9982709cd6de1765a7235169fe1ceb3cbea))
|
||||
* **media-releases:** support more media type mappings for jellyfin and emby ([#4382](https://github.com/homarr-labs/homarr/issues/4382)) ([6f3b7ec](https://github.com/homarr-labs/homarr/commit/6f3b7ec4915754e183300a915baebc11f264df62))
|
||||
* **releases-widget:** Search multiple docker hub pages for releases ([#4158](https://github.com/homarr-labs/homarr/issues/4158)) ([e0526c5](https://github.com/homarr-labs/homarr/commit/e0526c5ea7ee83adcd22d78fbf63334cb59bcf79))
|
||||
|
||||
## [1.43.0](https://github.com/homarr-labs/homarr/compare/v1.42.1...v1.43.0) (2025-10-24)
|
||||
|
||||
### Features
|
||||
|
||||
* **integrations:** add app linking ([#4338](https://github.com/homarr-labs/homarr/issues/4338)) ([172db0e](https://github.com/homarr-labs/homarr/commit/172db0e3f905839b01e934a121c5bc5e45f3314a))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency @tanstack/react-query to ^5.90.5 ([#4316](https://github.com/homarr-labs/homarr/issues/4316)) ([4cab12b](https://github.com/homarr-labs/homarr/commit/4cab12b80a4a37a20393ed8a3727997e157405d3))
|
||||
* **deps:** update dependency node-ical to ^0.22.0 ([#4344](https://github.com/homarr-labs/homarr/issues/4344)) ([7281b4a](https://github.com/homarr-labs/homarr/commit/7281b4ae32f6b86d3a1c6666db84419387b496a5))
|
||||
* **deps:** update dependency octokit to ^5.0.4 ([#4340](https://github.com/homarr-labs/homarr/issues/4340)) ([bd98370](https://github.com/homarr-labs/homarr/commit/bd983701b7d4eea15c81e7d6482f1390b98a35fb))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.29.5 ([#4335](https://github.com/homarr-labs/homarr/issues/4335)) ([fb4b8f7](https://github.com/homarr-labs/homarr/commit/fb4b8f7ca3cc3c11392dc1665be2ad907d15772e))
|
||||
* **deps:** update mantine monorepo to ^8.3.5 ([#4291](https://github.com/homarr-labs/homarr/issues/4291)) ([63c5c9c](https://github.com/homarr-labs/homarr/commit/63c5c9ca9846d6c11cc44372366a68d3d46ee57c))
|
||||
* **deps:** update nextjs monorepo to v15.5.6 ([#4314](https://github.com/homarr-labs/homarr/issues/4314)) ([0df6e8f](https://github.com/homarr-labs/homarr/commit/0df6e8fb20e59457e7c12d123ba7f948619062e1))
|
||||
|
||||
## [1.42.1](https://github.com/homarr-labs/homarr/compare/v1.42.0...v1.42.1) (2025-10-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency @ctrl/qbittorrent to ^9.10.0 ([#4310](https://github.com/homarr-labs/homarr/issues/4310)) ([3f87bbd](https://github.com/homarr-labs/homarr/commit/3f87bbd42bbe122870ca388e8b52069265cc577f))
|
||||
* **deps:** update dependency typescript-eslint to ^8.46.1 ([#4313](https://github.com/homarr-labs/homarr/issues/4313)) ([f78eaa5](https://github.com/homarr-labs/homarr/commit/f78eaa533e9f75cfbdc223d5581e19f73fdb7303))
|
||||
* **deps:** update nextjs monorepo to v15.5.5 ([#4312](https://github.com/homarr-labs/homarr/issues/4312)) ([857b46b](https://github.com/homarr-labs/homarr/commit/857b46bc333b5f3d5b3336986bdbe399e28bdc99))
|
||||
* **docker:** stats not working ([#4311](https://github.com/homarr-labs/homarr/issues/4311)) ([9aa2d5c](https://github.com/homarr-labs/homarr/commit/9aa2d5ccd8cfa8facea962c5e21efeee7f695c97))
|
||||
|
||||
## [1.42.0](https://github.com/homarr-labs/homarr/compare/v1.41.0...v1.42.0) (2025-10-17)
|
||||
|
||||
### Features
|
||||
|
||||
* **nextcloud:** reuse color from event for indicator ([#4270](https://github.com/homarr-labs/homarr/issues/4270)) ([afca800](https://github.com/homarr-labs/homarr/commit/afca8007f1919e0bb8cb17eefb9c41e3f28291cc))
|
||||
* **users:** allow profile picture change for external providers ([#4275](https://github.com/homarr-labs/homarr/issues/4275)) ([5c86930](https://github.com/homarr-labs/homarr/commit/5c86930220e97888eeae1c5440572ba7ef2434be))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **calendar:** recurring events not working ([#4265](https://github.com/homarr-labs/homarr/issues/4265)) ([1b7e67d](https://github.com/homarr-labs/homarr/commit/1b7e67dd9760c159e525cd88face00e28a260d13))
|
||||
* **deps:** update dependency @auth/core to ^0.41.0 ([#4282](https://github.com/homarr-labs/homarr/issues/4282)) ([60370a7](https://github.com/homarr-labs/homarr/commit/60370a7e33dc1957aa433cbd6a11ab69661606cc))
|
||||
* **deps:** update dependency @auth/drizzle-adapter to ^1.11.0 ([#4283](https://github.com/homarr-labs/homarr/issues/4283)) ([cd5d9c6](https://github.com/homarr-labs/homarr/commit/cd5d9c66ca42687faa9aeeff3d4d74d21a472a92))
|
||||
* **deps:** update dependency @ctrl/qbittorrent to ^9.9.1 ([#4229](https://github.com/homarr-labs/homarr/issues/4229)) ([c2b2b09](https://github.com/homarr-labs/homarr/commit/c2b2b098a1ae7df027265dfcd184c01be1831ffc))
|
||||
* **deps:** update dependency ioredis to v5.8.1 ([#4259](https://github.com/homarr-labs/homarr/issues/4259)) ([4ffbea3](https://github.com/homarr-labs/homarr/commit/4ffbea3b521b45c3dd52595d4f6824d25a6b4a05))
|
||||
* **deps:** update dependency mysql2 to v3.15.2 ([#4280](https://github.com/homarr-labs/homarr/issues/4280)) ([65ccfdf](https://github.com/homarr-labs/homarr/commit/65ccfdfb13e83e78e65585aaf15a07b9d9755697))
|
||||
* **deps:** update dependency next-intl to v4.3.10 ([#4271](https://github.com/homarr-labs/homarr/issues/4271)) ([5d402f8](https://github.com/homarr-labs/homarr/commit/5d402f88d29892ddebfc7e9efc902be4c3bbeba1))
|
||||
* **deps:** update dependency next-intl to v4.3.11 ([#4273](https://github.com/homarr-labs/homarr/issues/4273)) ([f792b2a](https://github.com/homarr-labs/homarr/commit/f792b2a553f7a5851db36fc6a7ec495e2662dbc0))
|
||||
* **deps:** update dependency next-intl to v4.3.12 ([#4274](https://github.com/homarr-labs/homarr/issues/4274)) ([3a7c8d5](https://github.com/homarr-labs/homarr/commit/3a7c8d5e19c28a1e3b084e80eb52a3193a419d7f))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.29.4 ([#4293](https://github.com/homarr-labs/homarr/issues/4293)) ([fde68f8](https://github.com/homarr-labs/homarr/commit/fde68f8bcc75aefb9cdb15c0b3b5d12c18a54d35))
|
||||
* **deps:** update dependency trpc-to-openapi to ^3.1.0 ([#4268](https://github.com/homarr-labs/homarr/issues/4268)) ([d80f070](https://github.com/homarr-labs/homarr/commit/d80f070ba3dca6bd8af10ee771f7cbd3d24dafa3))
|
||||
* **deps:** update dependency typescript-eslint to ^8.46.0 ([#4264](https://github.com/homarr-labs/homarr/issues/4264)) ([0aede0d](https://github.com/homarr-labs/homarr/commit/0aede0dec38016f5df08ce42f4f2e9237aaecaa6))
|
||||
* **deps:** update dependency zod to ^4.1.12 ([#4263](https://github.com/homarr-labs/homarr/issues/4263)) ([e887716](https://github.com/homarr-labs/homarr/commit/e8877169f88179fc9a23947c3f34478f851cb66a))
|
||||
* **deps:** update mantine monorepo to ^8.3.4 ([#4290](https://github.com/homarr-labs/homarr/issues/4290)) ([94533c6](https://github.com/homarr-labs/homarr/commit/94533c667cd234fdfe8f064b3d4892ad902e74c7))
|
||||
* **deps:** update tiptap monorepo to v2.26.3 ([#4292](https://github.com/homarr-labs/homarr/issues/4292)) ([38e368f](https://github.com/homarr-labs/homarr/commit/38e368fdbcf9aba54fbc960031742cb162a72b72))
|
||||
* **docker-widget:** memory usage wrong ([#4266](https://github.com/homarr-labs/homarr/issues/4266)) ([cccec5e](https://github.com/homarr-labs/homarr/commit/cccec5e19538aa8df154281f14fa939414bc8190))
|
||||
* **management:** improve homepage ([#4260](https://github.com/homarr-labs/homarr/issues/4260)) ([7e5f4e5](https://github.com/homarr-labs/homarr/commit/7e5f4e59a9edcb05dfa8a09919408e93e8d0313f))
|
||||
* **plex:** tv show links not working ([#4272](https://github.com/homarr-labs/homarr/issues/4272)) ([a80c5d4](https://github.com/homarr-labs/homarr/commit/a80c5d40e0e944d40dfdfe8e902ee10872e26bc0))
|
||||
* **users:** profile picture upload limit wrong ([#4276](https://github.com/homarr-labs/homarr/issues/4276)) ([35a8ac8](https://github.com/homarr-labs/homarr/commit/35a8ac820f084271bc7591834096226639aec0d4))
|
||||
|
||||
## [1.41.0](https://github.com/homarr-labs/homarr/compare/v1.40.0...v1.41.0) (2025-10-10)
|
||||
|
||||
### Features
|
||||
|
||||
* **dynamic-sections:** add custom css classes option ([#4256](https://github.com/homarr-labs/homarr/issues/4256)) ([4c2e352](https://github.com/homarr-labs/homarr/commit/4c2e352acac62434c840e407887f8fddfc8c426a))
|
||||
* **integrations:** allow changing secret kinds of existing integration ([#4254](https://github.com/homarr-labs/homarr/issues/4254)) ([3b708c5](https://github.com/homarr-labs/homarr/commit/3b708c5ebdcfb3425cb0b777f72ff2aaea23386b))
|
||||
* **weather-widget:** add imperial windspeed option ([#4253](https://github.com/homarr-labs/homarr/issues/4253)) ([50a23d7](https://github.com/homarr-labs/homarr/commit/50a23d76e3dc14f23a27125297b8901a6724d18a))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency @ctrl/deluge to ^7.4.0 ([#4227](https://github.com/homarr-labs/homarr/issues/4227)) ([cf2977f](https://github.com/homarr-labs/homarr/commit/cf2977f82189d9fa8b896012dd7730a641cc252b))
|
||||
* **deps:** update dependency @ctrl/qbittorrent to ^9.9.0 ([#4228](https://github.com/homarr-labs/homarr/issues/4228)) ([ee05137](https://github.com/homarr-labs/homarr/commit/ee051373512fbfb5fb70d78ea45755b2cf370f6a))
|
||||
* **deps:** update dependency @kubernetes/client-node to ^1.4.0 ([#4247](https://github.com/homarr-labs/homarr/issues/4247)) ([ec09fb7](https://github.com/homarr-labs/homarr/commit/ec09fb70a950cbef131352f60fdf45553c7fe99f))
|
||||
* **deps:** update dependency @octokit/auth-app to ^8.1.1 ([#4217](https://github.com/homarr-labs/homarr/issues/4217)) ([5955ae6](https://github.com/homarr-labs/homarr/commit/5955ae6f8b3c90a7c9b17760650095676303192e))
|
||||
* **deps:** update dependency dockerode to ^4.0.9 ([#4214](https://github.com/homarr-labs/homarr/issues/4214)) ([340f8f9](https://github.com/homarr-labs/homarr/commit/340f8f98e452c068c5d26973195ea676d2b3ee6e))
|
||||
* **deps:** update dependency dotenv to ^17.2.3 ([#4218](https://github.com/homarr-labs/homarr/issues/4218)) ([af448f0](https://github.com/homarr-labs/homarr/commit/af448f008571562492748986bd7cb2651623130e))
|
||||
* **deps:** update dependency drizzle-orm to ^0.44.6 ([#4237](https://github.com/homarr-labs/homarr/issues/4237)) ([3934252](https://github.com/homarr-labs/homarr/commit/3934252053792874c41a6bd545b20acd7ccef4e8))
|
||||
* **deps:** update dependency eslint-plugin-react-hooks to v6 ([#4231](https://github.com/homarr-labs/homarr/issues/4231)) ([76bc24b](https://github.com/homarr-labs/homarr/commit/76bc24b06aeec71b53b9f3351b98c076621d69d9))
|
||||
* **deps:** update dependency fast-xml-parser to ^5.3.0 ([#4245](https://github.com/homarr-labs/homarr/issues/4245)) ([c35c15c](https://github.com/homarr-labs/homarr/commit/c35c15c635f37d09071b9ccafcd532e1e2e9ccd4))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.29.1 ([#4210](https://github.com/homarr-labs/homarr/issues/4210)) ([eb7a3a3](https://github.com/homarr-labs/homarr/commit/eb7a3a31f0ddf135664f5609c39b9d8ee72ef432))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.29.2 ([#4212](https://github.com/homarr-labs/homarr/issues/4212)) ([055770c](https://github.com/homarr-labs/homarr/commit/055770c5e8e43dfc035b2a25d650383df865c118))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.29.3 ([#4238](https://github.com/homarr-labs/homarr/issues/4238)) ([e726754](https://github.com/homarr-labs/homarr/commit/e7267548fe132d6b32e3ba8379ebe31fa8f16635))
|
||||
* **deps:** update dependency typescript-eslint to ^8.45.0 ([#4215](https://github.com/homarr-labs/homarr/issues/4215)) ([e827f66](https://github.com/homarr-labs/homarr/commit/e827f6653f1caabaf4da7d477aa59849e9d023b9))
|
||||
* **deps:** update dependency winston to v3.18.0 ([#4219](https://github.com/homarr-labs/homarr/issues/4219)) ([e1162c7](https://github.com/homarr-labs/homarr/commit/e1162c7907b3285f1e8bb9980340ca758786715d))
|
||||
* **deps:** update dependency winston to v3.18.1 ([#4220](https://github.com/homarr-labs/homarr/issues/4220)) ([84ace42](https://github.com/homarr-labs/homarr/commit/84ace42aa5eb5f3f8e3351e64b1e1ba6fe747544))
|
||||
* **deps:** update dependency winston to v3.18.3 ([#4222](https://github.com/homarr-labs/homarr/issues/4222)) ([d9de969](https://github.com/homarr-labs/homarr/commit/d9de9694c0d7bbb630d40e64c01dee082856ce0f))
|
||||
* **deps:** update mantine monorepo to ^8.3.3 ([#4242](https://github.com/homarr-labs/homarr/issues/4242)) ([982a0c9](https://github.com/homarr-labs/homarr/commit/982a0c96a5d40a532d4fa06640f0d35e8125e1f2))
|
||||
* **deps:** update react monorepo to v19.2.0 ([#4230](https://github.com/homarr-labs/homarr/issues/4230)) ([35172fa](https://github.com/homarr-labs/homarr/commit/35172fa91007487c6da3f6e0709dadeab20621c6))
|
||||
* **deps:** update testcontainers-node monorepo to ^11.7.0 ([#4211](https://github.com/homarr-labs/homarr/issues/4211)) ([a21fbd2](https://github.com/homarr-labs/homarr/commit/a21fbd26dad82427e62a5a6b18f72dc32c00e608))
|
||||
* **deps:** update testcontainers-node monorepo to ^11.7.1 ([#4213](https://github.com/homarr-labs/homarr/issues/4213)) ([7e500f7](https://github.com/homarr-labs/homarr/commit/7e500f772d769b30354a73d73e2fee6382fb8811))
|
||||
|
||||
## [1.40.0](https://github.com/homarr-labs/homarr/compare/v1.39.0...v1.40.0) (2025-10-03)
|
||||
|
||||
### Features
|
||||
|
||||
* **about:** add hotkeys list ([#4165](https://github.com/homarr-labs/homarr/issues/4165)) ([f82f343](https://github.com/homarr-labs/homarr/commit/f82f3436313fa2e2ccf04ff0e7886bf4efffd78e))
|
||||
* **media-server:** add stats for nerds ([#4170](https://github.com/homarr-labs/homarr/issues/4170)) ([6e93c3b](https://github.com/homarr-labs/homarr/commit/6e93c3b60813671f027062d55b2f16d2c8445475))
|
||||
* **media-transcoding:** add periodic live updates ([#4166](https://github.com/homarr-labs/homarr/issues/4166)) ([b76003d](https://github.com/homarr-labs/homarr/commit/b76003de9bfe9c06d829e8da0faf77c688c31e2d))
|
||||
* **medias:** support upload of multiple items ([#4169](https://github.com/homarr-labs/homarr/issues/4169)) ([dcb845b](https://github.com/homarr-labs/homarr/commit/dcb845b609354b0423f5b7a9d08862b88fd90fd9))
|
||||
* **translations:** add crowdin live support ([#4177](https://github.com/homarr-labs/homarr/issues/4177)) ([514034d](https://github.com/homarr-labs/homarr/commit/514034d184d1654518295e847da3183cc7da2c91))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency better-sqlite3 to ^12.3.0 ([#4160](https://github.com/homarr-labs/homarr/issues/4160)) ([0b797c1](https://github.com/homarr-labs/homarr/commit/0b797c1fd0d338c85553e800ca11699d3e3a4c7d))
|
||||
* **deps:** update dependency better-sqlite3 to ^12.4.1 ([#4162](https://github.com/homarr-labs/homarr/issues/4162)) ([4da3aa4](https://github.com/homarr-labs/homarr/commit/4da3aa43f35cc51a117b51e4bc3827837696cc21))
|
||||
* **deps:** update dependency drizzle-kit to ^0.31.5 ([#4195](https://github.com/homarr-labs/homarr/issues/4195)) ([2ec0ffe](https://github.com/homarr-labs/homarr/commit/2ec0ffec5e50d2b931d9b65cf81fd141ebfd531f))
|
||||
* **deps:** update dependency fastify to ^5.6.1 ([#4167](https://github.com/homarr-labs/homarr/issues/4167)) ([319aafe](https://github.com/homarr-labs/homarr/commit/319aafe35dd60ce44aa6aa65008caf1ede17af5e))
|
||||
* **deps:** update dependency ioredis to v5.8.0 ([#4174](https://github.com/homarr-labs/homarr/issues/4174)) ([bb47f16](https://github.com/homarr-labs/homarr/commit/bb47f163d3ef0b1082d5af72a0906787e52ee154))
|
||||
* **deps:** update dependency jotai to ^2.15.0 ([#4192](https://github.com/homarr-labs/homarr/issues/4192)) ([82c31ef](https://github.com/homarr-labs/homarr/commit/82c31efc02e8f7b3793c2ef115656f85ef97c9ba))
|
||||
* **deps:** update dependency mysql2 to v3.15.1 ([#4185](https://github.com/homarr-labs/homarr/issues/4185)) ([026d9b8](https://github.com/homarr-labs/homarr/commit/026d9b841853657e4284c6c703b340a5dc0a9eab))
|
||||
* **deps:** update dependency sass to ^1.93.1 ([#4149](https://github.com/homarr-labs/homarr/issues/4149)) ([2049b90](https://github.com/homarr-labs/homarr/commit/2049b90c5bc4043c73f681c9c805629d2ddb570e))
|
||||
* **deps:** update dependency sass to ^1.93.2 ([#4171](https://github.com/homarr-labs/homarr/issues/4171)) ([6d3005f](https://github.com/homarr-labs/homarr/commit/6d3005fb70cdfe882cf36ee77ef75492ec902c1c))
|
||||
* **deps:** update dependency typescript-eslint to ^8.44.1 ([#4168](https://github.com/homarr-labs/homarr/issues/4168)) ([16dff3f](https://github.com/homarr-labs/homarr/commit/16dff3f982dc758e9f698a4ec4472180032366a2))
|
||||
* **deps:** update mantine monorepo to ^8.3.2 ([#4179](https://github.com/homarr-labs/homarr/issues/4179)) ([6932f31](https://github.com/homarr-labs/homarr/commit/6932f3198a3534a92a9b56efd0f9e59f9fb68a14))
|
||||
* **deps:** update nextjs monorepo to v15.5.4 ([#4178](https://github.com/homarr-labs/homarr/issues/4178)) ([333ef48](https://github.com/homarr-labs/homarr/commit/333ef4835100e7017d909f2ce3323ac82b47f4e2))
|
||||
* **deps:** update tanstack-query monorepo to ^5.90.1 ([#4161](https://github.com/homarr-labs/homarr/issues/4161)) ([e5dddc8](https://github.com/homarr-labs/homarr/commit/e5dddc84446dcbc7831d74a2285dce946e1d5524))
|
||||
* **deps:** update tanstack-query monorepo to ^5.90.2 ([#4163](https://github.com/homarr-labs/homarr/issues/4163)) ([5f4734e](https://github.com/homarr-labs/homarr/commit/5f4734ef6ba83cedf16031e2ac6c02dd3586c9a1))
|
||||
* **deps:** update tiptap monorepo to v2.26.2 ([#4173](https://github.com/homarr-labs/homarr/issues/4173)) ([1346370](https://github.com/homarr-labs/homarr/commit/1346370ce39e23fa6d431b0d3f5d064eb9eb210f))
|
||||
* **deps:** update trpc monorepo to ^11.6.0 ([#4188](https://github.com/homarr-labs/homarr/issues/4188)) ([b7b79d8](https://github.com/homarr-labs/homarr/commit/b7b79d8722f50af293c971fbc3547eb7487ecaf1))
|
||||
* **deps:** update turbo monorepo to ^2.5.7 ([#4176](https://github.com/homarr-labs/homarr/issues/4176)) ([82ce369](https://github.com/homarr-labs/homarr/commit/82ce369c0413fbb20db814ee77dfb6e13184655b))
|
||||
* **deps:** update turbo monorepo to ^2.5.8 ([#4180](https://github.com/homarr-labs/homarr/issues/4180)) ([89a1cc8](https://github.com/homarr-labs/homarr/commit/89a1cc84bc9411c68100b6d947b6ae033237403e))
|
||||
|
||||
## [1.39.0](https://github.com/homarr-labs/homarr/compare/v1.38.0...v1.39.0) (2025-09-26)
|
||||
|
||||
### Features
|
||||
|
||||
* **calendar:** add homeassistant support ([#4152](https://github.com/homarr-labs/homarr/issues/4152)) ([0a908de](https://github.com/homarr-labs/homarr/commit/0a908de1e7768d4524170297e5d20bb1e786d824))
|
||||
* **calendar:** add periodic live updates ([#4154](https://github.com/homarr-labs/homarr/issues/4154)) ([2443d73](https://github.com/homarr-labs/homarr/commit/2443d7300b958edabceb95ab8663b9bebb30e4d0))
|
||||
* **weather:** add periodic live updates ([#4155](https://github.com/homarr-labs/homarr/issues/4155)) ([5b32f1e](https://github.com/homarr-labs/homarr/commit/5b32f1eb79d132291992e102f1f2fc72805742a9))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **calendar:** missing badge for sonarr series ([#4150](https://github.com/homarr-labs/homarr/issues/4150)) ([b7d1d4c](https://github.com/homarr-labs/homarr/commit/b7d1d4ceef110761ede7e2c7e54e86945d6b4fb0))
|
||||
* **calendar:** support multi-day events ([#4151](https://github.com/homarr-labs/homarr/issues/4151)) ([c8a6cda](https://github.com/homarr-labs/homarr/commit/c8a6cdae8194bad1bfcadfcb06853ab56437b6d4))
|
||||
* **deps:** update dependency @ctrl/deluge to ^7.3.0 ([#4121](https://github.com/homarr-labs/homarr/issues/4121)) ([eb85804](https://github.com/homarr-labs/homarr/commit/eb85804cff5c5b61697ee270411e75735fc0c32f))
|
||||
* **deps:** update dependency @ctrl/qbittorrent to ^9.8.0 ([#4118](https://github.com/homarr-labs/homarr/issues/4118)) ([c0ed041](https://github.com/homarr-labs/homarr/commit/c0ed041bdfb3bbb246cdc048f7f4d27818e21348))
|
||||
* **deps:** update dependency @ctrl/transmission to ^7.4.0 ([#4120](https://github.com/homarr-labs/homarr/issues/4120)) ([2eb8210](https://github.com/homarr-labs/homarr/commit/2eb82108a8d1447652eed840fab0757ff0168c77))
|
||||
* **deps:** update dependency @tabler/icons-react to ^3.35.0 ([#4115](https://github.com/homarr-labs/homarr/issues/4115)) ([61ae703](https://github.com/homarr-labs/homarr/commit/61ae7030145bee4a4d20afdd176f3e8486668c5f))
|
||||
* **deps:** update dependency dns-caching to ^0.2.6 ([#4131](https://github.com/homarr-labs/homarr/issues/4131)) ([2c3856a](https://github.com/homarr-labs/homarr/commit/2c3856a2a91c7566f951ec5238b6c4c5c0a26f40))
|
||||
* **deps:** update dependency dns-caching to ^0.2.7 ([#4137](https://github.com/homarr-labs/homarr/issues/4137)) ([484f0bd](https://github.com/homarr-labs/homarr/commit/484f0bdca45f949999f974d4d119e656d6f7206e))
|
||||
* **deps:** update dependency mysql2 to v3.15.0 ([#4125](https://github.com/homarr-labs/homarr/issues/4125)) ([cf892f5](https://github.com/homarr-labs/homarr/commit/cf892f517f9b5e06420541650599ec977942387f))
|
||||
* **deps:** update dependency next-intl to v4.3.9 ([#4096](https://github.com/homarr-labs/homarr/issues/4096)) ([034dcac](https://github.com/homarr-labs/homarr/commit/034dcacb0818ed9daa1e29474ca0f6e39a5c8198))
|
||||
* **deps:** update dependency node-ical to ^0.21.0 ([#4111](https://github.com/homarr-labs/homarr/issues/4111)) ([bcc71e6](https://github.com/homarr-labs/homarr/commit/bcc71e6eaa0dedd3a5609ef5384219afe01ea13a))
|
||||
* **deps:** update dependency sass to ^1.93.0 ([#4147](https://github.com/homarr-labs/homarr/issues/4147)) ([aced8c7](https://github.com/homarr-labs/homarr/commit/aced8c77bb33cc3df2d9db3f5c3513632d8e51c2))
|
||||
* **deps:** update dependency typescript-eslint to ^8.44.0 ([#4114](https://github.com/homarr-labs/homarr/issues/4114)) ([7ea746b](https://github.com/homarr-labs/homarr/commit/7ea746bed12ff66b84ffd993268e47db18c40a88))
|
||||
* **deps:** update dependency zod to ^4.1.11 ([#4128](https://github.com/homarr-labs/homarr/issues/4128)) ([f0a0b9b](https://github.com/homarr-labs/homarr/commit/f0a0b9bca197cd970b5ad279e50f05806fe51809))
|
||||
* **deps:** update dependency zod to ^4.1.9 ([#4094](https://github.com/homarr-labs/homarr/issues/4094)) ([303067b](https://github.com/homarr-labs/homarr/commit/303067b0c8fdc92647fbaa574f29a633c63c40d3))
|
||||
* **deps:** update dependency zod-validation-error to ^4.0.2 ([#4142](https://github.com/homarr-labs/homarr/issues/4142)) ([0c577a6](https://github.com/homarr-labs/homarr/commit/0c577a6f11990b94dc524c6e41ab624e4fdf5c9b))
|
||||
* **deps:** update tanstack-query monorepo to ^5.89.0 ([#4122](https://github.com/homarr-labs/homarr/issues/4122)) ([11f29d8](https://github.com/homarr-labs/homarr/commit/11f29d8def97286cc5f4b2aa45a345a79ab3630b))
|
||||
* **deps:** update testcontainers-node monorepo to ^11.6.0 ([#4130](https://github.com/homarr-labs/homarr/issues/4130)) ([83df2ec](https://github.com/homarr-labs/homarr/commit/83df2ec58b3d6bb78660a3ec79690b51a1cd395c))
|
||||
* Recalculate time ago on re-render ([#4107](https://github.com/homarr-labs/homarr/issues/4107)) ([0b15561](https://github.com/homarr-labs/homarr/commit/0b15561f20abe94908ee62f99a33ff3f030c49e2))
|
||||
|
||||
## [1.38.0](https://github.com/homarr-labs/homarr/compare/v1.37.0...v1.38.0) (2025-09-19)
|
||||
|
||||
### Features
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:22.19.0-alpine AS base
|
||||
FROM node:24.11.0-alpine AS base
|
||||
|
||||
FROM base AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
@@ -20,19 +20,26 @@ const withNextIntl = createNextIntlPlugin({
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
reactStrictMode: true,
|
||||
/** We already do linting and typechecking as separate tasks in CI */
|
||||
eslint: { ignoreDuringBuilds: true },
|
||||
reactCompiler: true,
|
||||
/** We already do typechecking as separate tasks in CI */
|
||||
typescript: { ignoreBuildErrors: true },
|
||||
/**
|
||||
* dockerode is required in the external server packages because of https://github.com/homarr-labs/homarr/issues/612
|
||||
* isomorphic-dompurify and jsdom are required, see https://github.com/kkomelin/isomorphic-dompurify/issues/356
|
||||
*/
|
||||
serverExternalPackages: ["dockerode"],
|
||||
serverExternalPackages: ["dockerode", "isomorphic-dompurify", "jsdom"],
|
||||
experimental: {
|
||||
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
|
||||
turbopackFileSystemCacheForDev: true,
|
||||
},
|
||||
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
|
||||
images: {
|
||||
domains: ["cdn.jsdelivr.net"],
|
||||
localPatterns: [
|
||||
{
|
||||
pathname: "/**",
|
||||
search: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/require-await,no-restricted-syntax
|
||||
async headers() {
|
||||
|
||||
+32
-30
@@ -50,59 +50,61 @@
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@homarr/widgets": "workspace:^0.1.0",
|
||||
"@mantine/colors-generator": "^8.3.1",
|
||||
"@mantine/core": "^8.3.1",
|
||||
"@mantine/dropzone": "^8.3.1",
|
||||
"@mantine/hooks": "^8.3.1",
|
||||
"@mantine/modals": "^8.3.1",
|
||||
"@mantine/tiptap": "^8.3.1",
|
||||
"@mantine/colors-generator": "^8.3.7",
|
||||
"@mantine/core": "^8.3.7",
|
||||
"@mantine/dropzone": "^8.3.7",
|
||||
"@mantine/hooks": "^8.3.7",
|
||||
"@mantine/modals": "^8.3.7",
|
||||
"@mantine/tiptap": "^8.3.7",
|
||||
"@million/lint": "1.0.14",
|
||||
"@tabler/icons-react": "^3.34.1",
|
||||
"@tanstack/react-query": "^5.87.4",
|
||||
"@tanstack/react-query-devtools": "^5.87.4",
|
||||
"@tanstack/react-query-next-experimental": "^5.87.4",
|
||||
"@trpc/client": "^11.5.1",
|
||||
"@trpc/next": "^11.5.1",
|
||||
"@trpc/react-query": "^11.5.1",
|
||||
"@trpc/server": "^11.5.1",
|
||||
"@tabler/icons-react": "^3.35.0",
|
||||
"@tanstack/react-query": "^5.90.7",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"@tanstack/react-query-next-experimental": "^5.90.2",
|
||||
"@trpc/client": "^11.7.1",
|
||||
"@trpc/next": "^11.7.1",
|
||||
"@trpc/react-query": "^11.7.1",
|
||||
"@trpc/server": "^11.7.1",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"chroma-js": "^3.1.2",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.18",
|
||||
"dotenv": "^17.2.2",
|
||||
"dayjs": "^1.11.19",
|
||||
"dotenv": "^17.2.3",
|
||||
"flag-icons": "^7.5.0",
|
||||
"glob": "^11.0.3",
|
||||
"jotai": "^2.14.0",
|
||||
"isomorphic-dompurify": "^2.32.0",
|
||||
"jotai": "^2.15.1",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "15.5.3",
|
||||
"next": "16.0.1",
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"sass": "^1.92.1",
|
||||
"superjson": "2.2.2",
|
||||
"swagger-ui-react": "^5.29.0",
|
||||
"sass": "^1.93.3",
|
||||
"superjson": "2.2.5",
|
||||
"swagger-ui-react": "^5.30.2",
|
||||
"use-deep-compare-effect": "^1.8.1",
|
||||
"zod": "^4.1.8"
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/chroma-js": "3.1.1",
|
||||
"@types/node": "^22.18.3",
|
||||
"@types/chroma-js": "3.1.2",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "19.1.13",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.39.1",
|
||||
"node-loader": "^2.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Group, Menu, ScrollArea } from "@mantine/core";
|
||||
import { useHotkeys } from "@mantine/hooks";
|
||||
@@ -19,14 +18,17 @@ import {
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { env } from "@homarr/common/env";
|
||||
import { hotkeys } from "@homarr/definitions";
|
||||
import { useConfirmModal, useModalAction } from "@homarr/modals";
|
||||
import { AppSelectModal } from "@homarr/modals-collection";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import { useItemActions } from "~/components/board/items/item-actions";
|
||||
import { ItemSelectModal } from "~/components/board/items/item-select-modal";
|
||||
@@ -61,6 +63,7 @@ export const BoardContentHeaderActions = () => {
|
||||
};
|
||||
|
||||
const AddMenu = () => {
|
||||
const { data: session } = useSession();
|
||||
const { openModal: openCategoryEditModal } = useModalAction(CategoryEditModal);
|
||||
const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal);
|
||||
const { openModal: openAppSelectModal } = useModalAction(AppSelectModal);
|
||||
@@ -95,12 +98,13 @@ const AddMenu = () => {
|
||||
|
||||
const handleSelectApp = useCallback(() => {
|
||||
openAppSelectModal({
|
||||
onSelect: (appId) => {
|
||||
onSelect: (app) => {
|
||||
createItem({
|
||||
kind: "app",
|
||||
options: { appId },
|
||||
options: { appId: app.id },
|
||||
});
|
||||
},
|
||||
withCreate: session?.user.permissions.includes("app-create") ?? false,
|
||||
});
|
||||
}, [openAppSelectModal, createItem]);
|
||||
|
||||
@@ -165,7 +169,7 @@ const EditModeMenu = () => {
|
||||
open();
|
||||
}, [board, isEditMode, saveBoard, open]);
|
||||
|
||||
useHotkeys([["mod+e", toggle]]);
|
||||
useHotkeys([[hotkeys.toggleBoardEdit, toggle]]);
|
||||
usePreventLeaveWithDirty(isEditMode);
|
||||
|
||||
return (
|
||||
|
||||
@@ -120,11 +120,14 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
|
||||
/>
|
||||
{session?.user.permissions.includes("media-upload") && (
|
||||
<UploadMedia
|
||||
onSuccess={({ url }) =>
|
||||
onSuccess={(medias) => {
|
||||
const first = medias.at(0);
|
||||
if (!first) return;
|
||||
|
||||
startTransition(() => {
|
||||
form.setFieldValue("backgroundImageUrl", url);
|
||||
})
|
||||
}
|
||||
form.setFieldValue("backgroundImageUrl", first.url);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{({ onClick, loading }) => (
|
||||
<ActionIcon onClick={onClick} loading={loading} mt={24} size={36} variant="default">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Link from "next/link";
|
||||
import type { MantineColor } from "@mantine/core";
|
||||
import { Button, Card, Stack, Text } from "@mantine/core";
|
||||
import { IconBook2, IconCategoryPlus, IconLayoutDashboard, IconMailForward } from "@tabler/icons-react";
|
||||
@@ -8,6 +7,7 @@ import { getMantineColor } from "@homarr/common";
|
||||
import { db } from "@homarr/db";
|
||||
import { createDocumentationLink } from "@homarr/definitions";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
import { Link } from "@homarr/ui";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
export const InitFinish = async () => {
|
||||
|
||||
@@ -23,6 +23,7 @@ import type { SupportedLanguage } from "@homarr/translation";
|
||||
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
|
||||
|
||||
import { Analytics } from "~/components/layout/analytics";
|
||||
import { CrowdinLiveTranslation } from "~/components/layout/crowdin-live-translation";
|
||||
import { SearchEngineOptimization } from "~/components/layout/search-engine-optimization";
|
||||
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
|
||||
import { DayJsLoader } from "./_client-providers/dayjs-loader";
|
||||
@@ -118,10 +119,12 @@ export default async function Layout(props: {
|
||||
(innerProps) => <SpotlightProvider {...innerProps} />,
|
||||
]);
|
||||
|
||||
const { locale } = await props.params;
|
||||
|
||||
return (
|
||||
// Instead of ColorSchemScript we use data-mantine-color-scheme to prevent flickering
|
||||
<html
|
||||
lang={(await props.params).locale}
|
||||
lang={locale}
|
||||
dir={direction}
|
||||
data-mantine-color-scheme={colorScheme}
|
||||
style={{
|
||||
@@ -132,6 +135,7 @@ export default async function Layout(props: {
|
||||
<head>
|
||||
<Analytics />
|
||||
<SearchEngineOptimization />
|
||||
<CrowdinLiveTranslation locale={locale} />
|
||||
</head>
|
||||
<body className={["font-sans", fontSans.variable].join(" ")}>
|
||||
<StackedProvider>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Box, Grid, GridCol, Group, Image, Stack, Title } from "@mantine/core";
|
||||
|
||||
import { splitToNChunks } from "@homarr/common";
|
||||
import { integrationDefs } from "@homarr/definitions";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import classes from "./hero-banner.module.css";
|
||||
|
||||
@@ -12,19 +13,20 @@ const icons = Object.values(integrationDefs)
|
||||
const countIconGroups = 3;
|
||||
const animationDurationInSeconds = icons.length;
|
||||
const arrayInChunks = splitToNChunks(icons, countIconGroups);
|
||||
const gridSpan = 12 / countIconGroups;
|
||||
|
||||
export const HeroBanner = () => {
|
||||
const gridSpan = 12 / countIconGroups;
|
||||
export const HeroBanner = async () => {
|
||||
const t = await getScopedI18n("management.page.home");
|
||||
|
||||
return (
|
||||
<Box className={classes.bannerContainer} p={{ base: "lg", md: "3rem" }} bg="dark.6" pos="relative">
|
||||
<Stack gap={0}>
|
||||
<Title fz={{ base: "h4", md: "h2" }} c="dimmed">
|
||||
Welcome back to your
|
||||
{t("heroBanner.title")}
|
||||
</Title>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Image src="/logo/logo.png" w={{ base: 32, md: 40 }} h={{ base: 32, md: 40 }} />
|
||||
<Title fz={{ base: "h3", md: "h1" }}>Homarr Board</Title>
|
||||
<Title fz={{ base: "h3", md: "h1" }}>{t("heroBanner.subtitle", { app: "Homarr" })}</Title>
|
||||
</Group>
|
||||
</Stack>
|
||||
<Box visibleFrom="md" className={classes.scrollContainer} w={"30%"} top={0} right={0} pos="absolute">
|
||||
|
||||
@@ -11,14 +11,23 @@ import {
|
||||
Center,
|
||||
Flex,
|
||||
Group,
|
||||
Kbd,
|
||||
List,
|
||||
ListItem,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { IconLanguage, IconLibrary, IconUsers } from "@tabler/icons-react";
|
||||
import { IconKeyboard, IconLanguage, IconLibrary, IconUsers } from "@tabler/icons-react";
|
||||
|
||||
import { capitalize, objectEntries } from "@homarr/common";
|
||||
import { hotkeys } from "@homarr/definitions";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { homarrLogoPath } from "~/components/layout/logo/homarr-logo";
|
||||
@@ -149,6 +158,45 @@ export default async function AboutPage() {
|
||||
</List>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="hotkeys">
|
||||
<AccordionControl icon={<IconKeyboard size="1rem" />}>
|
||||
<Stack gap={0}>
|
||||
<Text>{t("accordion.hotkeys.title")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("accordion.hotkeys.subtitle")}
|
||||
</Text>
|
||||
</Stack>
|
||||
</AccordionControl>
|
||||
<AccordionPanel>
|
||||
<Table>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>{t("accordion.hotkeys.field.shortcut")}</TableTh>
|
||||
<TableTh>{t("accordion.hotkeys.field.action")}</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{objectEntries(hotkeys).map(([key, shortcut]) => (
|
||||
<TableTr key={key}>
|
||||
<TableTd>
|
||||
<Kbd size="md">
|
||||
{shortcut
|
||||
.split("+")
|
||||
.map((key) => capitalize(key.trim()))
|
||||
.join(" + ")}
|
||||
</Kbd>
|
||||
</TableTd>
|
||||
<TableTd>{t(`accordion.hotkeys.action.${key}`)}</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("accordion.hotkeys.note")}
|
||||
</Text>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Fragment } from "react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconBox, IconPencil } from "@tabler/icons-react";
|
||||
@@ -10,7 +9,7 @@ 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 { Link, SearchInput, TablePagination } from "@homarr/ui";
|
||||
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { Menu } from "@mantine/core";
|
||||
import { IconCopy, IconDeviceMobile, IconHome, IconSettings, IconTrash } from "@tabler/icons-react";
|
||||
|
||||
@@ -12,6 +11,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { useConfirmModal, useModalAction } from "@homarr/modals";
|
||||
import { DuplicateBoardModal } from "@homarr/modals-collection";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import { useBoardPermissions } from "~/components/board/permissions/client";
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
@@ -21,7 +20,7 @@ import type { RouterOutputs } from "@homarr/api";
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
import { UserAvatar } from "@homarr/ui";
|
||||
import { Link, UserAvatar } from "@homarr/ui";
|
||||
|
||||
import { getBoardPermissionsAsync } from "~/components/board/permissions/server";
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { useCallback } from "react";
|
||||
import { SegmentedControl } from "@mantine/core";
|
||||
|
||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||
import type { UseFormReturnType } from "@homarr/form";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
interface FormType {
|
||||
secrets: { kind: IntegrationSecretKind; value: string | null }[];
|
||||
}
|
||||
|
||||
interface SecretKindsSegmentedControlProps<TFormType extends FormType> {
|
||||
defaultKinds?: IntegrationSecretKind[];
|
||||
secretKinds: IntegrationSecretKind[][];
|
||||
form: UseFormReturnType<TFormType, (values: TFormType) => TFormType>;
|
||||
}
|
||||
|
||||
export const SecretKindsSegmentedControl = <TFormType extends FormType>({
|
||||
defaultKinds,
|
||||
secretKinds,
|
||||
form,
|
||||
}: SecretKindsSegmentedControlProps<TFormType>) => {
|
||||
const t = useScopedI18n("integration.secrets");
|
||||
|
||||
const defaultValue = defaultKinds?.length === 0 ? "empty" : defaultKinds?.join("-");
|
||||
const secretKindGroups = secretKinds.map((kinds) => ({
|
||||
label:
|
||||
kinds.length === 0
|
||||
? t("noSecretsRequired.segmentTitle")
|
||||
: kinds.map((kind) => t(`kind.${kind}.label`)).join(" & "),
|
||||
value: kinds.length === 0 ? "empty" : kinds.join("-"),
|
||||
}));
|
||||
|
||||
const onChange = useCallback(
|
||||
(value: string) => {
|
||||
if (value === "empty") {
|
||||
const emptyValues = [] satisfies FormType["secrets"];
|
||||
// @ts-expect-error somehow it is not able to understand that secrets is an array?
|
||||
form.setFieldValue("secrets", emptyValues);
|
||||
return;
|
||||
}
|
||||
|
||||
const kinds = value.split("-") as IntegrationSecretKind[];
|
||||
const secrets = kinds.map((kind) => ({
|
||||
kind,
|
||||
value: "",
|
||||
})) satisfies FormType["secrets"];
|
||||
// @ts-expect-error somehow it is not able to understand that secrets is an array?
|
||||
form.setFieldValue("secrets", secrets);
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
return (
|
||||
<SegmentedControl
|
||||
fullWidth
|
||||
data={secretKindGroups}
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
></SegmentedControl>
|
||||
);
|
||||
};
|
||||
+167
-40
@@ -1,23 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button, Fieldset, Group, Stack, TextInput } from "@mantine/core";
|
||||
import type { z } from "zod/v4";
|
||||
import { Alert, Anchor, Button, ButtonGroup, Fieldset, Group, Stack, Text, TextInput } from "@mantine/core";
|
||||
import { IconInfoCircle, IconPencil, IconPlus, IconUnlink } from "@tabler/icons-react";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
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 { getAllSecretKindOptions, getDefaultSecretKinds } from "@homarr/definitions";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import { useConfirmModal, useModalAction } from "@homarr/modals";
|
||||
import { AppSelectModal } from "@homarr/modals-collection";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
import { integrationUpdateSchema } from "@homarr/validation/integration";
|
||||
|
||||
import { SecretCard } from "../../_components/secrets/integration-secret-card";
|
||||
import { IntegrationSecretInput } from "../../_components/secrets/integration-secret-inputs";
|
||||
import { SecretKindsSegmentedControl } from "../../_components/secrets/integration-secret-segmented-control";
|
||||
import { IntegrationTestConnectionError } from "../../_components/test-connection/integration-test-connection-error";
|
||||
import type { AnyMappedTestConnectionError } from "../../_components/test-connection/types";
|
||||
|
||||
@@ -25,25 +29,41 @@ interface EditIntegrationForm {
|
||||
integration: RouterOutputs["integration"]["byId"];
|
||||
}
|
||||
|
||||
const formSchema = integrationUpdateSchema.omit({ id: true, appId: true }).and(
|
||||
z.object({
|
||||
app: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
iconUrl: z.string(),
|
||||
href: z.string().nullable(),
|
||||
})
|
||||
.nullable(),
|
||||
}),
|
||||
);
|
||||
|
||||
export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
const t = useI18n();
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const secretsKinds =
|
||||
const allSecretKinds = getAllSecretKindOptions(integration.kind);
|
||||
|
||||
const initialSecretsKinds =
|
||||
getAllSecretKindOptions(integration.kind).find((secretKinds) =>
|
||||
integration.secrets.every((secret) => secretKinds.includes(secret.kind)),
|
||||
) ?? getDefaultSecretKinds(integration.kind);
|
||||
|
||||
const hasUrlSecret = secretsKinds.includes("url");
|
||||
const hasUrlSecret = initialSecretsKinds.includes("url");
|
||||
|
||||
const router = useRouter();
|
||||
const form = useZodForm(integrationUpdateSchema.omit({ id: true }), {
|
||||
const form = useZodForm(formSchema, {
|
||||
initialValues: {
|
||||
name: integration.name,
|
||||
url: integration.url,
|
||||
secrets: secretsKinds.map((kind) => ({
|
||||
secrets: initialSecretsKinds.map((kind) => ({
|
||||
kind,
|
||||
value: integration.secrets.find((secret) => secret.kind === kind)?.value ?? "",
|
||||
})),
|
||||
app: integration.app ?? null,
|
||||
},
|
||||
});
|
||||
const { mutateAsync, isPending } = clientApi.integration.update.useMutation();
|
||||
@@ -51,7 +71,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
|
||||
const secretsMap = new Map(integration.secrets.map((secret) => [secret.kind, secret]));
|
||||
|
||||
const handleSubmitAsync = async (values: FormType) => {
|
||||
const handleSubmitAsync = async ({ app, ...values }: FormType) => {
|
||||
const url = hasUrlSecret
|
||||
? new URL(values.secrets.find((secret) => secret.kind === "url")?.value ?? values.url).origin
|
||||
: values.url;
|
||||
@@ -64,6 +84,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
kind: secret.kind,
|
||||
value: secret.value === "" ? null : secret.value,
|
||||
})),
|
||||
appId: app?.id ?? null,
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
@@ -93,8 +114,12 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isInitialSecretKinds =
|
||||
initialSecretsKinds.every((kind) => form.values.secrets.some((secret) => secret.kind === kind)) &&
|
||||
form.values.secrets.length === initialSecretsKinds.length;
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
|
||||
<form onSubmit={form.onSubmit(async (values) => await handleSubmitAsync(values))}>
|
||||
<Stack>
|
||||
<TextInput withAsterisk label={t("integration.field.name.label")} {...form.getInputProps("name")} />
|
||||
|
||||
@@ -104,39 +129,65 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
|
||||
<Fieldset legend={t("integration.secrets.title")}>
|
||||
<Stack gap="sm">
|
||||
{secretsKinds.map((kind, index) => (
|
||||
<SecretCard
|
||||
key={kind}
|
||||
secret={secretsMap.get(kind) ?? { kind, value: null, updatedAt: null }}
|
||||
onCancel={() =>
|
||||
new Promise((resolve) => {
|
||||
// When nothing changed, just close the secret card
|
||||
if ((form.values.secrets[index]?.value ?? "") === (secretsMap.get(kind)?.value ?? "")) {
|
||||
return resolve(true);
|
||||
{allSecretKinds.length > 1 && (
|
||||
<SecretKindsSegmentedControl
|
||||
defaultKinds={initialSecretsKinds}
|
||||
secretKinds={allSecretKinds}
|
||||
form={form}
|
||||
/>
|
||||
)}
|
||||
{!isInitialSecretKinds
|
||||
? null
|
||||
: form.values.secrets.map((secret, index) => (
|
||||
<SecretCard
|
||||
key={secret.kind}
|
||||
secret={secretsMap.get(secret.kind) ?? { kind: secret.kind, value: null, updatedAt: null }}
|
||||
onCancel={() =>
|
||||
new Promise((resolve) => {
|
||||
// When nothing changed, just close the secret card
|
||||
if ((secret.value ?? "") === (secretsMap.get(secret.kind)?.value ?? "")) {
|
||||
return resolve(true);
|
||||
}
|
||||
openConfirmModal({
|
||||
title: t("integration.secrets.reset.title"),
|
||||
children: t("integration.secrets.reset.message"),
|
||||
onCancel: () => resolve(false),
|
||||
onConfirm: () => {
|
||||
form.setFieldValue(`secrets.${index}.value`, secretsMap.get(secret.kind)?.value ?? "");
|
||||
resolve(true);
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
openConfirmModal({
|
||||
title: t("integration.secrets.reset.title"),
|
||||
children: t("integration.secrets.reset.message"),
|
||||
onCancel: () => resolve(false),
|
||||
onConfirm: () => {
|
||||
form.setFieldValue(`secrets.${index}.value`, secretsMap.get(kind)?.value ?? "");
|
||||
resolve(true);
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
>
|
||||
<IntegrationSecretInput
|
||||
label={t(`integration.secrets.kind.${kind}.newLabel`)}
|
||||
key={kind}
|
||||
kind={kind}
|
||||
{...form.getInputProps(`secrets.${index}.value`)}
|
||||
/>
|
||||
</SecretCard>
|
||||
))}
|
||||
>
|
||||
<IntegrationSecretInput
|
||||
label={t(`integration.secrets.kind.${secret.kind}.newLabel`)}
|
||||
key={secret.kind}
|
||||
kind={secret.kind}
|
||||
{...form.getInputProps(`secrets.${index}.value`)}
|
||||
/>
|
||||
</SecretCard>
|
||||
))}
|
||||
{isInitialSecretKinds
|
||||
? null
|
||||
: form.values.secrets.map(({ kind }, index) => (
|
||||
<IntegrationSecretInput
|
||||
withAsterisk
|
||||
key={kind}
|
||||
kind={kind}
|
||||
{...form.getInputProps(`secrets.${index}.value`)}
|
||||
/>
|
||||
))}
|
||||
{form.values.secrets.length === 0 && (
|
||||
<Alert icon={<IconInfoCircle size={"1rem"} />} color={"blue"}>
|
||||
<Text c={"blue"}>{t("integration.secrets.noSecretsRequired.text")}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
|
||||
<IntegrationLinkApp value={form.values.app} onChange={(app) => form.setFieldValue("app", app)} />
|
||||
|
||||
{error !== null && <IntegrationTestConnectionError error={error} url={form.values.url} />}
|
||||
|
||||
<Group justify="end" align="center">
|
||||
@@ -152,4 +203,80 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
);
|
||||
};
|
||||
|
||||
type FormType = Omit<z.infer<typeof integrationUpdateSchema>, "id">;
|
||||
type FormType = z.infer<typeof formSchema>;
|
||||
|
||||
interface IntegrationAppSelectProps {
|
||||
value: FormType["app"];
|
||||
onChange: (app: FormType["app"]) => void;
|
||||
}
|
||||
|
||||
const IntegrationLinkApp = ({ value, onChange }: IntegrationAppSelectProps) => {
|
||||
const { openModal } = useModalAction(AppSelectModal);
|
||||
const t = useI18n();
|
||||
const { data: session } = useSession();
|
||||
const canCreateApps = session?.user.permissions.includes("app-create") ?? false;
|
||||
|
||||
const handleChange = () =>
|
||||
openModal(
|
||||
{
|
||||
onSelect: onChange,
|
||||
withCreate: canCreateApps,
|
||||
},
|
||||
{
|
||||
title: t("integration.page.edit.app.action.select"),
|
||||
},
|
||||
);
|
||||
|
||||
if (!value) {
|
||||
return (
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
leftSection={<IconPlus size={16} stroke={1.5} />}
|
||||
fullWidth
|
||||
onClick={handleChange}
|
||||
>
|
||||
{t("integration.page.edit.app.action.add")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fieldset legend={t("integration.field.app.sectionTitle")}>
|
||||
<Group justify="space-between">
|
||||
<Group gap="sm">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={value.iconUrl} alt={value.name} width={32} height={32} />
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" fw="bold">
|
||||
{value.name}
|
||||
</Text>
|
||||
{value.href !== null && (
|
||||
<Anchor href={value.href} target="_blank" rel="noopener noreferrer" size="sm">
|
||||
{value.href}
|
||||
</Anchor>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
leftSection={<IconUnlink size={16} stroke={1.5} />}
|
||||
onClick={() => onChange(null)}
|
||||
>
|
||||
{t("integration.page.edit.app.action.remove")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
leftSection={<IconPencil size={16} stroke={1.5} />}
|
||||
onClick={handleChange}
|
||||
>
|
||||
{t("common.action.change")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Group>
|
||||
</Fieldset>
|
||||
);
|
||||
};
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
import type { ChangeEvent } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Flex, Group, Menu, ScrollArea, Text, TextInput } from "@mantine/core";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationName, integrationKinds } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { IntegrationAvatar } from "@homarr/ui";
|
||||
import { IntegrationAvatar, Link } from "@homarr/ui";
|
||||
|
||||
interface IntegrationCreateDropdownContentProps {
|
||||
enableMockIntegration: boolean;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { startTransition, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Alert,
|
||||
@@ -10,17 +9,21 @@ import {
|
||||
Collapse,
|
||||
Fieldset,
|
||||
Group,
|
||||
Loader,
|
||||
SegmentedControl,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
import { IconCheck, IconInfoCircle } from "@tabler/icons-react";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||
import type { Modify } from "@homarr/common/types";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
import {
|
||||
getAllSecretKindOptions,
|
||||
getIconUrl,
|
||||
@@ -28,14 +31,16 @@ import {
|
||||
getIntegrationName,
|
||||
integrationDefs,
|
||||
} from "@homarr/definitions";
|
||||
import type { UseFormReturnType } from "@homarr/form";
|
||||
import type { GetInputPropsReturnType, UseFormReturnType } from "@homarr/form";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
import { appHrefSchema } from "@homarr/validation/app";
|
||||
import { integrationCreateSchema } from "@homarr/validation/integration";
|
||||
|
||||
import { IntegrationSecretInput } from "../_components/secrets/integration-secret-inputs";
|
||||
import { SecretKindsSegmentedControl } from "../_components/secrets/integration-secret-segmented-control";
|
||||
import { IntegrationTestConnectionError } from "../_components/test-connection/integration-test-connection-error";
|
||||
import type { AnyMappedTestConnectionError } from "../_components/test-connection/types";
|
||||
|
||||
@@ -45,10 +50,11 @@ interface NewIntegrationFormProps {
|
||||
};
|
||||
}
|
||||
|
||||
const formSchema = integrationCreateSchema.omit({ kind: true }).and(
|
||||
const formSchema = integrationCreateSchema.omit({ kind: true, app: true }).and(
|
||||
z.object({
|
||||
createApp: z.boolean(),
|
||||
hasApp: z.boolean(),
|
||||
appHref: appHrefSchema,
|
||||
appId: z.string().nullable(),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -57,7 +63,8 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
const secretKinds = getAllSecretKindOptions(searchParams.kind);
|
||||
const hasUrlSecret = secretKinds.some((kinds) => kinds.includes("url"));
|
||||
const router = useRouter();
|
||||
const [opened, setOpened] = useState(false);
|
||||
const { data: session } = useSession();
|
||||
const canCreateApps = session?.user.permissions.includes("app-create") ?? false;
|
||||
|
||||
let url = searchParams.url ?? getIntegrationDefaultUrl(searchParams.kind) ?? "";
|
||||
if (hasUrlSecret) {
|
||||
@@ -73,31 +80,40 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
value: "",
|
||||
})),
|
||||
attemptSearchEngineCreation: true,
|
||||
createApp: false,
|
||||
appHref: "",
|
||||
},
|
||||
onValuesChange(values, previous) {
|
||||
if (values.createApp !== previous.createApp) {
|
||||
setOpened(values.createApp);
|
||||
}
|
||||
hasApp: false,
|
||||
appHref: url,
|
||||
appId: null,
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: createIntegrationAsync, isPending: isPendingIntegration } =
|
||||
clientApi.integration.create.useMutation();
|
||||
const { mutateAsync: createAppAsync, isPending: isPendingApp } = clientApi.app.create.useMutation();
|
||||
const isPending = isPendingIntegration || isPendingApp;
|
||||
const { mutateAsync: createIntegrationAsync, isPending } = clientApi.integration.create.useMutation();
|
||||
const [error, setError] = useState<null | AnyMappedTestConnectionError>(null);
|
||||
|
||||
const handleSubmitAsync = async (values: FormType) => {
|
||||
const handleSubmitAsync = async ({ appId, appHref, hasApp, ...values }: FormType) => {
|
||||
const url = hasUrlSecret
|
||||
? new URL(values.secrets.find((secret) => secret.kind === "url")?.value ?? values.url).origin
|
||||
: values.url;
|
||||
|
||||
const hasCustomHref = appHref !== null && appHref.trim().length >= 1;
|
||||
|
||||
const app = hasApp
|
||||
? appId !== null
|
||||
? { id: appId }
|
||||
: {
|
||||
name: values.name,
|
||||
href: hasCustomHref ? appHref : url,
|
||||
iconUrl: getIconUrl(searchParams.kind),
|
||||
description: null,
|
||||
pingUrl: url,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await createIntegrationAsync(
|
||||
{
|
||||
kind: searchParams.kind,
|
||||
...values,
|
||||
url,
|
||||
app,
|
||||
},
|
||||
{
|
||||
async onSuccess(data) {
|
||||
@@ -116,32 +132,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
message: t("integration.page.create.notification.success.message"),
|
||||
});
|
||||
|
||||
if (!values.createApp) {
|
||||
await revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
|
||||
return;
|
||||
}
|
||||
|
||||
const hasCustomHref = values.appHref !== null && values.appHref.trim().length >= 1;
|
||||
await createAppAsync(
|
||||
{
|
||||
name: values.name,
|
||||
href: hasCustomHref ? values.appHref : url,
|
||||
iconUrl: getIconUrl(searchParams.kind),
|
||||
description: null,
|
||||
pingUrl: url,
|
||||
},
|
||||
{
|
||||
async onSettled() {
|
||||
await revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
title: t("app.page.create.notification.error.title"),
|
||||
message: t("app.page.create.notification.error.message"),
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
await revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
|
||||
},
|
||||
onError: () => {
|
||||
showErrorNotification({
|
||||
@@ -195,15 +186,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
/>
|
||||
)}
|
||||
|
||||
<Checkbox
|
||||
{...form.getInputProps("createApp", { type: "checkbox" })}
|
||||
label={t("integration.field.createApp.label")}
|
||||
description={t("integration.field.createApp.description")}
|
||||
/>
|
||||
|
||||
<Collapse in={opened}>
|
||||
<TextInput placeholder={t("integration.field.appHref.placeholder")} {...form.getInputProps("appHref")} />
|
||||
</Collapse>
|
||||
<AppForm form={form} canCreateApps={canCreateApps} />
|
||||
|
||||
<Group justify="end" align="center">
|
||||
<Button variant="default" component={Link} href="/manage/integrations">
|
||||
@@ -218,40 +201,115 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
);
|
||||
};
|
||||
|
||||
interface SecretKindsSegmentedControlProps {
|
||||
secretKinds: IntegrationSecretKind[][];
|
||||
form: UseFormReturnType<FormType, (values: FormType) => FormType>;
|
||||
}
|
||||
type FormType = z.infer<typeof formSchema>;
|
||||
|
||||
const SecretKindsSegmentedControl = ({ secretKinds, form }: SecretKindsSegmentedControlProps) => {
|
||||
const t = useScopedI18n("integration.secrets");
|
||||
const AppForm = ({ form, canCreateApps }: { form: UseFormReturnType<FormType>; canCreateApps: boolean }) => {
|
||||
const t = useI18n();
|
||||
const checkboxInputProps = form.getInputProps("hasApp", { type: "checkbox" });
|
||||
|
||||
const secretKindGroups = secretKinds.map((kinds) => ({
|
||||
label:
|
||||
kinds.length === 0
|
||||
? t("noSecretsRequired.segmentTitle")
|
||||
: kinds.map((kind) => t(`kind.${kind}.label`)).join(" & "),
|
||||
value: kinds.length === 0 ? "empty" : kinds.join("-"),
|
||||
}));
|
||||
return (
|
||||
<>
|
||||
<Checkbox
|
||||
{...checkboxInputProps}
|
||||
onChange={(event) => {
|
||||
startTransition(() => {
|
||||
form.setFieldValue("appHref", event.currentTarget.checked ? form.values.url : null);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
checkboxInputProps.onChange(event);
|
||||
});
|
||||
}}
|
||||
label={t("integration.field.createApp.label")}
|
||||
description={t("integration.field.createApp.description")}
|
||||
/>
|
||||
|
||||
const onChange = useCallback(
|
||||
(value: string) => {
|
||||
if (value === "empty") {
|
||||
form.setFieldValue("secrets", []);
|
||||
return;
|
||||
}
|
||||
<Collapse in={form.values.hasApp}>
|
||||
<Fieldset legend={t("integration.field.app.sectionTitle")}>
|
||||
<Stack gap="sm">
|
||||
{canCreateApps && (
|
||||
<SegmentedControl
|
||||
data={(["new", "existing"] as const).map((value) => ({
|
||||
value,
|
||||
label: t(`integration.page.create.app.option.${value}.title`),
|
||||
}))}
|
||||
value={form.values.appHref === null ? "existing" : "new"}
|
||||
onChange={(value) => {
|
||||
if (value === "existing") {
|
||||
form.setFieldValue("appId", null);
|
||||
form.setFieldValue("appHref", null);
|
||||
} else {
|
||||
form.setFieldValue("appId", null);
|
||||
form.setFieldValue("appHref", form.values.url);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
const kinds = value.split("-") as IntegrationSecretKind[];
|
||||
const secrets = kinds.map((kind) => ({
|
||||
kind,
|
||||
value: "",
|
||||
}));
|
||||
form.setFieldValue("secrets", secrets);
|
||||
},
|
||||
[form],
|
||||
{typeof form.values.appHref === "string" && canCreateApps ? (
|
||||
<TextInput
|
||||
placeholder={t("integration.field.appHref.placeholder")}
|
||||
withAsterisk
|
||||
label={t("integration.page.create.app.option.new.url.label")}
|
||||
description={t("integration.page.create.app.option.new.url.description")}
|
||||
{...form.getInputProps("appHref")}
|
||||
/>
|
||||
) : (
|
||||
<IntegrationAppSelect {...form.getInputProps("appId")} />
|
||||
)}
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
|
||||
return <SegmentedControl fullWidth data={secretKindGroups} onChange={onChange}></SegmentedControl>;
|
||||
};
|
||||
|
||||
type FormType = z.infer<typeof formSchema>;
|
||||
type IntegrationAppSelectProps = Modify<
|
||||
GetInputPropsReturnType,
|
||||
{
|
||||
value?: string | null;
|
||||
onChange: (value: string | null) => void;
|
||||
}
|
||||
>;
|
||||
|
||||
const IntegrationAppSelect = ({ value, ...props }: IntegrationAppSelectProps) => {
|
||||
const { data, isPending } = clientApi.app.selectable.useQuery();
|
||||
const t = useI18n();
|
||||
|
||||
const appMap = new Map(data?.map((app) => [app.id, app] as const));
|
||||
|
||||
return (
|
||||
<Select
|
||||
withAsterisk
|
||||
label={t("integration.page.create.app.option.existing.label")}
|
||||
searchable
|
||||
clearable
|
||||
leftSection={
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
value ? <img width={20} height={20} src={appMap.get(value)?.iconUrl} alt={appMap.get(value)?.name} /> : null
|
||||
}
|
||||
renderOption={({ option, checked }) => (
|
||||
<Group flex="1" gap="xs">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img width={20} height={20} src={appMap.get(option.value)?.iconUrl} alt={option.label} />
|
||||
<Stack gap={0}>
|
||||
<Text>{option.label}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{appMap.get(option.value)?.href}
|
||||
</Text>
|
||||
</Stack>
|
||||
{checked && (
|
||||
<IconCheck
|
||||
style={{ marginInlineStart: "auto" }}
|
||||
stroke={1.5}
|
||||
color="currentColor"
|
||||
opacity={0.6}
|
||||
size={18}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
{...props}
|
||||
data={data?.map((app) => ({ value: app.id, label: app.name }))}
|
||||
rightSection={isPending ? <Loader size="sm" /> : null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Fragment } from "react";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import {
|
||||
AccordionControl,
|
||||
@@ -36,7 +35,7 @@ import { objectEntries } from "@homarr/common";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
import { getIntegrationName } from "@homarr/definitions";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
import { CountBadge, IntegrationAvatar } from "@homarr/ui";
|
||||
import { CountBadge, IntegrationAvatar, Link } from "@homarr/ui";
|
||||
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
|
||||
@@ -14,7 +14,7 @@ export const UploadMediaButton = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<UploadMedia onSettled={onSettledAsync}>
|
||||
<UploadMedia onSettled={onSettledAsync} multiple>
|
||||
{({ onClick, loading }) => (
|
||||
<Button onClick={onClick} loading={loading} rightSection={<IconUpload size={16} stroke={1.5} />}>
|
||||
{t("media.action.upload.label")}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
ActionIcon,
|
||||
@@ -25,7 +24,7 @@ import { humanFileSize } from "@homarr/common";
|
||||
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
|
||||
import { createLocalImageUrl } from "@homarr/icons/local";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
import { SearchInput, TablePagination, UserAvatar } from "@homarr/ui";
|
||||
import { Link, SearchInput, TablePagination, UserAvatar } from "@homarr/ui";
|
||||
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
import Link from "next/link";
|
||||
import type { Metadata } from "next";
|
||||
import { Card, Group, SimpleGrid, Space, Stack, Text } from "@mantine/core";
|
||||
import { IconArrowRight } from "@tabler/icons-react";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { isProviderEnabled } from "@homarr/auth/server";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
import { HeroBanner } from "./_components/hero-banner";
|
||||
|
||||
interface LinkProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
count: number;
|
||||
href: string;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export async function generateMetadata() {
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getScopedI18n("management");
|
||||
|
||||
return {
|
||||
@@ -29,78 +20,32 @@ export async function generateMetadata() {
|
||||
|
||||
export default async function ManagementPage() {
|
||||
const statistics = await api.home.getStats();
|
||||
const session = await auth();
|
||||
const t = await getScopedI18n("management.page.home");
|
||||
|
||||
const links: LinkProps[] = [
|
||||
{
|
||||
count: statistics.countBoards,
|
||||
href: "/manage/boards",
|
||||
subtitle: t("statisticLabel.boards"),
|
||||
title: t("statistic.board"),
|
||||
},
|
||||
{
|
||||
count: statistics.countUsers,
|
||||
href: "/manage/users",
|
||||
subtitle: t("statisticLabel.authentication"),
|
||||
title: t("statistic.user"),
|
||||
hidden: !session?.user.permissions.includes("admin"),
|
||||
},
|
||||
{
|
||||
count: statistics.countInvites,
|
||||
href: "/manage/users/invites",
|
||||
subtitle: t("statisticLabel.authentication"),
|
||||
title: t("statistic.invite"),
|
||||
hidden: !isProviderEnabled("credentials") || !session?.user.permissions.includes("admin"),
|
||||
},
|
||||
{
|
||||
count: statistics.countIntegrations,
|
||||
href: "/manage/integrations",
|
||||
subtitle: t("statisticLabel.resources"),
|
||||
title: t("statistic.integration"),
|
||||
},
|
||||
{
|
||||
count: statistics.countApps,
|
||||
href: "/manage/apps",
|
||||
subtitle: t("statisticLabel.resources"),
|
||||
title: t("statistic.app"),
|
||||
hidden: !session?.user,
|
||||
},
|
||||
{
|
||||
count: statistics.countGroups,
|
||||
href: "/manage/users/groups",
|
||||
subtitle: t("statisticLabel.authorization"),
|
||||
title: t("statistic.group"),
|
||||
hidden: !session?.user.permissions.includes("admin"),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb />
|
||||
<HeroBanner />
|
||||
<Space h="md" />
|
||||
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
|
||||
{links.map(
|
||||
(link) =>
|
||||
!link.hidden && (
|
||||
<Card component={Link} href={link.href} key={link.href} radius="lg">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group wrap="nowrap">
|
||||
<Text size="2.4rem" fw="bolder">
|
||||
{link.count}
|
||||
</Text>
|
||||
<Stack gap={0}>
|
||||
<Text c="red" size="xs">
|
||||
{link.subtitle}
|
||||
</Text>
|
||||
<Text fw="bold">{link.title}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<IconArrowRight />
|
||||
</Group>
|
||||
</Card>
|
||||
),
|
||||
)}
|
||||
{statistics.map((statistic) => (
|
||||
<Card component={Link} href={statistic.path} key={statistic.path} radius="lg">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group wrap="nowrap">
|
||||
<Text size="2.4rem" fw="bolder">
|
||||
{statistic.count}
|
||||
</Text>
|
||||
<Stack gap={0}>
|
||||
<Text c="red" size="xs">
|
||||
{t(`statisticLabel.${statistic.subtitleKey}`)}
|
||||
</Text>
|
||||
<Text fw="bold">{t(`statistic.${statistic.titleKey}`)}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<IconArrowRight />
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import type { SegmentedControlItem } from "@mantine/core";
|
||||
import { Button, Fieldset, Grid, Group, SegmentedControl, Stack, Textarea, TextInput } from "@mantine/core";
|
||||
import { WidgetIntegrationSelect } from "node_modules/@homarr/widgets/src/widget-integration-select";
|
||||
@@ -12,6 +11,7 @@ import { useZodForm } from "@homarr/form";
|
||||
import { IconPicker } from "@homarr/forms-collection";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
import { searchEngineManageSchema } from "@homarr/validation/search-engine";
|
||||
|
||||
type FormType = z.infer<typeof searchEngineManageSchema>;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconPencil, IconSearch } from "@tabler/icons-react";
|
||||
@@ -9,7 +8,7 @@ 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 { Link, SearchInput, TablePagination } from "@homarr/ui";
|
||||
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { X509Certificate } from "node:crypto";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import {
|
||||
Button,
|
||||
@@ -19,6 +18,7 @@ import { IconCertificateOff } from "@tabler/icons-react";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { getTrustedCertificateHostnamesAsync } from "@homarr/certificates/server";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { NoResults } from "~/components/no-results";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { X509Certificate } from "node:crypto";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Button, Card, Group, SimpleGrid, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconAlertTriangle, IconCertificate, IconCertificateOff } from "@tabler/icons-react";
|
||||
@@ -10,6 +9,7 @@ import { loadCustomRootCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { getMantineColor } from "@homarr/common";
|
||||
import type { SupportedLanguage } from "@homarr/translation";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { NoResults } from "~/components/no-results";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Anchor, Center, Stack, Text } from "@mantine/core";
|
||||
import { IconShipOff } from "@tabler/icons-react";
|
||||
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
export default function DockerErrorPage() {
|
||||
const t = useI18n();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Anchor, Center, Stack, Text } from "@mantine/core";
|
||||
import { IconCubeOff } from "@tabler/icons-react";
|
||||
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
export default function KubernetesErrorPage() {
|
||||
const t = useI18n();
|
||||
|
||||
+1
-1
@@ -1,11 +1,11 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Card, Group, Text } from "@mantine/core";
|
||||
import { IconArrowRight } from "@tabler/icons-react";
|
||||
|
||||
import type { KubernetesLabelResourceType } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import classes from "./resource-tile.module.css";
|
||||
|
||||
|
||||
+14
-28
@@ -92,38 +92,24 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
||||
});
|
||||
}, [mutate, user.id, openConfirmModal, tManageAvatar]);
|
||||
|
||||
const isCredentialsUser = user.provider === "credentials";
|
||||
|
||||
return (
|
||||
<Box pos="relative">
|
||||
<Menu
|
||||
opened={opened}
|
||||
keepMounted
|
||||
onChange={isCredentialsUser ? toggle : undefined}
|
||||
position="bottom-start"
|
||||
withArrow
|
||||
>
|
||||
<Menu opened={opened} keepMounted onChange={toggle} position="bottom-start" withArrow>
|
||||
<Menu.Target>
|
||||
<UnstyledButton
|
||||
component={isCredentialsUser ? undefined : "div"}
|
||||
style={{ cursor: !isCredentialsUser ? "default" : undefined }}
|
||||
onClick={isCredentialsUser ? toggle : undefined}
|
||||
>
|
||||
<UnstyledButton onClick={toggle}>
|
||||
<UserAvatar user={user} size={200} />
|
||||
{isCredentialsUser && (
|
||||
<Button
|
||||
component="div"
|
||||
pos="absolute"
|
||||
bottom={0}
|
||||
left={0}
|
||||
size="compact-md"
|
||||
fw="normal"
|
||||
variant="default"
|
||||
leftSection={<IconPencil size={18} stroke={1.5} />}
|
||||
>
|
||||
{t("common.action.edit")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
component="div"
|
||||
pos="absolute"
|
||||
bottom={0}
|
||||
left={0}
|
||||
size="compact-md"
|
||||
fw="normal"
|
||||
variant="default"
|
||||
leftSection={<IconPencil size={18} stroke={1.5} />}
|
||||
>
|
||||
{t("common.action.edit")}
|
||||
</Button>
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { Anchor, Button, Group, Text, Title, Tooltip } from "@mantine/core";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
import type { MRT_ColumnDef } from "mantine-react-table";
|
||||
@@ -10,7 +9,7 @@ import { MantineReactTable } from "mantine-react-table";
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { UserAvatar } from "@homarr/ui";
|
||||
import { Link, UserAvatar } from "@homarr/ui";
|
||||
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
||||
|
||||
interface UserListComponentProps {
|
||||
|
||||
+7
-8
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { startTransition, useCallback, useMemo, useState } from "react";
|
||||
import { startTransition, useCallback, useState } from "react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -20,8 +20,9 @@ import { IconPlus, IconUserCheck } from "@tabler/icons-react";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { everyoneGroup, groupPermissions } from "@homarr/definitions";
|
||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
||||
import { everyoneGroup, groupPermissions } from "@homarr/definitions";
|
||||
import type { IsValid } from "@homarr/form";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { showErrorNotification } from "@homarr/notifications";
|
||||
@@ -113,11 +114,9 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
|
||||
},
|
||||
);
|
||||
|
||||
const allForms = useMemo(() => [generalForm, securityForm, groupsForm], [generalForm, securityForm, groupsForm]);
|
||||
|
||||
const activeForm = allForms[active];
|
||||
const isCurrentFormValid = activeForm ? activeForm.isValid : () => true;
|
||||
const canNavigateToNextStep = isCurrentFormValid();
|
||||
const allForms = [generalForm, securityForm, groupsForm];
|
||||
const isValidCallback: IsValid<unknown> | undefined = allForms[active]?.isValid;
|
||||
const currentFormValid = isValidCallback?.() ?? true;
|
||||
|
||||
const controlledGoToNextStep = useCallback(async () => {
|
||||
if (active + 1 === stepperMax) {
|
||||
@@ -218,7 +217,7 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
|
||||
</Stepper.Completed>
|
||||
</Stepper>
|
||||
<StepperNavigationComponent
|
||||
hasNext={hasNext && canNavigateToNextStep}
|
||||
hasNext={hasNext && currentFormValid}
|
||||
hasPrevious={hasPrevious}
|
||||
isComplete={active === stepperMax}
|
||||
isLoadingNextStep={isPending}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Link from "next/link";
|
||||
import { Button, Card, Group } from "@mantine/core";
|
||||
import { IconArrowBackUp, IconArrowLeft, IconArrowRight, IconRotate } from "@tabler/icons-react";
|
||||
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
interface StepperNavigationComponentProps {
|
||||
hasPrevious: boolean;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { NavLink } from "@mantine/core";
|
||||
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
interface NavigationLinkProps {
|
||||
href: string;
|
||||
label: string;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Alert, Anchor } from "@mantine/core";
|
||||
import { IconExclamationCircle } from "@tabler/icons-react";
|
||||
|
||||
import { createDocumentationLink } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
export const ReservedGroupAlert = () => {
|
||||
const t = useI18n();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import Link from "next/link";
|
||||
import { Button, Grid, GridCol, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconId, IconLock, IconSettings, IconUsersGroup } from "@tabler/icons-react";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
import { NavigationLink } from "./_navigation";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Alert, Anchor, Center, Group, Stack, Table, TableTbody, TableTd, TableTr, Text, Title } from "@mantine/core";
|
||||
import { IconExclamationCircle } from "@tabler/icons-react";
|
||||
@@ -10,7 +9,7 @@ import { auth } from "@homarr/auth/next";
|
||||
import { isProviderEnabled } from "@homarr/auth/server";
|
||||
import { everyoneGroup } from "@homarr/definitions";
|
||||
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
||||
import { SearchInput, UserAvatar } from "@homarr/ui";
|
||||
import { Link, SearchInput, UserAvatar } from "@homarr/ui";
|
||||
|
||||
import { ReservedGroupAlert } from "../_reserved-group-alert";
|
||||
import { AddGroupMember } from "./_add-group-member";
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import type { DragEndEvent, DraggableAttributes, DragStartEvent } from "@dnd-kit/core";
|
||||
import {
|
||||
closestCenter,
|
||||
@@ -41,7 +40,7 @@ import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import { showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { UserAvatarGroup } from "@homarr/ui";
|
||||
import { Link, UserAvatarGroup } from "@homarr/ui";
|
||||
|
||||
interface GroupsTableProps {
|
||||
initialGroupIds: string[];
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Card, Group, Stack, Text, ThemeIcon, Title, UnstyledButton } from "@mantine/core";
|
||||
import { IconChevronRight, IconUsersGroup } from "@tabler/icons-react";
|
||||
@@ -7,6 +6,7 @@ import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { everyoneGroup } from "@homarr/definitions";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
|
||||
import { db, eq } from "@homarr/db";
|
||||
import { medias } from "@homarr/db/schema";
|
||||
@@ -19,11 +20,24 @@ export async function GET(_req: NextRequest, props: { params: Promise<{ id: stri
|
||||
notFound();
|
||||
}
|
||||
|
||||
let content = new Uint8Array(image.content);
|
||||
|
||||
// Sanitize SVG content to prevent XSS attacks
|
||||
if (image.contentType === "image/svg+xml" || image.contentType === "image/svg") {
|
||||
const svgText = new TextDecoder().decode(content);
|
||||
const sanitized = DOMPurify.sanitize(svgText, {
|
||||
USE_PROFILES: { svg: true, svgFilters: true },
|
||||
});
|
||||
content = new TextEncoder().encode(sanitized);
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
headers.set("Content-Type", image.contentType);
|
||||
headers.set("Content-Length", image.content.length.toString());
|
||||
headers.set("Content-Length", content.length.toString());
|
||||
headers.set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox");
|
||||
headers.set("X-Content-Type-Options", "nosniff");
|
||||
|
||||
return new NextResponse(new Uint8Array(image.content), {
|
||||
return new NextResponse(content, {
|
||||
status: 200,
|
||||
headers,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Anchor, Button, Group, Stack, Table, TableTbody, TableTh, TableThead, TableTr } from "@mantine/core";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import type { AccessQueryData } from "./access-settings";
|
||||
import { AccessSelectRow } from "./access-table-rows";
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Anchor, Box, Button, Group, Stack, Table, TableTbody, TableTh, TableThead, TableTr } from "@mantine/core";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { UserAvatar } from "@homarr/ui";
|
||||
import { Link, UserAvatar } from "@homarr/ui";
|
||||
|
||||
import type { AccessQueryData } from "./access-settings";
|
||||
import { AccessDisplayRow, AccessSelectRow } from "./access-table-rows";
|
||||
|
||||
@@ -12,6 +12,7 @@ export class DynamicSectionMockBuilder {
|
||||
options: {
|
||||
title: "",
|
||||
borderColor: "",
|
||||
customCssClasses: [],
|
||||
},
|
||||
layouts: [],
|
||||
...section,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Badge, Box, Card } from "@mantine/core";
|
||||
import combineClasses from "clsx";
|
||||
|
||||
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
|
||||
|
||||
@@ -24,7 +25,7 @@ export const BoardDynamicSection = ({ section }: Props) => {
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
className={classes.itemCard}
|
||||
className={combineClasses(classes.itemCard, section.options.customCssClasses.join(" "))}
|
||||
w="100%"
|
||||
h="100%"
|
||||
withBorder
|
||||
|
||||
@@ -19,6 +19,7 @@ export const addDynamicSectionCallback = () => (board: Board) => {
|
||||
options: {
|
||||
title: "",
|
||||
borderColor: "",
|
||||
customCssClasses: [],
|
||||
},
|
||||
layouts: createDynamicSectionLayouts(board, firstSection),
|
||||
} satisfies DynamicSection;
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { z } from "zod/v4";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { createModal } from "@homarr/modals";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { TextMultiSelect } from "@homarr/ui";
|
||||
import { dynamicSectionOptionsSchema } from "@homarr/validation/shared";
|
||||
|
||||
interface ModalProps {
|
||||
@@ -31,6 +32,10 @@ export const DynamicSectionEditModal = createModal<ModalProps>(({ actions, inner
|
||||
>
|
||||
<Stack>
|
||||
<TextInput label={t("section.dynamic.option.title.label")} {...form.getInputProps("title")} />
|
||||
<TextMultiSelect
|
||||
label={t("section.dynamic.option.customCssClasses.label")}
|
||||
{...form.getInputProps("customCssClasses")}
|
||||
/>
|
||||
<ColorInput
|
||||
label={t("section.dynamic.option.borderColor.label")}
|
||||
format="hex"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.flagIcon {
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -6,8 +6,7 @@ import { IconCheck } from "@tabler/icons-react";
|
||||
|
||||
import type { SupportedLanguage } from "@homarr/translation";
|
||||
import { localeConfigurations, supportedLanguages } from "@homarr/translation";
|
||||
|
||||
import classes from "./language-combobox.module.css";
|
||||
import { LanguageIcon } from "@homarr/ui";
|
||||
|
||||
import "flag-icons/css/flag-icons.min.css";
|
||||
|
||||
@@ -84,7 +83,7 @@ const OptionItem = ({
|
||||
return (
|
||||
<Group wrap="nowrap" justify="space-between">
|
||||
<Group wrap="nowrap">
|
||||
<span className={`fi fi-${localeConfigurations[localeKey].flagIcon} ${classes.flagIcon}`}></span>
|
||||
<LanguageIcon icon={localeConfigurations[localeKey].icon} />
|
||||
<Group wrap="nowrap" gap="xs">
|
||||
<Text>{localeConfigurations[localeKey].name}</Text>
|
||||
<Text size="xs" c="dimmed" inherit>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import Script from "next/script";
|
||||
|
||||
import type { SupportedLanguage } from "@homarr/translation";
|
||||
|
||||
export const CrowdinLiveTranslation = (props: { locale: SupportedLanguage }) => {
|
||||
if (props.locale !== "cr") return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script type="text/javascript" src="//cdn.crowdin.com/jipt/jipt.js"></Script>
|
||||
|
||||
<Script type="text/javascript" id="crowdin-configuration">
|
||||
{`var _jipt = []; _jipt.push(['project', 'homarr_labs']);`}
|
||||
</Script>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { AppShellHeader, Group, UnstyledButton } from "@mantine/core";
|
||||
|
||||
import { Spotlight } from "@homarr/spotlight";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import { ClientBurger } from "./header/burger";
|
||||
import { DesktopSearchInput, MobileSearchButton } from "./header/search";
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { ForwardedRef, ReactNode } from "react";
|
||||
import { forwardRef } from "react";
|
||||
import Link from "next/link";
|
||||
import type { ActionIconProps } from "@mantine/core";
|
||||
import { ActionIcon } from "@mantine/core";
|
||||
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
type HeaderButtonProps = (
|
||||
| {
|
||||
onClick?: () => void;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { NavLink } from "@mantine/core";
|
||||
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
export const CommonNavLink = (props: ClientNavigationLink) =>
|
||||
"href" in props ? <NavLinkHref {...props} /> : <NavLinkWithItems {...props} />;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Center, Menu, Stack, Text, useMantineColorScheme } from "@mantine/core";
|
||||
import { useHotkeys, useTimeout } from "@mantine/hooks";
|
||||
@@ -19,8 +18,10 @@ import {
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { signOut, useSession } from "@homarr/auth/client";
|
||||
import { hotkeys } from "@homarr/definitions";
|
||||
import { createModal, useModalAction } from "@homarr/modals";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import { useAuthContext } from "~/app/[locale]/_client-providers/session";
|
||||
import { CurrentLanguageCombobox } from "./language/current-language-combobox";
|
||||
@@ -34,7 +35,7 @@ interface UserAvatarMenuProps {
|
||||
export const UserAvatarMenu = ({ children, availableUpdatesPromise }: UserAvatarMenuProps) => {
|
||||
const t = useScopedI18n("common.userAvatar.menu");
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||
useHotkeys([["mod+J", toggleColorScheme]]);
|
||||
useHotkeys([[hotkeys.toggleColorScheme, toggleColorScheme]]);
|
||||
|
||||
const ColorSchemeIcon = colorScheme === "dark" ? IconSun : IconMoon;
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { createTRPCClient, httpLink } from "@trpc/client";
|
||||
import SuperJSON from "superjson";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import type { AppRouter } from "@homarr/api";
|
||||
import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
|
||||
import { api } from "@homarr/api/server";
|
||||
import { localeCookieKey } from "@homarr/definitions";
|
||||
import type { SupportedLanguage } from "@homarr/translation";
|
||||
import { supportedLanguages } from "@homarr/translation";
|
||||
@@ -12,12 +9,12 @@ import { createI18nMiddleware } from "@homarr/translation/middleware";
|
||||
|
||||
let isOnboardingFinished = false;
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
export async function proxy(request: NextRequest) {
|
||||
// Redirect to onboarding if it's not finished yet
|
||||
const pathname = request.nextUrl.pathname;
|
||||
|
||||
if (!isOnboardingFinished && !pathname.endsWith("/init")) {
|
||||
const currentOnboardingStep = await serverFetchApi.onboard.currentStep.query();
|
||||
const currentOnboardingStep = await api.onboard.currentStep();
|
||||
if (currentOnboardingStep.current !== "finish") {
|
||||
return NextResponse.redirect(new URL("/init", request.url));
|
||||
}
|
||||
@@ -29,11 +26,12 @@ export async function middleware(request: NextRequest) {
|
||||
const currentLocale = request.cookies.get(localeCookieKey)?.value;
|
||||
let defaultLocale: SupportedLanguage = "en";
|
||||
if (!currentLocale || !supportedLanguages.includes(currentLocale as SupportedLanguage)) {
|
||||
defaultLocale = await serverFetchApi.serverSettings.getCulture.query().then((culture) => culture.defaultLocale);
|
||||
defaultLocale = await api.serverSettings.getCulture().then((culture) => culture.defaultLocale);
|
||||
}
|
||||
|
||||
// We don't want to fallback to accept-language header so we clear it
|
||||
request.headers.set("accept-language", "");
|
||||
|
||||
const next = createI18nMiddleware(defaultLocale);
|
||||
return next(request);
|
||||
}
|
||||
@@ -41,13 +39,3 @@ export async function middleware(request: NextRequest) {
|
||||
export const config = {
|
||||
matcher: ["/((?!api|static|.*\\..*|_next|favicon.ico|robots.txt).*)"],
|
||||
};
|
||||
|
||||
export const serverFetchApi = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
httpLink({
|
||||
url: getTrpcUrl(),
|
||||
transformer: SuperJSON,
|
||||
headers: createHeadersCallbackForSource("server-fetch"),
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -37,22 +37,22 @@
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@homarr/widgets": "workspace:^0.1.0",
|
||||
"dayjs": "^1.11.18",
|
||||
"dotenv": "^17.2.2",
|
||||
"fastify": "^5.6.0",
|
||||
"superjson": "2.2.2",
|
||||
"dayjs": "^1.11.19",
|
||||
"dotenv": "^17.2.3",
|
||||
"fastify": "^5.6.2",
|
||||
"superjson": "2.2.5",
|
||||
"undici": "7.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node": "^22.18.3",
|
||||
"dotenv-cli": "^10.0.0",
|
||||
"esbuild": "^0.25.9",
|
||||
"eslint": "^9.35.0",
|
||||
"@types/node": "^24.10.0",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"esbuild": "^0.26.0",
|
||||
"eslint": "^9.39.1",
|
||||
"prettier": "^3.6.2",
|
||||
"tsx": "4.20.4",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
"@homarr/log": "workspace:^",
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"dotenv": "^17.2.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"tsx": "4.20.4",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
@@ -34,9 +34,9 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"esbuild": "^0.25.9",
|
||||
"eslint": "^9.35.0",
|
||||
"esbuild": "^0.26.0",
|
||||
"eslint": "^9.39.1",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
+26
-26
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "1.38.0",
|
||||
"version": "1.43.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env CI=true turbo build",
|
||||
@@ -39,28 +39,28 @@
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/commit-analyzer": "^13.0.1",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/github": "^11.0.6",
|
||||
"@semantic-release/npm": "^12.0.2",
|
||||
"@semantic-release/github": "^12.0.2",
|
||||
"@semantic-release/npm": "^13.1.1",
|
||||
"@semantic-release/release-notes-generator": "^14.1.0",
|
||||
"@testcontainers/redis": "^11.5.1",
|
||||
"@turbo/gen": "^2.5.6",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"@testcontainers/redis": "^11.8.0",
|
||||
"@turbo/gen": "^2.6.0",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"@vitest/coverage-v8": "^4.0.8",
|
||||
"@vitest/ui": "^4.0.8",
|
||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
||||
"cross-env": "^10.0.0",
|
||||
"jsdom": "^27.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"jsdom": "^27.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"semantic-release": "^24.2.8",
|
||||
"testcontainers": "^11.5.1",
|
||||
"turbo": "^2.5.6",
|
||||
"typescript": "^5.9.2",
|
||||
"semantic-release": "^25.0.2",
|
||||
"testcontainers": "^11.8.0",
|
||||
"turbo": "^2.6.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.8"
|
||||
},
|
||||
"packageManager": "pnpm@10.16.1",
|
||||
"packageManager": "pnpm@10.20.0",
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
"node": ">=24.11.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
@@ -77,23 +77,23 @@
|
||||
"overrides": {
|
||||
"@babel/helpers@<7.26.10": ">=7.28.4",
|
||||
"@babel/runtime@<7.26.10": ">=7.28.4",
|
||||
"axios@>=1.0.0 <1.8.2": ">=1.12.2",
|
||||
"axios@>=1.0.0 <1.8.2": ">=1.13.2",
|
||||
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
|
||||
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
|
||||
"esbuild@<=0.24.2": ">=0.25.9",
|
||||
"esbuild@<=0.24.2": ">=0.26.0",
|
||||
"form-data@>=4.0.0 <4.0.4": ">=4.0.4",
|
||||
"hono@<4.6.5": ">=4.9.7",
|
||||
"hono@<4.6.5": ">=4.10.4",
|
||||
"linkifyjs@<4.3.2": ">=4.3.2",
|
||||
"nanoid@>=4.0.0 <5.0.9": ">=5.1.5",
|
||||
"nanoid@>=4.0.0 <5.0.9": ">=5.1.6",
|
||||
"prismjs@<1.30.0": ">=1.30.0",
|
||||
"proxmox-api>undici": "7.16.0",
|
||||
"react-is": "^19.1.1",
|
||||
"rollup@>=4.0.0 <4.22.4": ">=4.50.1",
|
||||
"react-is": "^19.2.0",
|
||||
"rollup@>=4.0.0 <4.22.4": ">=4.53.1",
|
||||
"sha.js@<=2.4.11": ">=2.4.12",
|
||||
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.0",
|
||||
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.0",
|
||||
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
|
||||
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
|
||||
"tmp@<=0.2.3": ">=0.2.5",
|
||||
"vite@>=5.0.0 <=5.4.18": ">=7.1.5"
|
||||
"vite@>=5.0.0 <=5.4.18": ">=7.2.2"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@types/node-unifi": "patches/@types__node-unifi.patch",
|
||||
|
||||
@@ -26,13 +26,13 @@
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@umami/node": "^0.4.0",
|
||||
"superjson": "2.2.2"
|
||||
"superjson": "2.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
+15
-14
@@ -40,27 +40,28 @@
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"@homarr/request-handler": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@kubernetes/client-node": "^1.3.0",
|
||||
"@tanstack/react-query": "^5.87.4",
|
||||
"@trpc/client": "^11.5.1",
|
||||
"@trpc/react-query": "^11.5.1",
|
||||
"@trpc/server": "^11.5.1",
|
||||
"@trpc/tanstack-react-query": "^11.5.1",
|
||||
"@kubernetes/client-node": "^1.4.0",
|
||||
"@tanstack/react-query": "^5.90.7",
|
||||
"@trpc/client": "^11.7.1",
|
||||
"@trpc/react-query": "^11.7.1",
|
||||
"@trpc/server": "^11.7.1",
|
||||
"@trpc/tanstack-react-query": "^11.7.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"next": "15.5.3",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"superjson": "2.2.2",
|
||||
"trpc-to-openapi": "^3.0.1",
|
||||
"zod": "^4.1.8"
|
||||
"next": "16.0.1",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"superjson": "2.2.5",
|
||||
"trpc-to-openapi": "^3.1.0",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.39.1",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export const createOneIntegrationMiddleware = <TKind extends IntegrationKind>(
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
where: and(eq(integrations.id, input.integrationId), inArray(integrations.kind, kinds)),
|
||||
with: {
|
||||
app: true,
|
||||
secrets: true,
|
||||
groupPermissions: true,
|
||||
userPermissions: true,
|
||||
@@ -65,6 +66,7 @@ export const createOneIntegrationMiddleware = <TKind extends IntegrationKind>(
|
||||
ctx: {
|
||||
integration: {
|
||||
...rest,
|
||||
externalUrl: rest.app?.href ?? null,
|
||||
kind: kind as TKind,
|
||||
decryptedSecrets: secrets.map((secret) => ({
|
||||
...secret,
|
||||
@@ -96,6 +98,7 @@ export const createManyIntegrationMiddleware = <TKind extends IntegrationKind>(
|
||||
? await ctx.db.query.integrations.findMany({
|
||||
where: and(inArray(integrations.id, input.integrationIds), inArray(integrations.kind, kinds)),
|
||||
with: {
|
||||
app: true,
|
||||
secrets: true,
|
||||
items: {
|
||||
with: {
|
||||
@@ -125,6 +128,7 @@ export const createManyIntegrationMiddleware = <TKind extends IntegrationKind>(
|
||||
integrations: dbIntegrations.map(
|
||||
({ secrets, kind, items: _ignore1, groupPermissions: _ignore2, userPermissions: _ignore3, ...rest }) => ({
|
||||
...rest,
|
||||
externalUrl: rest.app?.href ?? null,
|
||||
kind: kind as TKind,
|
||||
decryptedSecrets: secrets.map((secret) => ({
|
||||
...secret,
|
||||
|
||||
@@ -118,20 +118,22 @@ export const appRouter = createTRPCRouter({
|
||||
create: permissionRequiredProcedure
|
||||
.requiresPermission("app-create")
|
||||
.input(appManageSchema)
|
||||
.output(z.object({ appId: z.string() }))
|
||||
.output(z.object({ appId: z.string() }).and(selectAppSchema))
|
||||
.meta({ openapi: { method: "POST", path: "/api/apps", tags: ["apps"], protect: true } })
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const id = createId();
|
||||
await ctx.db.insert(apps).values({
|
||||
const insertValues = {
|
||||
id,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
iconUrl: input.iconUrl,
|
||||
href: input.href,
|
||||
pingUrl: input.pingUrl === "" ? null : input.pingUrl,
|
||||
});
|
||||
};
|
||||
await ctx.db.insert(apps).values(insertValues);
|
||||
|
||||
return { appId: id };
|
||||
// TODO: breaking change necessary for removing appId property
|
||||
return { appId: id, ...insertValues };
|
||||
}),
|
||||
createMany: permissionRequiredProcedure
|
||||
.requiresPermission("app-create")
|
||||
|
||||
+136
-20
@@ -1,31 +1,147 @@
|
||||
import type { AnySQLiteTable } from "drizzle-orm/sqlite-core";
|
||||
|
||||
import { isProviderEnabled } from "@homarr/auth/server";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { apps, boards, groups, integrations, invites, users } from "@homarr/db/schema";
|
||||
import { db, eq, inArray, or } from "@homarr/db";
|
||||
import {
|
||||
apps,
|
||||
boards,
|
||||
boardUserPermissions,
|
||||
groupMembers,
|
||||
groups,
|
||||
integrations,
|
||||
invites,
|
||||
medias,
|
||||
searchEngines,
|
||||
users,
|
||||
} from "@homarr/db/schema";
|
||||
import type { TranslationObject } from "@homarr/translation";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
|
||||
interface HomeStatistic {
|
||||
titleKey: keyof TranslationObject["management"]["page"]["home"]["statistic"];
|
||||
subtitleKey: keyof TranslationObject["management"]["page"]["home"]["statisticLabel"];
|
||||
count: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export const homeRouter = createTRPCRouter({
|
||||
getStats: publicProcedure.query(async ({ ctx }) => {
|
||||
const isAdmin = ctx.session?.user.permissions.includes("admin") ?? false;
|
||||
const isCredentialsEnabled = isProviderEnabled("credentials");
|
||||
|
||||
return {
|
||||
countBoards: await getCountForTableAsync(ctx.db, boards, true),
|
||||
countUsers: await getCountForTableAsync(ctx.db, users, isAdmin),
|
||||
countGroups: await getCountForTableAsync(ctx.db, groups, true),
|
||||
countInvites: await getCountForTableAsync(ctx.db, invites, isAdmin),
|
||||
countIntegrations: await getCountForTableAsync(ctx.db, integrations, isCredentialsEnabled && isAdmin),
|
||||
countApps: await getCountForTableAsync(ctx.db, apps, true),
|
||||
};
|
||||
const statistics: HomeStatistic[] = [];
|
||||
|
||||
const boardIds: string[] = [];
|
||||
if (ctx.session?.user && !ctx.session.user.permissions.includes("board-view-all")) {
|
||||
const permissionsOfCurrentUserWhenPresent = await ctx.db.query.boardUserPermissions.findMany({
|
||||
where: eq(boardUserPermissions.userId, ctx.session.user.id),
|
||||
});
|
||||
|
||||
const permissionsOfCurrentUserGroupsWhenPresent = await ctx.db.query.groupMembers.findMany({
|
||||
where: eq(groupMembers.userId, ctx.session.user.id),
|
||||
with: {
|
||||
group: {
|
||||
with: {
|
||||
boardPermissions: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
boardIds.push(
|
||||
...permissionsOfCurrentUserWhenPresent
|
||||
.map((permission) => permission.boardId)
|
||||
.concat(
|
||||
permissionsOfCurrentUserGroupsWhenPresent
|
||||
.map((groupMember) => groupMember.group.boardPermissions.map((permission) => permission.boardId))
|
||||
.flat(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
statistics.push({
|
||||
titleKey: "board",
|
||||
subtitleKey: "boards",
|
||||
count: await db.$count(
|
||||
boards,
|
||||
ctx.session?.user.permissions.includes("board-view-all")
|
||||
? undefined
|
||||
: or(
|
||||
eq(boards.isPublic, true),
|
||||
eq(boards.creatorId, ctx.session?.user.id ?? ""),
|
||||
boardIds.length > 0 ? inArray(boards.id, boardIds) : undefined,
|
||||
),
|
||||
),
|
||||
path: "/manage/boards",
|
||||
});
|
||||
|
||||
if (isAdmin) {
|
||||
statistics.push({
|
||||
titleKey: "user",
|
||||
subtitleKey: "authentication",
|
||||
count: await db.$count(users),
|
||||
path: "/manage/users",
|
||||
});
|
||||
}
|
||||
|
||||
if (isAdmin && isCredentialsEnabled) {
|
||||
statistics.push({
|
||||
titleKey: "invite",
|
||||
subtitleKey: "authentication",
|
||||
count: await db.$count(invites),
|
||||
path: "/manage/users/invites",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.session?.user.permissions.includes("integration-create")) {
|
||||
statistics.push({
|
||||
titleKey: "integration",
|
||||
subtitleKey: "resources",
|
||||
count: await db.$count(integrations),
|
||||
path: "/manage/integrations",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.session?.user) {
|
||||
statistics.push({
|
||||
titleKey: "app",
|
||||
subtitleKey: "resources",
|
||||
count: await db.$count(apps),
|
||||
path: "/manage/apps",
|
||||
});
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
statistics.push({
|
||||
titleKey: "group",
|
||||
subtitleKey: "authorization",
|
||||
count: await db.$count(groups),
|
||||
path: "/manage/users/groups",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.session?.user.permissions.includes("search-engine-create")) {
|
||||
statistics.push({
|
||||
titleKey: "searchEngine",
|
||||
subtitleKey: "resources",
|
||||
count: await db.$count(searchEngines),
|
||||
path: "/manage/search-engines",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.session?.user.permissions.includes("media-upload")) {
|
||||
statistics.push({
|
||||
titleKey: "media",
|
||||
subtitleKey: "resources",
|
||||
count: await db.$count(
|
||||
medias,
|
||||
ctx.session.user.permissions.includes("media-view-all")
|
||||
? undefined
|
||||
: eq(medias.creatorId, ctx.session.user.id),
|
||||
),
|
||||
path: "/manage/medias",
|
||||
});
|
||||
}
|
||||
|
||||
return statistics;
|
||||
}),
|
||||
});
|
||||
|
||||
const getCountForTableAsync = async (db: Database, table: AnySQLiteTable, canView: boolean) => {
|
||||
if (!canView) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return await db.$count(table);
|
||||
};
|
||||
@@ -4,8 +4,9 @@ import { z } from "zod/v4";
|
||||
import { createId, objectEntries } from "@homarr/common";
|
||||
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, asc, eq, handleTransactionsAsync, inArray, like } from "@homarr/db";
|
||||
import { and, asc, eq, handleTransactionsAsync, inArray, like, or } from "@homarr/db";
|
||||
import {
|
||||
apps,
|
||||
groupMembers,
|
||||
groupPermissions,
|
||||
integrationGroupPermissions,
|
||||
@@ -212,6 +213,14 @@ export const integrationRouter = createTRPCRouter({
|
||||
updatedAt: true,
|
||||
},
|
||||
},
|
||||
app: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
iconUrl: true,
|
||||
href: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -233,6 +242,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
value: integrationSecretKindObject[secret.kind].isPublic ? decryptSecret(secret.value) : null,
|
||||
updatedAt: secret.updatedAt,
|
||||
})),
|
||||
app: integration.app,
|
||||
};
|
||||
}),
|
||||
create: permissionRequiredProcedure
|
||||
@@ -245,6 +255,13 @@ export const integrationRouter = createTRPCRouter({
|
||||
url: input.url,
|
||||
});
|
||||
|
||||
if (input.app && "name" in input.app && !ctx.session.user.permissions.includes("app-create")) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Permission denied",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await testConnectionAsync({
|
||||
id: "new",
|
||||
name: input.name,
|
||||
@@ -267,12 +284,15 @@ export const integrationRouter = createTRPCRouter({
|
||||
};
|
||||
}
|
||||
|
||||
const appId = await createAppIfNecessaryAsync(ctx.db, input.app);
|
||||
|
||||
const integrationId = createId();
|
||||
await ctx.db.insert(integrations).values({
|
||||
id: integrationId,
|
||||
name: input.name,
|
||||
url: input.url,
|
||||
kind: input.kind,
|
||||
appId,
|
||||
});
|
||||
|
||||
if (input.secrets.length >= 1) {
|
||||
@@ -358,6 +378,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
.set({
|
||||
name: input.name,
|
||||
url: input.url,
|
||||
appId: input.appId,
|
||||
})
|
||||
.where(eq(integrations.id, input.id));
|
||||
|
||||
@@ -386,6 +407,21 @@ export const integrationRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
const removedSecrets = integration.secrets.filter(
|
||||
(dbSecret) => !input.secrets.some((secret) => dbSecret.kind === secret.kind),
|
||||
);
|
||||
if (removedSecrets.length >= 1) {
|
||||
await ctx.db
|
||||
.delete(integrationSecrets)
|
||||
.where(
|
||||
or(
|
||||
...removedSecrets.map((secret) =>
|
||||
and(eq(integrationSecrets.integrationId, input.id), eq(integrationSecrets.kind, secret.kind)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
logger.info("Updated integration", {
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
@@ -637,3 +673,30 @@ const addSecretAsync = async (db: Database, input: AddSecretInput) => {
|
||||
integrationId: input.integrationId,
|
||||
});
|
||||
};
|
||||
|
||||
const createAppIfNecessaryAsync = async (db: Database, app: z.infer<typeof integrationCreateSchema>["app"]) => {
|
||||
if (!app) return null;
|
||||
if ("id" in app) return app.id;
|
||||
|
||||
logger.info("Creating app", {
|
||||
name: app.name,
|
||||
url: app.href,
|
||||
});
|
||||
const appId = createId();
|
||||
await db.insert(apps).values({
|
||||
id: appId,
|
||||
name: app.name,
|
||||
description: app.description,
|
||||
iconUrl: app.iconUrl,
|
||||
href: app.href,
|
||||
pingUrl: app.pingUrl,
|
||||
});
|
||||
|
||||
logger.info("Created app", {
|
||||
id: appId,
|
||||
name: app.name,
|
||||
url: app.href,
|
||||
});
|
||||
|
||||
return appId;
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { getAllSecretKindOptions } from "@homarr/definitions";
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
type FormIntegration = Integration & {
|
||||
type FormIntegration = Omit<Integration, "appId"> & {
|
||||
secrets: {
|
||||
kind: IntegrationSecretKind;
|
||||
value: string | null;
|
||||
@@ -75,6 +75,7 @@ export const testConnectionAsync = async (
|
||||
const integrationInstance = await createIntegrationAsync({
|
||||
...baseIntegration,
|
||||
decryptedSecrets,
|
||||
externalUrl: null,
|
||||
});
|
||||
|
||||
const result = await integrationInstance.testConnectionAsync();
|
||||
|
||||
@@ -55,34 +55,47 @@ export const mediaRouter = createTRPCRouter({
|
||||
.requiresPermission("media-upload")
|
||||
.input(mediaUploadSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const content = Buffer.from(await input.file.arrayBuffer());
|
||||
const id = createId();
|
||||
const media = {
|
||||
id,
|
||||
creatorId: ctx.session.user.id,
|
||||
content,
|
||||
size: input.file.size,
|
||||
contentType: input.file.type,
|
||||
name: input.file.name,
|
||||
} satisfies InferInsertModel<typeof medias>;
|
||||
await ctx.db.insert(medias).values(media);
|
||||
const files = await Promise.all(
|
||||
input.files.map(async (file) => ({
|
||||
id: createId(),
|
||||
meta: file,
|
||||
content: Buffer.from(await file.arrayBuffer()),
|
||||
})),
|
||||
);
|
||||
const insertMedias = files.map(
|
||||
(file): InferInsertModel<typeof medias> => ({
|
||||
id: file.id,
|
||||
creatorId: ctx.session.user.id,
|
||||
content: file.content,
|
||||
size: file.meta.size,
|
||||
contentType: file.meta.type,
|
||||
name: file.meta.name,
|
||||
}),
|
||||
);
|
||||
await ctx.db.insert(medias).values(insertMedias);
|
||||
|
||||
const localIconRepository = await ctx.db.query.iconRepositories.findFirst({
|
||||
where: eq(iconRepositories.slug, LOCAL_ICON_REPOSITORY_SLUG),
|
||||
});
|
||||
|
||||
if (!localIconRepository) return id;
|
||||
const ids = files.map((file) => file.id);
|
||||
if (!localIconRepository) return ids;
|
||||
|
||||
const icon = mapMediaToIcon(media);
|
||||
await ctx.db.insert(icons).values({
|
||||
id: createId(),
|
||||
checksum: icon.checksum,
|
||||
name: icon.fileNameWithExtension,
|
||||
url: icon.imageUrl,
|
||||
iconRepositoryId: localIconRepository.id,
|
||||
});
|
||||
await ctx.db.insert(icons).values(
|
||||
insertMedias.map((media) => {
|
||||
const icon = mapMediaToIcon(media);
|
||||
|
||||
return id;
|
||||
return {
|
||||
id: createId(),
|
||||
checksum: icon.checksum,
|
||||
name: icon.fileNameWithExtension,
|
||||
url: icon.imageUrl,
|
||||
iconRepositoryId: localIconRepository.id,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return ids;
|
||||
}),
|
||||
deleteMedia: protectedProcedure.input(byIdSchema).mutation(async ({ ctx, input }) => {
|
||||
const dbMedia = await ctx.db.query.medias.findFirst({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { createId } from "@homarr/common";
|
||||
import { encryptSecret } from "@homarr/common/server";
|
||||
import { integrations, integrationSecrets } from "@homarr/db/schema";
|
||||
import { apps, integrations, integrationSecrets } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
||||
|
||||
@@ -251,6 +251,102 @@ describe("create should create a new integration", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("with create integration access should create a new integration with new linked app", async () => {
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
deviceType: undefined,
|
||||
session: defaultSessionWithPermissions(["integration-create", "app-create"]),
|
||||
});
|
||||
const input = {
|
||||
name: "Jellyfin",
|
||||
kind: "jellyfin" as const,
|
||||
url: "http://jellyfin.local",
|
||||
secrets: [{ kind: "apiKey" as const, value: "1234567890" }],
|
||||
attemptSearchEngineCreation: false,
|
||||
app: {
|
||||
name: "Jellyfin",
|
||||
description: null,
|
||||
pingUrl: "http://jellyfin.local",
|
||||
href: "https://jellyfin.home",
|
||||
iconUrl: "logo.png",
|
||||
},
|
||||
};
|
||||
|
||||
const fakeNow = new Date("2023-07-01T00:00:00Z");
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(fakeNow);
|
||||
await caller.create(input);
|
||||
vi.useRealTimers();
|
||||
|
||||
const dbIntegration = await db.query.integrations.findFirst({
|
||||
with: {
|
||||
app: true,
|
||||
},
|
||||
});
|
||||
const dbSecret = await db.query.integrationSecrets.findFirst();
|
||||
expect(dbIntegration).toBeDefined();
|
||||
expect(dbIntegration!.name).toBe(input.name);
|
||||
expect(dbIntegration!.kind).toBe(input.kind);
|
||||
expect(dbIntegration!.url).toBe(input.url);
|
||||
expect(dbIntegration!.app!.name).toBe(input.app.name);
|
||||
expect(dbIntegration!.app!.pingUrl).toBe(input.app.pingUrl);
|
||||
expect(dbIntegration!.app!.href).toBe(input.app.href);
|
||||
expect(dbIntegration!.app!.iconUrl).toBe(input.app.iconUrl);
|
||||
|
||||
expect(dbSecret!.integrationId).toBe(dbIntegration!.id);
|
||||
expect(dbSecret).toBeDefined();
|
||||
expect(dbSecret!.kind).toBe(input.secrets[0]!.kind);
|
||||
expect(dbSecret!.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/);
|
||||
expect(dbSecret!.updatedAt).toEqual(fakeNow);
|
||||
});
|
||||
|
||||
test("with create integration access should create a new integration with existing linked app", async () => {
|
||||
const db = createDb();
|
||||
const appId = createId();
|
||||
await db.insert(apps).values({
|
||||
id: appId,
|
||||
name: "Existing Jellyfin",
|
||||
iconUrl: "logo.png",
|
||||
});
|
||||
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
deviceType: undefined,
|
||||
session: defaultSessionWithPermissions(["integration-create"]),
|
||||
});
|
||||
const input = {
|
||||
name: "Jellyfin",
|
||||
kind: "jellyfin" as const,
|
||||
url: "http://jellyfin.local",
|
||||
secrets: [{ kind: "apiKey" as const, value: "1234567890" }],
|
||||
attemptSearchEngineCreation: false,
|
||||
app: {
|
||||
id: appId,
|
||||
},
|
||||
};
|
||||
|
||||
const fakeNow = new Date("2023-07-01T00:00:00Z");
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(fakeNow);
|
||||
await caller.create(input);
|
||||
vi.useRealTimers();
|
||||
|
||||
const dbIntegration = await db.query.integrations.findFirst();
|
||||
const dbSecret = await db.query.integrationSecrets.findFirst();
|
||||
expect(dbIntegration).toBeDefined();
|
||||
expect(dbIntegration!.name).toBe(input.name);
|
||||
expect(dbIntegration!.kind).toBe(input.kind);
|
||||
expect(dbIntegration!.url).toBe(input.url);
|
||||
expect(dbIntegration!.appId).toBe(appId);
|
||||
|
||||
expect(dbSecret!.integrationId).toBe(dbIntegration!.id);
|
||||
expect(dbSecret).toBeDefined();
|
||||
expect(dbSecret!.kind).toBe(input.secrets[0]!.kind);
|
||||
expect(dbSecret!.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/);
|
||||
expect(dbSecret!.updatedAt).toEqual(fakeNow);
|
||||
});
|
||||
|
||||
test("without create integration access should throw permission error", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
@@ -273,6 +369,36 @@ describe("create should create a new integration", () => {
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrow("Permission denied");
|
||||
});
|
||||
|
||||
test("without create app access should throw permission error with new linked app", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const caller = integrationRouter.createCaller({
|
||||
db,
|
||||
deviceType: undefined,
|
||||
session: defaultSessionWithPermissions(["integration-create"]),
|
||||
});
|
||||
const input = {
|
||||
name: "Jellyfin",
|
||||
kind: "jellyfin" as const,
|
||||
url: "http://jellyfin.local",
|
||||
secrets: [{ kind: "apiKey" as const, value: "1234567890" }],
|
||||
attemptSearchEngineCreation: false,
|
||||
app: {
|
||||
name: "Jellyfin",
|
||||
description: null,
|
||||
href: "https://jellyfin.home",
|
||||
iconUrl: "logo.png",
|
||||
pingUrl: null,
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
const actAsync = async () => await caller.create(input);
|
||||
|
||||
// Assert
|
||||
await expect(actAsync()).rejects.toThrow("Permission denied");
|
||||
});
|
||||
});
|
||||
|
||||
describe("update should update an integration", () => {
|
||||
@@ -285,6 +411,7 @@ describe("update should update an integration", () => {
|
||||
});
|
||||
|
||||
const lastWeek = new Date("2023-06-24T00:00:00Z");
|
||||
const appId = createId();
|
||||
const integrationId = createId();
|
||||
const toInsert = {
|
||||
id: integrationId,
|
||||
@@ -293,6 +420,11 @@ describe("update should update an integration", () => {
|
||||
url: "http://hole.local",
|
||||
};
|
||||
|
||||
await db.insert(apps).values({
|
||||
id: appId,
|
||||
name: "Previous",
|
||||
iconUrl: "logo.png",
|
||||
});
|
||||
await db.insert(integrations).values(toInsert);
|
||||
|
||||
const usernameToInsert = {
|
||||
@@ -320,6 +452,7 @@ describe("update should update an integration", () => {
|
||||
{ kind: "password" as const, value: null },
|
||||
{ kind: "apiKey" as const, value: "1234567890" },
|
||||
],
|
||||
appId,
|
||||
};
|
||||
|
||||
const fakeNow = new Date("2023-07-01T00:00:00Z");
|
||||
@@ -335,6 +468,7 @@ describe("update should update an integration", () => {
|
||||
expect(dbIntegration!.name).toBe(input.name);
|
||||
expect(dbIntegration!.kind).toBe(input.kind);
|
||||
expect(dbIntegration!.url).toBe(input.url);
|
||||
expect(dbIntegration!.appId).toBe(appId);
|
||||
|
||||
expect(dbSecrets.length).toBe(3);
|
||||
const username = expectToBeDefined(dbSecrets.find((secret) => secret.kind === "username"));
|
||||
@@ -365,6 +499,7 @@ describe("update should update an integration", () => {
|
||||
name: "Pi Hole",
|
||||
url: "http://hole.local",
|
||||
secrets: [],
|
||||
appId: null,
|
||||
});
|
||||
await expect(actAsync()).rejects.toThrow("Integration not found");
|
||||
});
|
||||
@@ -385,6 +520,7 @@ describe("update should update an integration", () => {
|
||||
name: "Pi Hole",
|
||||
url: "http://hole.local",
|
||||
secrets: [],
|
||||
appId: null,
|
||||
});
|
||||
|
||||
// Assert
|
||||
|
||||
@@ -55,6 +55,7 @@ describe("testConnectionAsync should run test connection of integration", () =>
|
||||
value: "secret",
|
||||
}),
|
||||
],
|
||||
externalUrl: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,6 +105,7 @@ describe("testConnectionAsync should run test connection of integration", () =>
|
||||
value: "dbSecret",
|
||||
}),
|
||||
],
|
||||
externalUrl: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,6 +155,7 @@ describe("testConnectionAsync should run test connection of integration", () =>
|
||||
value: "secret",
|
||||
}),
|
||||
],
|
||||
externalUrl: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -206,6 +209,7 @@ describe("testConnectionAsync should run test connection of integration", () =>
|
||||
value: "secret",
|
||||
}),
|
||||
],
|
||||
externalUrl: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -263,6 +267,7 @@ describe("testConnectionAsync should run test connection of integration", () =>
|
||||
value: "dbPassword",
|
||||
}),
|
||||
],
|
||||
externalUrl: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -336,6 +341,7 @@ describe("testConnectionAsync should run test connection of integration", () =>
|
||||
value: "privateKey",
|
||||
}),
|
||||
],
|
||||
externalUrl: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -114,11 +114,10 @@ export const userRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
// Max image size of 256KB, only png and jpeg are allowed
|
||||
image: z
|
||||
.string()
|
||||
.regex(/^data:image\/(png|jpeg|gif|webp);base64,[A-Za-z0-9/+]+=*$/g)
|
||||
.max(262144)
|
||||
.max(350000) // approximately 256KB in base64 (256 * 1024 * 4 / 3 + prefixes)
|
||||
.nullable(),
|
||||
}),
|
||||
)
|
||||
@@ -147,13 +146,6 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
if (user.provider !== "credentials") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Profile image can not be changed for users with external providers",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db
|
||||
.update(users)
|
||||
.set({
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import type { Modify } from "@homarr/common/types";
|
||||
import type { Integration } from "@homarr/db/schema";
|
||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import type { CalendarEvent } from "@homarr/integrations/types";
|
||||
import { radarrReleaseTypes } from "@homarr/integrations/types";
|
||||
import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar";
|
||||
|
||||
@@ -19,14 +24,56 @@ export const calendarRouter = createTRPCRouter({
|
||||
)
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar")))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const results = await Promise.all(
|
||||
return await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
const innerHandler = calendarMonthRequestHandler.handler(integration, input);
|
||||
const { integrationIds: _integrationIds, ...handlerInput } = input;
|
||||
const innerHandler = calendarMonthRequestHandler.handler(integration, handlerInput);
|
||||
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
|
||||
return data;
|
||||
return {
|
||||
events: data,
|
||||
integration: {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
return results.flat();
|
||||
}),
|
||||
subscribeToEvents: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
year: z.number(),
|
||||
month: z.number(),
|
||||
releaseType: z.array(z.enum(radarrReleaseTypes)),
|
||||
showUnmonitored: z.boolean(),
|
||||
}),
|
||||
)
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar")))
|
||||
.subscription(({ ctx, input }) => {
|
||||
return observable<{
|
||||
integration: Modify<Integration, { kind: IntegrationKindByCategory<"calendar"> }>;
|
||||
events: CalendarEvent[];
|
||||
}>((emit) => {
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
for (const integrationWithSecrets of ctx.integrations) {
|
||||
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
|
||||
const { integrationIds: _integrationIds, ...handlerInput } = input;
|
||||
const innerHandler = calendarMonthRequestHandler.handler(integrationWithSecrets, handlerInput);
|
||||
const unsubscribe = innerHandler.subscribe((events) => {
|
||||
emit.next({
|
||||
integration,
|
||||
events,
|
||||
});
|
||||
});
|
||||
unsubscribes.push(unsubscribe);
|
||||
}
|
||||
return () => {
|
||||
unsubscribes.forEach((unsubscribe) => {
|
||||
unsubscribe();
|
||||
});
|
||||
};
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -1,4 +1,7 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import type { MediaTranscoding } from "@homarr/request-handler/media-transcoding";
|
||||
import { mediaTranscodingRequestHandler } from "@homarr/request-handler/media-transcoding";
|
||||
import { paginatedSchema } from "@homarr/validation/common";
|
||||
|
||||
@@ -15,7 +18,7 @@ export const mediaTranscodingRouter = createTRPCRouter({
|
||||
.input(paginatedSchema.pick({ page: true, pageSize: true }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const innerHandler = mediaTranscodingRequestHandler.handler(ctx.integration, {
|
||||
pageOffset: input.page,
|
||||
pageOffset: (input.page - 1) * input.pageSize,
|
||||
pageSize: input.pageSize,
|
||||
});
|
||||
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
@@ -25,4 +28,19 @@ export const mediaTranscodingRouter = createTRPCRouter({
|
||||
data,
|
||||
};
|
||||
}),
|
||||
subscribeData: publicProcedure
|
||||
.concat(createIndexerManagerIntegrationMiddleware("query"))
|
||||
.input(paginatedSchema.pick({ page: true, pageSize: true }))
|
||||
.subscription(({ ctx, input }) => {
|
||||
return observable<{ integrationId: string; data: MediaTranscoding }>((emit) => {
|
||||
const innerHandler = mediaTranscodingRequestHandler.handler(ctx.integration, {
|
||||
pageOffset: (input.page - 1) * input.pageSize,
|
||||
pageSize: input.pageSize,
|
||||
});
|
||||
const unsubscribe = innerHandler.subscribe((data) => {
|
||||
emit.next({ integrationId: input.integrationId, data });
|
||||
});
|
||||
return unsubscribe;
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -40,15 +40,22 @@ export const releasesRouter = createTRPCRouter({
|
||||
.query(async ({ ctx, input }) => {
|
||||
return await Promise.all(
|
||||
input.repositories.map(async (repository) => {
|
||||
const innerHandler = releasesRequestHandler.handler(ctx.integration, {
|
||||
id: repository.id,
|
||||
identifier: repository.identifier,
|
||||
versionRegex: formatVersionFilterRegex(repository.versionFilter),
|
||||
});
|
||||
const response = await releasesRequestHandler
|
||||
.handler(ctx.integration, {
|
||||
id: repository.id,
|
||||
identifier: repository.identifier,
|
||||
versionRegex: formatVersionFilterRegex(repository.versionFilter),
|
||||
})
|
||||
.getCachedOrUpdatedDataAsync({
|
||||
forceUpdate: false,
|
||||
});
|
||||
|
||||
return await innerHandler.getCachedOrUpdatedDataAsync({
|
||||
forceUpdate: false,
|
||||
});
|
||||
return {
|
||||
id: repository.id,
|
||||
integration: { name: ctx.integration.name, kind: ctx.integration.kind },
|
||||
timestamp: response.timestamp,
|
||||
...response.data,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { fetchWithTimeout } from "@homarr/common";
|
||||
import type { Weather } from "@homarr/request-handler/weather";
|
||||
import { weatherRequestHandler } from "@homarr/request-handler/weather";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
@@ -9,45 +11,19 @@ const atLocationInput = z.object({
|
||||
latitude: z.number(),
|
||||
});
|
||||
|
||||
const atLocationOutput = z.object({
|
||||
current_weather: z.object({
|
||||
weathercode: z.number(),
|
||||
temperature: z.number(),
|
||||
windspeed: z.number(),
|
||||
}),
|
||||
daily: z.object({
|
||||
time: z.array(z.string()),
|
||||
weathercode: z.array(z.number()),
|
||||
temperature_2m_max: z.array(z.number()),
|
||||
temperature_2m_min: z.array(z.number()),
|
||||
sunrise: z.array(z.string()),
|
||||
sunset: z.array(z.string()),
|
||||
wind_speed_10m_max: z.array(z.number()),
|
||||
wind_gusts_10m_max: z.array(z.number()),
|
||||
}),
|
||||
});
|
||||
|
||||
export const weatherRouter = createTRPCRouter({
|
||||
atLocation: publicProcedure.input(atLocationInput).query(async ({ input }) => {
|
||||
const res = await fetchWithTimeout(
|
||||
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max¤t_weather=true&timezone=auto`,
|
||||
);
|
||||
const json: unknown = await res.json();
|
||||
const weather = await atLocationOutput.parseAsync(json);
|
||||
return {
|
||||
current: weather.current_weather,
|
||||
daily: weather.daily.time.map((value, index) => {
|
||||
return {
|
||||
time: value,
|
||||
weatherCode: weather.daily.weathercode[index] ?? 404,
|
||||
maxTemp: weather.daily.temperature_2m_max[index],
|
||||
minTemp: weather.daily.temperature_2m_min[index],
|
||||
sunrise: weather.daily.sunrise[index],
|
||||
sunset: weather.daily.sunset[index],
|
||||
maxWindSpeed: weather.daily.wind_speed_10m_max[index],
|
||||
maxWindGusts: weather.daily.wind_gusts_10m_max[index],
|
||||
};
|
||||
}),
|
||||
};
|
||||
const handler = weatherRequestHandler.handler(input);
|
||||
return await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false }).then((result) => result.data);
|
||||
}),
|
||||
subscribeAtLocation: publicProcedure.input(atLocationInput).subscription(({ input }) => {
|
||||
return observable<Weather>((emit) => {
|
||||
const handler = weatherRequestHandler.handler(input);
|
||||
const unsubscribe = handler.subscribe((data) => {
|
||||
emit.next(data);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -20,6 +20,7 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
|
||||
where: eq(users.id, user.id),
|
||||
columns: {
|
||||
name: true,
|
||||
image: true,
|
||||
colorScheme: true,
|
||||
},
|
||||
});
|
||||
@@ -59,6 +60,15 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
|
||||
`Username for user of oidc provider has changed. user=${user.id} old='${dbUser.name}' new='${profileUsername}'`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof profile.picture === "string" &&
|
||||
dbUser.image !== profile.picture &&
|
||||
!dbUser.image?.startsWith("data:")
|
||||
) {
|
||||
await db.update(users).set({ image: profile.picture }).where(eq(users.id, user.id));
|
||||
logger.info(`Profile picture for user of oidc provider has changed. user=${user.id}'`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`User '${dbUser.name}' logged in at ${dayjs().format()}`);
|
||||
|
||||
+10
-10
@@ -23,8 +23,8 @@
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.40.0",
|
||||
"@auth/drizzle-adapter": "^1.10.0",
|
||||
"@auth/core": "^0.41.1",
|
||||
"@auth/drizzle-adapter": "^1.11.1",
|
||||
"@homarr/certificates": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/core": "workspace:^0.1.0",
|
||||
@@ -35,20 +35,20 @@
|
||||
"bcrypt": "^6.0.0",
|
||||
"cookies": "^0.9.1",
|
||||
"ldapts": "8.0.9",
|
||||
"next": "15.5.3",
|
||||
"next-auth": "5.0.0-beta.29",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"zod": "^4.1.8"
|
||||
"next": "16.0.1",
|
||||
"next-auth": "5.0.0-beta.30",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/bcrypt": "6.0.0",
|
||||
"@types/cookies": "0.9.1",
|
||||
"eslint": "^9.35.0",
|
||||
"@types/cookies": "0.9.2",
|
||||
"eslint": "^9.39.1",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ export const OidcProvider = (headers: ReadonlyHeaders | null): OIDCConfig<Profil
|
||||
id: profile.sub,
|
||||
name,
|
||||
email: profile.email,
|
||||
image: typeof profile.picture === "string" ? profile.picture : null,
|
||||
provider: "oidc",
|
||||
};
|
||||
},
|
||||
|
||||
@@ -23,12 +23,11 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
test("should fail when wrong ldap base credentials", async () => {
|
||||
// Arrange
|
||||
const spy = vi.spyOn(ldapClient, "LdapClient");
|
||||
spy.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bindAsync: vi.fn(() => Promise.reject(new Error("bindAsync"))),
|
||||
}) as unknown as ldapClient.LdapClient,
|
||||
);
|
||||
spy.mockImplementation(function () {
|
||||
return {
|
||||
bindAsync: vi.fn(() => Promise.reject(new Error("bindAsync"))),
|
||||
} as unknown as ldapClient.LdapClient;
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
@@ -44,13 +43,12 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
test("should fail when user not found", async () => {
|
||||
// Arrange
|
||||
const spy = vi.spyOn(ldapClient, "LdapClient");
|
||||
spy.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn(() => Promise.resolve([])),
|
||||
}) as unknown as ldapClient.LdapClient,
|
||||
);
|
||||
spy.mockImplementation(function () {
|
||||
return {
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn(() => Promise.resolve([])),
|
||||
} as unknown as ldapClient.LdapClient;
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
@@ -66,20 +64,19 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
test("should fail when user has invalid email", async () => {
|
||||
// Arrange
|
||||
const spy = vi.spyOn(ldapClient, "LdapClient");
|
||||
spy.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
dn: "test",
|
||||
mail: "test",
|
||||
},
|
||||
]),
|
||||
),
|
||||
}) as unknown as ldapClient.LdapClient,
|
||||
);
|
||||
spy.mockImplementation(function () {
|
||||
return {
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
dn: "test",
|
||||
mail: "test",
|
||||
},
|
||||
]),
|
||||
),
|
||||
} as unknown as ldapClient.LdapClient;
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
@@ -103,15 +100,14 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
]),
|
||||
);
|
||||
const spy = vi.spyOn(ldapClient, "LdapClient");
|
||||
spy.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bindAsync: vi.fn((props: ldapClient.BindOptions) =>
|
||||
props.distinguishedName === "test" ? Promise.reject(new Error("bindAsync")) : Promise.resolve(),
|
||||
),
|
||||
searchAsync: searchSpy,
|
||||
}) as unknown as ldapClient.LdapClient,
|
||||
);
|
||||
spy.mockImplementation(function () {
|
||||
return {
|
||||
bindAsync: vi.fn((props: ldapClient.BindOptions) =>
|
||||
props.distinguishedName === "test" ? Promise.reject(new Error("bindAsync")) : Promise.resolve(),
|
||||
),
|
||||
searchAsync: searchSpy,
|
||||
} as unknown as ldapClient.LdapClient;
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
@@ -129,21 +125,20 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(ldapClient, "LdapClient");
|
||||
spy.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
dn: "test",
|
||||
mail: "test@gmail.com",
|
||||
},
|
||||
]),
|
||||
),
|
||||
disconnectAsync: vi.fn(),
|
||||
}) as unknown as ldapClient.LdapClient,
|
||||
);
|
||||
spy.mockImplementation(function () {
|
||||
return {
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
dn: "test",
|
||||
mail: "test@gmail.com",
|
||||
},
|
||||
]),
|
||||
),
|
||||
disconnectAsync: vi.fn(),
|
||||
} as unknown as ldapClient.LdapClient;
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await authorizeWithLdapCredentialsAsync(db, {
|
||||
@@ -168,21 +163,20 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(ldapClient, "LdapClient");
|
||||
spy.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
dn: "test",
|
||||
mail: "test@gmail.com",
|
||||
},
|
||||
]),
|
||||
),
|
||||
disconnectAsync: vi.fn(),
|
||||
}) as unknown as ldapClient.LdapClient,
|
||||
);
|
||||
spy.mockImplementation(function () {
|
||||
return {
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
dn: "test",
|
||||
mail: "test@gmail.com",
|
||||
},
|
||||
]),
|
||||
),
|
||||
disconnectAsync: vi.fn(),
|
||||
} as unknown as ldapClient.LdapClient;
|
||||
});
|
||||
await db.insert(users).values({
|
||||
id: createId(),
|
||||
name: "test",
|
||||
@@ -220,21 +214,20 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
test("should authorize user with correct credentials and return updated name", async () => {
|
||||
// Arrange
|
||||
const spy = vi.spyOn(ldapClient, "LdapClient");
|
||||
spy.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
dn: "test55",
|
||||
mail: "test@gmail.com",
|
||||
},
|
||||
]),
|
||||
),
|
||||
disconnectAsync: vi.fn(),
|
||||
}) as unknown as ldapClient.LdapClient,
|
||||
);
|
||||
spy.mockImplementation(function () {
|
||||
return {
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
dn: "test55",
|
||||
mail: "test@gmail.com",
|
||||
},
|
||||
]),
|
||||
),
|
||||
disconnectAsync: vi.fn(),
|
||||
} as unknown as ldapClient.LdapClient;
|
||||
});
|
||||
|
||||
const userId = createId();
|
||||
const db = createDb();
|
||||
@@ -268,27 +261,26 @@ describe("authorizeWithLdapCredentials", () => {
|
||||
test("should authorize user with correct credentials and return his groups", async () => {
|
||||
// Arrange
|
||||
const spy = vi.spyOn(ldapClient, "LdapClient");
|
||||
spy.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn((argument: { options: { filter: string } }) =>
|
||||
argument.options.filter.includes("group")
|
||||
? Promise.resolve([
|
||||
{
|
||||
cn: "homarr_example",
|
||||
},
|
||||
])
|
||||
: Promise.resolve([
|
||||
{
|
||||
dn: "test55",
|
||||
mail: "test@gmail.com",
|
||||
},
|
||||
]),
|
||||
),
|
||||
disconnectAsync: vi.fn(),
|
||||
}) as unknown as ldapClient.LdapClient,
|
||||
);
|
||||
spy.mockImplementation(function () {
|
||||
return {
|
||||
bindAsync: vi.fn(() => Promise.resolve()),
|
||||
searchAsync: vi.fn((argument: { options: { filter: string } }) =>
|
||||
argument.options.filter.includes("group")
|
||||
? Promise.resolve([
|
||||
{
|
||||
cn: "homarr_example",
|
||||
},
|
||||
])
|
||||
: Promise.resolve([
|
||||
{
|
||||
dn: "test55",
|
||||
mail: "test@gmail.com",
|
||||
},
|
||||
]),
|
||||
),
|
||||
disconnectAsync: vi.fn(),
|
||||
} as unknown as ldapClient.LdapClient;
|
||||
});
|
||||
const db = createDb();
|
||||
const userId = createId();
|
||||
await db.insert(users).values({
|
||||
|
||||
@@ -25,14 +25,14 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/api": "workspace:^0.1.0",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1"
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -28,14 +28,14 @@
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"dotenv": "^17.2.2"
|
||||
"dotenv": "^17.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"esbuild": "^0.25.9",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
"esbuild": "^0.26.0",
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -29,22 +29,22 @@
|
||||
"dependencies": {
|
||||
"@homarr/core": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"dayjs": "^1.11.18",
|
||||
"dns-caching": "^0.2.5",
|
||||
"next": "15.5.3",
|
||||
"octokit": "^5.0.3",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"@paralleldrive/cuid2": "^3.1.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"dns-caching": "^0.2.7",
|
||||
"next": "16.0.1",
|
||||
"octokit": "^5.0.5",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"undici": "7.16.0",
|
||||
"zod": "^4.1.8",
|
||||
"zod-validation-error": "^4.0.1"
|
||||
"zod": "^4.1.12",
|
||||
"zod-validation-error": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,17 @@ const calculateTimeAgo = (timestamp: Date) => {
|
||||
};
|
||||
|
||||
export const useTimeAgo = (timestamp: Date, updateFrequency = 1000) => {
|
||||
const [timeAgo, setTimeAgo] = useState(calculateTimeAgo(timestamp));
|
||||
const [timeAgo, setTimeAgo] = useState(() => calculateTimeAgo(timestamp));
|
||||
|
||||
useEffect(() => {
|
||||
setTimeAgo(calculateTimeAgo(timestamp));
|
||||
}, [timestamp]);
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => setTimeAgo(calculateTimeAgo(timestamp)), updateFrequency);
|
||||
|
||||
return () => clearInterval(intervalId); // clear interval on hook unmount
|
||||
}, [timestamp]);
|
||||
}, [timestamp, updateFrequency]);
|
||||
|
||||
return timeAgo;
|
||||
};
|
||||
|
||||
@@ -47,3 +47,8 @@ export const humanFileSize = (size: number, concat = ""): string => {
|
||||
}
|
||||
return "∞";
|
||||
};
|
||||
|
||||
const IMPERIAL_MULTIPLIER = 1.609344;
|
||||
|
||||
export const metricToImperial = (metricValue: number) => metricValue / IMPERIAL_MULTIPLIER;
|
||||
export const imperialToMetric = (imperialValue: number) => imperialValue * IMPERIAL_MULTIPLIER;
|
||||
@@ -25,14 +25,14 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@t3-oss/env-nextjs": "^0.13.8",
|
||||
"ioredis": "5.7.0",
|
||||
"zod": "^4.1.8"
|
||||
"ioredis": "5.8.2",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -29,21 +29,21 @@
|
||||
"@homarr/core": "workspace:^0.1.0",
|
||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@tanstack/react-query": "^5.87.4",
|
||||
"@trpc/client": "^11.5.1",
|
||||
"@trpc/server": "^11.5.1",
|
||||
"@trpc/tanstack-react-query": "^11.5.1",
|
||||
"@tanstack/react-query": "^5.90.7",
|
||||
"@trpc/client": "^11.7.1",
|
||||
"@trpc/server": "^11.7.1",
|
||||
"@trpc/tanstack-react-query": "^11.7.1",
|
||||
"node-cron": "^4.2.1",
|
||||
"react": "19.1.1",
|
||||
"zod": "^4.1.8"
|
||||
"react": "19.2.0",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/react": "19.1.13",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
"@types/react": "19.2.2",
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
|
||||
import { pingJob } from "./jobs/ping";
|
||||
import { rssFeedsJob } from "./jobs/rss-feeds";
|
||||
import { updateCheckerJob } from "./jobs/update-checker";
|
||||
import { weatherJob } from "./jobs/weather";
|
||||
import { createCronJobGroup } from "./lib";
|
||||
|
||||
export const jobGroup = createCronJobGroup({
|
||||
@@ -48,6 +49,7 @@ export const jobGroup = createCronJobGroup({
|
||||
firewallVersion: firewallVersionJob,
|
||||
firewallInterfaces: firewallInterfacesJob,
|
||||
refreshNotifications: refreshNotificationsJob,
|
||||
weather: weatherJob,
|
||||
});
|
||||
|
||||
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
|
||||
@@ -0,0 +1,33 @@
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions";
|
||||
import { db, eq } from "@homarr/db";
|
||||
import { items } from "@homarr/db/schema";
|
||||
import { logger } from "@homarr/log";
|
||||
import { weatherRequestHandler } from "@homarr/request-handler/weather";
|
||||
|
||||
import type { WidgetComponentProps } from "../../../widgets";
|
||||
import { createCronJob } from "../lib";
|
||||
|
||||
export const weatherJob = createCronJob("weather", EVERY_10_MINUTES).withCallback(async () => {
|
||||
const weatherItems = await db.query.items.findMany({
|
||||
where: eq(items.kind, "weather"),
|
||||
});
|
||||
|
||||
const parsedItems = weatherItems.map((item) => ({
|
||||
id: item.id,
|
||||
options: SuperJSON.parse<WidgetComponentProps<"weather">["options"]>(item.options),
|
||||
}));
|
||||
|
||||
for (const item of parsedItems) {
|
||||
try {
|
||||
const innerHandler = weatherRequestHandler.handler({
|
||||
longitude: item.options.location.longitude,
|
||||
latitude: item.options.location.latitude,
|
||||
});
|
||||
await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
|
||||
} catch (error) {
|
||||
logger.error("Failed to update weather", { id: item.id, error });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `integration` ADD `app_id` varchar(128);--> statement-breakpoint
|
||||
ALTER TABLE `integration` ADD CONSTRAINT `integration_app_id_app_id_fk` FOREIGN KEY (`app_id`) REFERENCES `app`(`id`) ON DELETE set null ON UPDATE no action;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -253,6 +253,13 @@
|
||||
"when": 1756701556908,
|
||||
"tag": "0035_increase-secret-kind-length",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 36,
|
||||
"version": "5",
|
||||
"when": 1760968518445,
|
||||
"tag": "0036_add_app_reference_to_integration",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "integration" ADD COLUMN "app_id" varchar(128);--> statement-breakpoint
|
||||
ALTER TABLE "integration" ADD CONSTRAINT "integration_app_id_app_id_fk" FOREIGN KEY ("app_id") REFERENCES "public"."app"("id") ON DELETE set null ON UPDATE no action;
|
||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 251 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user