Compare commits
389
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4607a3ec33 | ||
|
|
ad88c908f0 | ||
|
|
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 | ||
|
|
c9983ad5a9 | ||
|
|
5045c64fe2 | ||
|
|
69d6ffd9ce | ||
|
|
3c2e6a852f | ||
|
|
312e084e2c | ||
|
|
2a67d2f9da | ||
|
|
a81634d239 | ||
|
|
288e37553c | ||
|
|
03545cee52 | ||
|
|
88c718eb41 | ||
|
|
ccf7e540dc | ||
|
|
9f2b868fce | ||
|
|
fedbff3fd1 | ||
|
|
0a1a75dc5f | ||
|
|
2d3af7afca | ||
|
|
6e31e25671 | ||
|
|
12b2b82b4e | ||
|
|
94fb9b2502 | ||
|
|
687cfd4cc7 | ||
|
|
5babf0b240 | ||
|
|
4287d1743b | ||
|
|
b749dc110d | ||
|
|
0e563fc47f | ||
|
|
1d485cf26c | ||
|
|
69b46959aa | ||
|
|
fe676daad2 | ||
|
|
43d5a27552 | ||
|
|
809fbd5176 | ||
|
|
5c65e1a75f | ||
|
|
86cf290c31 | ||
|
|
b398be0303 | ||
|
|
1412b6c0d3 | ||
|
|
6b3672a04d | ||
|
|
b30cc21dbe | ||
|
|
8c24da8732 | ||
|
|
cb7a6abca7 | ||
|
|
cd4c236e5e | ||
|
|
6880d61d6b | ||
|
|
764d5eb0ea | ||
|
|
b7d7767343 | ||
|
|
f7d6f3d9e1 | ||
|
|
3b7e3da17c | ||
|
|
f360c9768a | ||
|
|
8df7fdcab4 | ||
|
|
b8f0d9a21f | ||
|
|
73a50fa687 | ||
|
|
78626121dd | ||
|
|
c69602ff07 | ||
|
|
7c22e1d05d | ||
|
|
dc08bacebf | ||
|
|
26eb2c08c9 | ||
|
|
f7471b6c63 | ||
|
|
3703f428b0 | ||
|
|
1019657a63 | ||
|
|
789ac65cbd | ||
|
|
d7902f80c2 | ||
|
|
44d4cec3ff | ||
|
|
a843f6043b | ||
|
|
bfcbffbdc6 | ||
|
|
4d57c7ca13 | ||
|
|
a7976e9d70 | ||
|
|
a149d8c3c1 | ||
|
|
3e1285cacd | ||
|
|
5b24eb3074 | ||
|
|
6389fd8807 | ||
|
|
1d9eb9bf24 | ||
|
|
3c9f7ecbf8 | ||
|
|
cd90eaae9d | ||
|
|
fbb86c452d | ||
|
|
a1b5f5ceea | ||
|
|
eb5af11643 | ||
|
|
7ac2a81f94 | ||
|
|
ecf1bd98b7 | ||
|
|
8231efa281 | ||
|
|
1323bb276f | ||
|
|
2eef4a8304 | ||
|
|
842f4f6284 | ||
|
|
4fc38bcd5b | ||
|
|
99179e4e7d | ||
|
|
8fa245910c | ||
|
|
b50ec029fa | ||
|
|
8bf278f603 | ||
|
|
de33439b22 | ||
|
|
b65ec27c0d | ||
|
|
1f4a9037d7 | ||
|
|
e70074b219 | ||
|
|
bc94e34ada | ||
|
|
d9440087b7 | ||
|
|
32b2a09ab4 | ||
|
|
5d8e2a3c7b | ||
|
|
dd67dd794a | ||
|
|
10668ee359 | ||
|
|
d5eb812b72 | ||
|
|
2ac96a11b7 | ||
|
|
52f0b86e7b | ||
|
|
d7146d82d1 | ||
|
|
eef6b86ad6 | ||
|
|
9cb8261ddc | ||
|
|
d4f73d2b39 | ||
|
|
e4f14140a3 | ||
|
|
b77d8114ab | ||
|
|
0df1555b50 | ||
|
|
b2eb2c32f6 | ||
|
|
b34440470f | ||
|
|
047654351a | ||
|
|
4e4483c42d | ||
|
|
5e92e66803 | ||
|
|
83fec579e7 | ||
|
|
1a30ccc857 | ||
|
|
0ee023adf3 | ||
|
|
fb6b5e88ff | ||
|
|
ca488f8210 | ||
|
|
d317cacbe8 | ||
|
|
65b9ddd771 | ||
|
|
3e59c20dba | ||
|
|
d2e9b5e983 | ||
|
|
75683c206d | ||
|
|
a3dde2e7ee | ||
|
|
7b26a386eb | ||
|
|
032f7ffd29 | ||
|
|
2b1a3de8df | ||
|
|
4823c0db8b | ||
|
|
b0fe8a9f65 | ||
|
|
a1def95852 | ||
|
|
9c2d50de91 | ||
|
|
0e5f43c1c3 | ||
|
|
42071e36ca | ||
|
|
de7699689f | ||
|
|
be0e78371e | ||
|
|
ea4960e113 | ||
|
|
916542210d | ||
|
|
94eb6772b9 | ||
|
|
afe10b8715 | ||
|
|
99bfc4a0b6 | ||
|
|
d58faf5976 | ||
|
|
cf98156bdc | ||
|
|
f166d153fe | ||
|
|
dc9a83ba3c | ||
|
|
0e3c6f0f97 | ||
|
|
51b06922bd | ||
|
|
9f3e68fdfe | ||
|
|
88eed15faf | ||
|
|
f277ceb302 | ||
|
|
e0c9c0fab7 | ||
|
|
21bde0f3ea | ||
|
|
5d908093ce | ||
|
|
6081a5af13 | ||
|
|
a488efc6f8 | ||
|
|
24e4d718e2 | ||
|
|
71c75a3507 | ||
|
|
5bd6757e22 | ||
|
|
81a7f3e93d | ||
|
|
93186c3be1 | ||
|
|
e695bccfc0 | ||
|
|
a12a86b672 | ||
|
|
feee4b3ad7 | ||
|
|
36f2181643 | ||
|
|
bc746b67b4 | ||
|
|
0fb7fd4292 | ||
|
|
51f3cd2ce7 | ||
|
|
8dea07250f |
No files matched your search
@@ -33,6 +33,18 @@ body:
|
|||||||
options:
|
options:
|
||||||
# The below comment is used to insert a new version with on-release.yml
|
# The below comment is used to insert a new version with on-release.yml
|
||||||
#NEXT_VERSION#
|
#NEXT_VERSION#
|
||||||
|
- 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
|
||||||
|
- 1.35.1
|
||||||
|
- 1.35.0
|
||||||
- 1.34.0
|
- 1.34.0
|
||||||
- 1.33.0
|
- 1.33.0
|
||||||
- 1.32.0
|
- 1.32.0
|
||||||
|
|||||||
@@ -3,7 +3,12 @@
|
|||||||
extends: ["config:recommended"],
|
extends: ["config:recommended"],
|
||||||
packageRules: [
|
packageRules: [
|
||||||
{
|
{
|
||||||
matchPackagePatterns: ["^@homarr/"],
|
matchPackageNames: ["^@homarr/"],
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
// Reenable once https://github.com/privatenumber/tsx/issues/737 is fixed
|
||||||
|
{
|
||||||
|
matchPackageNames: ["tsx"],
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -16,4 +21,6 @@
|
|||||||
automerge: false,
|
automerge: false,
|
||||||
baseBranches: ["dev"],
|
baseBranches: ["dev"],
|
||||||
dependencyDashboard: false,
|
dependencyDashboard: false,
|
||||||
|
// prevent special characters in branch names (otherwise git errors occur on windows)
|
||||||
|
branchNameStrict: true,
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,8 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
approve-automatic-prs:
|
approve-automatic-prs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -12,10 +14,12 @@ jobs:
|
|||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: tibdex/github-app-token@v2
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private_key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
||||||
app_id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
||||||
|
permission-pull-requests: write # required to approve pull request
|
||||||
|
|
||||||
- name: Install GitHub CLI
|
- name: Install GitHub CLI
|
||||||
run: sudo apt-get install -y gh
|
run: sudo apt-get install -y gh
|
||||||
- name: Approve automatic PRs
|
- name: Approve automatic PRs
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * *" # every day at midnight
|
- cron: "0 0 * * *" # every day at midnight
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # required to push changes
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
download-crowdin-translations:
|
download-crowdin-translations:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -15,10 +18,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: tibdex/github-app-token@v2
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private_key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
|
||||||
app_id: ${{ secrets.CROWDIN_APP_ID }}
|
app-id: ${{ secrets.CROWDIN_APP_ID }}
|
||||||
|
permission-contents: write # required to commit to crowdin branch
|
||||||
|
permission-pull-requests: write # required to create pull request
|
||||||
|
|
||||||
- name: Download Crowdin translations
|
- name: Download Crowdin translations
|
||||||
id: crowdin-download
|
id: crowdin-download
|
||||||
|
|||||||
@@ -49,18 +49,23 @@ jobs:
|
|||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: tibdex/github-app-token@v2
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private_key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||||
app_id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||||
|
permission-contents: write # required to commit package.json & changelog changes, merge them to dev and publish the release
|
||||||
|
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v4
|
- uses: pnpm/action-setup@v4
|
||||||
|
if: env.SKIP_RELEASE == 'false'
|
||||||
|
- uses: actions/setup-node@v5
|
||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
with:
|
with:
|
||||||
node-version: 22.18.0
|
node-version: 24.10.0
|
||||||
|
cache: "pnpm"
|
||||||
- run: npm i -g pnpm
|
- run: npm i -g pnpm
|
||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -49,10 +49,11 @@ jobs:
|
|||||||
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }} (new behaviour: ${{ steps.semver.outputs.bump }})"
|
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }} (new behaviour: ${{ steps.semver.outputs.bump }})"
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainApprovalToken
|
id: obtainApprovalToken
|
||||||
uses: tibdex/github-app-token@v2
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private_key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
||||||
app_id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
||||||
|
permission-pull-requests: write
|
||||||
- name: Approve PR
|
- name: Approve PR
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
|
||||||
@@ -60,10 +61,12 @@ jobs:
|
|||||||
gh pr review --approve --body "Automatically approved by GitHub Action"
|
gh pr review --approve --body "Automatically approved by GitHub Action"
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainMergeToken
|
id: obtainMergeToken
|
||||||
uses: tibdex/github-app-token@v2
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private_key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||||
app_id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||||
|
permission-contents: write # write to main branch (due to merge)
|
||||||
|
permission-pull-requests: write # merge pull request
|
||||||
- id: automerge
|
- id: automerge
|
||||||
if: ${{ steps.semver.outputs.bump != 'major' }}
|
if: ${{ steps.semver.outputs.bump != 'major' }}
|
||||||
name: automerge
|
name: automerge
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -11,12 +11,14 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: tibdex/github-app-token@v2
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private_key: ${{ secrets.HOMARR_DOCS_RELEASE_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.HOMARR_DOCS_RELEASE_APP_PRIVATE_KEY }}
|
||||||
app_id: ${{ vars.HOMARR_DOCS_RELEASE_APP_ID }}
|
app-id: ${{ vars.HOMARR_DOCS_RELEASE_APP_ID }}
|
||||||
installation_retrieval_mode: repository
|
owner: homarr-labs
|
||||||
installation_retrieval_payload: homarr-labs/documentation
|
repositories: |
|
||||||
|
documentation
|
||||||
|
permission-contents: write # required to dispatch repository workflow
|
||||||
- name: Trigger documentation release
|
- name: Trigger documentation release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||||
@@ -40,10 +42,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: tibdex/github-app-token@v2
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private_key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||||
app_id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||||
|
permission-contents: write # required to commit to branch
|
||||||
|
permission-pull-requests: write # required to create pr & enable automerge
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
@@ -57,6 +61,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git config --global user.email "175486441+homarr-releases[bot]@users.noreply.github.com"
|
git config --global user.email "175486441+homarr-releases[bot]@users.noreply.github.com"
|
||||||
git config --global user.name "Releases Homarr"
|
git config --global user.name "Releases Homarr"
|
||||||
|
git checkout -b update-bug-report-template
|
||||||
git add .
|
git add .
|
||||||
git commit -m "chore: update bug report template"
|
git commit -m "chore: update bug report template"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
permissions:
|
||||||
|
statuses: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
skip-stability-days:
|
||||||
|
if: ${{ !startsWith(github.head_ref, 'renovate/') }}
|
||||||
|
name: Skip Stability Days
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Add status check
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||||
|
run: |
|
||||||
|
gh api \
|
||||||
|
--method POST \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
/repos/${{github.repository}}/statuses/${{github.event.pull_request.head.sha}} \
|
||||||
|
-f 'state=success' \
|
||||||
|
-f 'target_url=https://docs.renovatebot.com/configuration-options#minimumreleaseage' \
|
||||||
|
-f 'description=Skipped for non renovate branches' \
|
||||||
|
-f 'context=renovate/stability-days'
|
||||||
@@ -9,9 +9,6 @@ env:
|
|||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
CROWDIN_TOKEN: "${{ secrets.CROWDIN_UPDATE_CONTRIBUTORS_TOKEN }}"
|
CROWDIN_TOKEN: "${{ secrets.CROWDIN_UPDATE_CONTRIBUTORS_TOKEN }}"
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-contributors:
|
update-contributors:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -21,20 +18,24 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: tibdex/github-app-token@v2
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private_key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
||||||
app_id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
app-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
||||||
|
permission-contents: write # required to commit to branch
|
||||||
|
permission-pull-requests: write # required to create pr & enable automerge
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
- name: Run update script
|
- name: Run update script
|
||||||
run: node ./scripts/update-contributors.mjs
|
run: node ./scripts/update-contributors.mjs
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: Update integration list
|
name: Update integration list
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch: { }
|
workflow_dispatch: {}
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- packages/definitions/src/integration.ts
|
- packages/definitions/src/integration.ts
|
||||||
@@ -20,10 +20,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: tibdex/github-app-token@v2
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private_key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
||||||
app_id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
app-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
||||||
|
permission-contents: write # required to commit to branch
|
||||||
|
permission-pull-requests: write # required to create pr & enable automerge
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
env:
|
env:
|
||||||
|
|||||||
+253
@@ -1,3 +1,256 @@
|
|||||||
|
## [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
|
||||||
|
|
||||||
|
* **integrations:** add ICal ([#3980](https://github.com/homarr-labs/homarr/issues/3980)) ([fedbff3](https://github.com/homarr-labs/homarr/commit/fedbff3fd162835bd97c3de3a4d3ba676fe13899))
|
||||||
|
* **notebook-widget:** add confirmation on dismiss ([#4064](https://github.com/homarr-labs/homarr/issues/4064)) ([cb7a6ab](https://github.com/homarr-labs/homarr/commit/cb7a6abca7b6908d41bf50c71f7fce491af548e6))
|
||||||
|
* **redis:** add database index environment variable ([#4092](https://github.com/homarr-labs/homarr/issues/4092)) ([9f2b868](https://github.com/homarr-labs/homarr/commit/9f2b868fcedf440892b3f0f3ee38388daa291f2e))
|
||||||
|
* **system-resources-widget:** add has-shadow option ([#4093](https://github.com/homarr-labs/homarr/issues/4093)) ([3c2e6a8](https://github.com/homarr-labs/homarr/commit/3c2e6a852fb8464e98fdb16a58e35b87d5925a1e))
|
||||||
|
* **system-resources-widget:** add label display mode option ([#4086](https://github.com/homarr-labs/homarr/issues/4086)) ([312e084](https://github.com/homarr-labs/homarr/commit/312e084e2c731ce1bff9c9dc3e1d8f7a88df5429))
|
||||||
|
* **system-resources-widget:** add visible charts option ([#4069](https://github.com/homarr-labs/homarr/issues/4069)) ([b30cc21](https://github.com/homarr-labs/homarr/commit/b30cc21dbe51aef598740b9320675023e4281820))
|
||||||
|
* **tasks:** add one second interval ([#4074](https://github.com/homarr-labs/homarr/issues/4074)) ([fe676da](https://github.com/homarr-labs/homarr/commit/fe676daad25e674f95e4fa8b949a77b34d376987))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** update dependency dockerode to ^4.0.8 ([#4076](https://github.com/homarr-labs/homarr/issues/4076)) ([43d5a27](https://github.com/homarr-labs/homarr/commit/43d5a27552b9fb89009645e7e711cb62a482e3c7))
|
||||||
|
* **deps:** update dependency jotai to ^2.14.0 ([#4066](https://github.com/homarr-labs/homarr/issues/4066)) ([6880d61](https://github.com/homarr-labs/homarr/commit/6880d61d6b6392b10911d35ffada8102d5b0d6f7))
|
||||||
|
* **deps:** update dependency mysql2 to v3.14.5 ([#4067](https://github.com/homarr-labs/homarr/issues/4067)) ([cd4c236](https://github.com/homarr-labs/homarr/commit/cd4c236e5e6bf5f38f86e8e70a04cdd29bc484a3))
|
||||||
|
* **deps:** update dependency next-intl to v4.3.8 ([#4095](https://github.com/homarr-labs/homarr/issues/4095)) ([2d3af7a](https://github.com/homarr-labs/homarr/commit/2d3af7afca26dac05c602c17d65d31e71d79fb03))
|
||||||
|
* **deps:** update dependency swagger-ui-react to ^5.29.0 ([#4081](https://github.com/homarr-labs/homarr/issues/4081)) ([0e563fc](https://github.com/homarr-labs/homarr/commit/0e563fc47f4c894f0df502afdb9818c7361bbae3))
|
||||||
|
* **deps:** update dependency typescript-eslint to ^8.43.0 ([#4075](https://github.com/homarr-labs/homarr/issues/4075)) ([809fbd5](https://github.com/homarr-labs/homarr/commit/809fbd51766ae88b814d69043d452f8959bdb98b))
|
||||||
|
* **deps:** update dependency undici to v7.16.0 ([#4083](https://github.com/homarr-labs/homarr/issues/4083)) ([4287d17](https://github.com/homarr-labs/homarr/commit/4287d1743b2e90c7957b9c74dcd975ab3d494112))
|
||||||
|
* **deps:** update dependency zod to ^4.1.7 ([#4089](https://github.com/homarr-labs/homarr/issues/4089)) ([687cfd4](https://github.com/homarr-labs/homarr/commit/687cfd4cc7da3c99f727432df18ecf1ebc9f2967))
|
||||||
|
* **deps:** update dependency zod to ^4.1.8 ([#4090](https://github.com/homarr-labs/homarr/issues/4090)) ([6e31e25](https://github.com/homarr-labs/homarr/commit/6e31e2567162b1ee88ea0a2fbb965064ff81ba23))
|
||||||
|
* **deps:** update mantine monorepo to ^8.3.0 ([#4068](https://github.com/homarr-labs/homarr/issues/4068)) ([8c24da8](https://github.com/homarr-labs/homarr/commit/8c24da87320f1d1a54aee5e87254c4bfcc993fb5))
|
||||||
|
* **deps:** update mantine monorepo to ^8.3.1 ([#4070](https://github.com/homarr-labs/homarr/issues/4070)) ([1412b6c](https://github.com/homarr-labs/homarr/commit/1412b6c0d3a46d32c764758fc3cc5b29415dd532))
|
||||||
|
* **deps:** update nextjs monorepo to v15.5.3 ([#4087](https://github.com/homarr-labs/homarr/issues/4087)) ([94fb9b2](https://github.com/homarr-labs/homarr/commit/94fb9b25028529a7320c18cf82d0cbdc238d4c52))
|
||||||
|
* **deps:** update tanstack-query monorepo to ^5.87.4 ([#4071](https://github.com/homarr-labs/homarr/issues/4071)) ([12b2b82](https://github.com/homarr-labs/homarr/commit/12b2b82b4ee34c038e127594fd42216957b41b55))
|
||||||
|
* **system-resources-widget:** chart resizing not working well ([#4069](https://github.com/homarr-labs/homarr/issues/4069)) ([6b3672a](https://github.com/homarr-labs/homarr/commit/6b3672a04d66acd80ea71da2c5fff4e31c24c1aa))
|
||||||
|
|
||||||
|
## [1.37.0](https://github.com/homarr-labs/homarr/compare/v1.36.1...v1.37.0) (2025-09-12)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **integration:** add github app authentication ([#3968](https://github.com/homarr-labs/homarr/issues/3968)) ([bfcbffb](https://github.com/homarr-labs/homarr/commit/bfcbffbdc6499ac5f10bfbf2f5db141eef955049))
|
||||||
|
* **integrations:** add truenas ([#3745](https://github.com/homarr-labs/homarr/issues/3745)) ([dc08bac](https://github.com/homarr-labs/homarr/commit/dc08bacebfe22ee528565259605787720a522b6b))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **adguard-integration:** parse error with protection_disabled_duration field ([#4018](https://github.com/homarr-labs/homarr/issues/4018)) ([eb5af11](https://github.com/homarr-labs/homarr/commit/eb5af116436ddd11b7ff094e882355f951ca622a))
|
||||||
|
* **api:** page is crashing ([#4039](https://github.com/homarr-labs/homarr/issues/4039)) ([a843f60](https://github.com/homarr-labs/homarr/commit/a843f6043b0ed90c5a205b697497490631e0dd19))
|
||||||
|
* **boards:** issues with old-import board name containing spaces ([#4052](https://github.com/homarr-labs/homarr/issues/4052)) ([f7471b6](https://github.com/homarr-labs/homarr/commit/f7471b6c63f80618d0b91b16fb5545e0a3760cbe))
|
||||||
|
* **deps:** update dependency @ctrl/deluge to ^7.2.0 ([#4009](https://github.com/homarr-labs/homarr/issues/4009)) ([e70074b](https://github.com/homarr-labs/homarr/commit/e70074b2191df1dac1e9aa94dbb0cec7073193c9))
|
||||||
|
* **deps:** update dependency @ctrl/qbittorrent to ^9.7.0 ([#4010](https://github.com/homarr-labs/homarr/issues/4010)) ([32b2a09](https://github.com/homarr-labs/homarr/commit/32b2a09ab4e9826b0640d47e1d443682b5fbcff5))
|
||||||
|
* **deps:** update dependency @ctrl/transmission to ^7.3.0 ([#4011](https://github.com/homarr-labs/homarr/issues/4011)) ([bc94e34](https://github.com/homarr-labs/homarr/commit/bc94e34ada04458901389f3d7ba18e76849fe6d4))
|
||||||
|
* **deps:** update dependency @extractus/feed-extractor to v7.1.7 ([#4034](https://github.com/homarr-labs/homarr/issues/4034)) ([fbb86c4](https://github.com/homarr-labs/homarr/commit/fbb86c452d3b8d8cde5357e369fbc14349b71c81))
|
||||||
|
* **deps:** update dependency @gitbeaker/rest to ^43.5.0 ([#4023](https://github.com/homarr-labs/homarr/issues/4023)) ([4fc38bc](https://github.com/homarr-labs/homarr/commit/4fc38bcd5bbed0b3ede2e742c0677bdfbab47295))
|
||||||
|
* **deps:** update dependency dotenv to ^17.2.2 ([#4024](https://github.com/homarr-labs/homarr/issues/4024)) ([1323bb2](https://github.com/homarr-labs/homarr/commit/1323bb276fc8120dd62385b4c79cc630b2078949))
|
||||||
|
* **deps:** update dependency fastify to ^5.6.0 ([#4042](https://github.com/homarr-labs/homarr/issues/4042)) ([a149d8c](https://github.com/homarr-labs/homarr/commit/a149d8c3c112740c9c563463ab72e89c77b9a4ec))
|
||||||
|
* **deps:** update dependency mysql2 to v3.14.4 ([#4013](https://github.com/homarr-labs/homarr/issues/4013)) ([8bf278f](https://github.com/homarr-labs/homarr/commit/8bf278f603ff37e68d7bed41c4c5f6e6351fe94e))
|
||||||
|
* **deps:** update dependency next-intl to v4.3.6 ([#4035](https://github.com/homarr-labs/homarr/issues/4035)) ([a1b5f5c](https://github.com/homarr-labs/homarr/commit/a1b5f5ceea96b18bda8aa7cee7002b10dc4b428c))
|
||||||
|
* **deps:** update dependency next-intl to v4.3.7 ([#4038](https://github.com/homarr-labs/homarr/issues/4038)) ([cd90eaa](https://github.com/homarr-labs/homarr/commit/cd90eaae9da566393114a02765f57f1e6f55fe1c))
|
||||||
|
* **deps:** update dependency sass to ^1.92.0 ([#4025](https://github.com/homarr-labs/homarr/issues/4025)) ([2eef4a8](https://github.com/homarr-labs/homarr/commit/2eef4a8304f0ab5b99313e6e8904582303dd1883))
|
||||||
|
* **deps:** update dependency sass to ^1.92.1 ([#4031](https://github.com/homarr-labs/homarr/issues/4031)) ([8231efa](https://github.com/homarr-labs/homarr/commit/8231efa28195389f4b83a06d137ea6b4b2f49eee))
|
||||||
|
* **deps:** update dependency swagger-ui-react to ^5.28.1 ([#4029](https://github.com/homarr-labs/homarr/issues/4029)) ([ecf1bd9](https://github.com/homarr-labs/homarr/commit/ecf1bd98b74c8e8fca03bcd210fb118382ce2d36))
|
||||||
|
* **deps:** update dependency typescript-eslint to ^8.42.0 ([#4022](https://github.com/homarr-labs/homarr/issues/4022)) ([99179e4](https://github.com/homarr-labs/homarr/commit/99179e4e7d957f5616a24c63982af957613db427))
|
||||||
|
* **deps:** update trpc monorepo to ^11.5.1 ([#4045](https://github.com/homarr-labs/homarr/issues/4045)) ([a7976e9](https://github.com/homarr-labs/homarr/commit/a7976e9d700793d8f48529bc5c1f11f8bca724ce))
|
||||||
|
* **iframe:** disable scrolling not working ([#4017](https://github.com/homarr-labs/homarr/issues/4017)) ([7ac2a81](https://github.com/homarr-labs/homarr/commit/7ac2a81f945c44745917fe4eb8318fdfff3deb14))
|
||||||
|
* **plex-integration:** unknown badge shown for added tv-shows ([#4032](https://github.com/homarr-labs/homarr/issues/4032)) ([4d57c7c](https://github.com/homarr-labs/homarr/commit/4d57c7ca13bdf78b5ac9d0710cd486ad1f41d4d4))
|
||||||
|
* **update-checker:** cached through updates ([#4046](https://github.com/homarr-labs/homarr/issues/4046)) ([26eb2c0](https://github.com/homarr-labs/homarr/commit/26eb2c08c94bd9d440a3c9e914fc9af4ffd48301))
|
||||||
|
* **video-widget:** hls videos not working ([#4015](https://github.com/homarr-labs/homarr/issues/4015)) ([de33439](https://github.com/homarr-labs/homarr/commit/de33439b22d3f5f2ff5856054cfa31a4f78ee088))
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* **deps:** update actions/setup-node action to v5 ([#4058](https://github.com/homarr-labs/homarr/issues/4058)) ([7862612](https://github.com/homarr-labs/homarr/commit/78626121ddfa5462eaad7a6933733ec03a83c04a))
|
||||||
|
|
||||||
|
## [1.36.1](https://github.com/homarr-labs/homarr/compare/v1.36.0...v1.36.1) (2025-09-05)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **dns:** disable caching by default ([#4007](https://github.com/homarr-labs/homarr/issues/4007)) ([10668ee](https://github.com/homarr-labs/homarr/commit/10668ee359940c1e24b543a7a20fe400474e12bf))
|
||||||
|
|
||||||
|
## [1.36.0](https://github.com/homarr-labs/homarr/compare/v1.35.1...v1.36.0) (2025-09-05)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **kubernetes:** add Talos providers ([#3979](https://github.com/homarr-labs/homarr/issues/3979)) ([3e59c20](https://github.com/homarr-labs/homarr/commit/3e59c20dba543634a6a79359f83c13fa81e7ccf3))
|
||||||
|
* **rss-widget:** add hide description option ([#4000](https://github.com/homarr-labs/homarr/issues/4000)) ([b2eb2c3](https://github.com/homarr-labs/homarr/commit/b2eb2c32f6995563feb3c8b4a100be3bfe1d3d80))
|
||||||
|
* **widget:** show name in edit modal ([#3970](https://github.com/homarr-labs/homarr/issues/3970)) ([d317cac](https://github.com/homarr-labs/homarr/commit/d317cacbe83394dd3f6de3e74b53f683654e91d1))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add dns caching ([#3736](https://github.com/homarr-labs/homarr/issues/3736)) ([fb6b5e8](https://github.com/homarr-labs/homarr/commit/fb6b5e88ff3a5e85d06885aed1f2a99788e04f2c))
|
||||||
|
* **deps:** tsx breaks imports like dns-caching ([#3998](https://github.com/homarr-labs/homarr/issues/3998)) ([0476543](https://github.com/homarr-labs/homarr/commit/047654351ab50a730551e61ce98065c9bb7ca3fa))
|
||||||
|
* **deps:** update dependency @ianvs/prettier-plugin-sort-imports to ^4.6.3 ([#3976](https://github.com/homarr-labs/homarr/issues/3976)) ([2b1a3de](https://github.com/homarr-labs/homarr/commit/2b1a3de8dffdd9f089b82e4bcfb304f156f60670))
|
||||||
|
* **deps:** update dependency @ianvs/prettier-plugin-sort-imports to ^4.7.0 ([#3977](https://github.com/homarr-labs/homarr/issues/3977)) ([a3dde2e](https://github.com/homarr-labs/homarr/commit/a3dde2e7ee40fcfddced43f4b5c124010f0410db))
|
||||||
|
* **deps:** update dependency @testcontainers/postgresql to ^11.5.1 ([#3948](https://github.com/homarr-labs/homarr/issues/3948)) ([9f3e68f](https://github.com/homarr-labs/homarr/commit/9f3e68fdfee5936727c97d0d99e174cd368de260))
|
||||||
|
* **deps:** update dependency dayjs to ^1.11.15 ([#3975](https://github.com/homarr-labs/homarr/issues/3975)) ([032f7ff](https://github.com/homarr-labs/homarr/commit/032f7ffd29eddd200d5434ba1c65b8cd5800c8ff))
|
||||||
|
* **deps:** update dependency dayjs to ^1.11.18 ([#3982](https://github.com/homarr-labs/homarr/issues/3982)) ([5e92e66](https://github.com/homarr-labs/homarr/commit/5e92e66803ddf9256fce8a92aec06dabe2491edc))
|
||||||
|
* **deps:** update dependency drizzle-orm to ^0.44.5 ([#3962](https://github.com/homarr-labs/homarr/issues/3962)) ([ea4960e](https://github.com/homarr-labs/homarr/commit/ea4960e11332af0062df964258d9e24def0984e7))
|
||||||
|
* **deps:** update dependency next-intl to v4.3.5 ([#3937](https://github.com/homarr-labs/homarr/issues/3937)) ([81a7f3e](https://github.com/homarr-labs/homarr/commit/81a7f3e93dfd52eea2c87cae8f2d66dd312f7055))
|
||||||
|
* **deps:** update dependency sass to ^1.91.0 ([#3965](https://github.com/homarr-labs/homarr/issues/3965)) ([0e5f43c](https://github.com/homarr-labs/homarr/commit/0e5f43c1c38a7e0186d86c29a73477f943eeb416))
|
||||||
|
* **deps:** update dependency swagger-ui-react to ^5.28.0 ([#3985](https://github.com/homarr-labs/homarr/issues/3985)) ([d2e9b5e](https://github.com/homarr-labs/homarr/commit/d2e9b5e983e2f277da30e98ad313e1afd35d70d3))
|
||||||
|
* **deps:** update dependency tsx to v4.20.5 ([#3938](https://github.com/homarr-labs/homarr/issues/3938)) ([93186c3](https://github.com/homarr-labs/homarr/commit/93186c3be1fdcf6c215efcd8d7236fc44fc74d77))
|
||||||
|
* **deps:** update dependency typescript-eslint to ^8.41.0 ([#3963](https://github.com/homarr-labs/homarr/issues/3963)) ([be0e783](https://github.com/homarr-labs/homarr/commit/be0e78371eb98740f313b1abd20413cbafa68a20))
|
||||||
|
* **deps:** update dependency undici to v7.15.0 ([#3949](https://github.com/homarr-labs/homarr/issues/3949)) ([f166d15](https://github.com/homarr-labs/homarr/commit/f166d153fee521ef31a5328f082b4f52fbfd7239))
|
||||||
|
* **deps:** update dependency zod to ^4.1.1 ([#3950](https://github.com/homarr-labs/homarr/issues/3950)) ([94eb677](https://github.com/homarr-labs/homarr/commit/94eb6772b9216b6377f6306b2a88c2bfe58bfb59))
|
||||||
|
* **deps:** update dependency zod to ^4.1.5 ([#3961](https://github.com/homarr-labs/homarr/issues/3961)) ([9165422](https://github.com/homarr-labs/homarr/commit/916542210d9bfbb40abf27123f4989ec15dcd45b))
|
||||||
|
* **deps:** update dependency zod-form-data to ^3.0.1 ([#3986](https://github.com/homarr-labs/homarr/issues/3986)) ([75683c2](https://github.com/homarr-labs/homarr/commit/75683c206d06da3b54c5ab0605b2a4b37674bbd6))
|
||||||
|
* **deps:** update mantine monorepo to ^8.2.7 ([#3939](https://github.com/homarr-labs/homarr/issues/3939)) ([5d90809](https://github.com/homarr-labs/homarr/commit/5d908093ce3c8b395547681ae0a428afd08a255d))
|
||||||
|
* **deps:** update mantine monorepo to ^8.2.8 ([#4004](https://github.com/homarr-labs/homarr/issues/4004)) ([eef6b86](https://github.com/homarr-labs/homarr/commit/eef6b86ad647fe955bdb125f504b4a98a79baf03))
|
||||||
|
* **deps:** update nextjs monorepo to v15.5.1 ([#3971](https://github.com/homarr-labs/homarr/issues/3971)) ([a1def95](https://github.com/homarr-labs/homarr/commit/a1def958525816ec41198407d6d84ef9384be58f))
|
||||||
|
* **deps:** update nextjs monorepo to v15.5.2 ([#3972](https://github.com/homarr-labs/homarr/issues/3972)) ([b0fe8a9](https://github.com/homarr-labs/homarr/commit/b0fe8a9f653e09cb3f861f39f06bb8d4e8dae006))
|
||||||
|
* **deps:** update tanstack-query monorepo to ^5.85.6 ([#3996](https://github.com/homarr-labs/homarr/issues/3996)) ([4e4483c](https://github.com/homarr-labs/homarr/commit/4e4483c42d9edd3187981e57694721ff5337be8e))
|
||||||
|
* **deps:** update tanstack-query monorepo to ^5.87.1 ([#3997](https://github.com/homarr-labs/homarr/issues/3997)) ([d7146d8](https://github.com/homarr-labs/homarr/commit/d7146d82d14885cffb24e14e7e5a610fdeec1b03))
|
||||||
|
* **deps:** update trpc monorepo to ^11.5.0 ([#3951](https://github.com/homarr-labs/homarr/issues/3951)) ([88eed15](https://github.com/homarr-labs/homarr/commit/88eed15fafbc68c1c669c28ff576aca503acd585))
|
||||||
|
* **header:** avatar menu flashes when opening on mobile ([#3999](https://github.com/homarr-labs/homarr/issues/3999)) ([9cb8261](https://github.com/homarr-labs/homarr/commit/9cb8261ddc5701ff5d5786ea1fba823e1e9317b2))
|
||||||
|
* **widgets:** improve integration selection ([#3969](https://github.com/homarr-labs/homarr/issues/3969)) ([65b9ddd](https://github.com/homarr-labs/homarr/commit/65b9ddd771a573330b4c40966f10bd1fc36a6e1d))
|
||||||
|
|
||||||
## [1.35.1](https://github.com/homarr-labs/homarr/compare/v1.35.0...v1.35.1) (2025-09-03)
|
## [1.35.1](https://github.com/homarr-labs/homarr/compare/v1.35.0...v1.35.1) (2025-09-03)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22.18.0-alpine AS base
|
FROM node:24.10.0-alpine AS base
|
||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
|
|||||||
@@ -42,9 +42,11 @@ const nextConfig: NextConfig = {
|
|||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
key: "Content-Security-Policy",
|
key: "Content-Security-Policy",
|
||||||
|
// worker-src / media-src with blob: is necessary for video.js, see https://github.com/homarr-labs/homarr/issues/3912 and https://stackoverflow.com/questions/65792855/problem-with-video-js-and-content-security-policy-csp
|
||||||
value: `
|
value: `
|
||||||
default-src 'self';
|
default-src 'self';
|
||||||
script-src * 'unsafe-inline' 'unsafe-eval';
|
script-src * 'unsafe-inline' 'unsafe-eval';
|
||||||
|
worker-src * blob:;
|
||||||
base-uri 'self';
|
base-uri 'self';
|
||||||
connect-src *;
|
connect-src *;
|
||||||
style-src * 'unsafe-inline';
|
style-src * 'unsafe-inline';
|
||||||
@@ -53,7 +55,7 @@ const nextConfig: NextConfig = {
|
|||||||
form-action 'self';
|
form-action 'self';
|
||||||
img-src * data:;
|
img-src * data:;
|
||||||
font-src * data:;
|
font-src * data:;
|
||||||
media-src * data:;
|
media-src * data: blob:;
|
||||||
`
|
`
|
||||||
.replace(/\s{2,}/g, " ")
|
.replace(/\s{2,}/g, " ")
|
||||||
.trim(),
|
.trim(),
|
||||||
|
|||||||
+30
-30
@@ -50,59 +50,59 @@
|
|||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@homarr/widgets": "workspace:^0.1.0",
|
"@homarr/widgets": "workspace:^0.1.0",
|
||||||
"@mantine/colors-generator": "^8.2.5",
|
"@mantine/colors-generator": "^8.3.5",
|
||||||
"@mantine/core": "^8.2.5",
|
"@mantine/core": "^8.3.5",
|
||||||
"@mantine/dropzone": "^8.2.5",
|
"@mantine/dropzone": "^8.3.5",
|
||||||
"@mantine/hooks": "^8.2.5",
|
"@mantine/hooks": "^8.3.5",
|
||||||
"@mantine/modals": "^8.2.5",
|
"@mantine/modals": "^8.3.5",
|
||||||
"@mantine/tiptap": "^8.2.5",
|
"@mantine/tiptap": "^8.3.5",
|
||||||
"@million/lint": "1.0.14",
|
"@million/lint": "1.0.14",
|
||||||
"@tabler/icons-react": "^3.34.1",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"@tanstack/react-query": "^5.85.5",
|
"@tanstack/react-query": "^5.90.5",
|
||||||
"@tanstack/react-query-devtools": "^5.85.5",
|
"@tanstack/react-query-devtools": "^5.90.2",
|
||||||
"@tanstack/react-query-next-experimental": "^5.85.5",
|
"@tanstack/react-query-next-experimental": "^5.90.2",
|
||||||
"@trpc/client": "^11.4.4",
|
"@trpc/client": "^11.6.0",
|
||||||
"@trpc/next": "^11.4.4",
|
"@trpc/next": "^11.6.0",
|
||||||
"@trpc/react-query": "^11.4.4",
|
"@trpc/react-query": "^11.6.0",
|
||||||
"@trpc/server": "^11.4.4",
|
"@trpc/server": "^11.6.0",
|
||||||
"@xterm/addon-canvas": "^0.7.0",
|
"@xterm/addon-canvas": "^0.7.0",
|
||||||
"@xterm/addon-fit": "0.10.0",
|
"@xterm/addon-fit": "0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"chroma-js": "^3.1.2",
|
"chroma-js": "^3.1.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.18",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.3",
|
||||||
"flag-icons": "^7.5.0",
|
"flag-icons": "^7.5.0",
|
||||||
"glob": "^11.0.3",
|
"glob": "^11.0.3",
|
||||||
"jotai": "^2.13.1",
|
"jotai": "^2.15.0",
|
||||||
"mantine-react-table": "2.0.0-beta.9",
|
"mantine-react-table": "2.0.0-beta.9",
|
||||||
"next": "15.5.0",
|
"next": "15.5.6",
|
||||||
"postcss-preset-mantine": "^1.18.0",
|
"postcss-preset-mantine": "^1.18.0",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"react": "19.1.1",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.2.0",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-simple-code-editor": "^0.14.1",
|
"react-simple-code-editor": "^0.14.1",
|
||||||
"sass": "^1.90.0",
|
"sass": "^1.93.2",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.3",
|
||||||
"swagger-ui-react": "^5.27.1",
|
"swagger-ui-react": "^5.29.5",
|
||||||
"use-deep-compare-effect": "^1.8.1",
|
"use-deep-compare-effect": "^1.8.1",
|
||||||
"zod": "^4.0.17"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/chroma-js": "3.1.1",
|
"@types/chroma-js": "3.1.1",
|
||||||
"@types/node": "^22.17.2",
|
"@types/node": "^24.9.1",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/react": "19.1.10",
|
"@types/react": "19.2.2",
|
||||||
"@types/react-dom": "19.1.7",
|
"@types/react-dom": "19.2.2",
|
||||||
"@types/swagger-ui-react": "^5.18.0",
|
"@types/swagger-ui-react": "^5.18.0",
|
||||||
"concurrently": "^9.2.0",
|
"concurrently": "^9.2.1",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.38.0",
|
||||||
"node-loader": "^2.1.0",
|
"node-loader": "^2.1.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,10 +19,12 @@ import {
|
|||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import { useSession } from "@homarr/auth/client";
|
||||||
import { useRequiredBoard } from "@homarr/boards/context";
|
import { useRequiredBoard } from "@homarr/boards/context";
|
||||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
import { env } from "@homarr/common/env";
|
import { env } from "@homarr/common/env";
|
||||||
|
import { hotkeys } from "@homarr/definitions";
|
||||||
import { useConfirmModal, useModalAction } from "@homarr/modals";
|
import { useConfirmModal, useModalAction } from "@homarr/modals";
|
||||||
import { AppSelectModal } from "@homarr/modals-collection";
|
import { AppSelectModal } from "@homarr/modals-collection";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
@@ -61,6 +63,7 @@ export const BoardContentHeaderActions = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AddMenu = () => {
|
const AddMenu = () => {
|
||||||
|
const { data: session } = useSession();
|
||||||
const { openModal: openCategoryEditModal } = useModalAction(CategoryEditModal);
|
const { openModal: openCategoryEditModal } = useModalAction(CategoryEditModal);
|
||||||
const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal);
|
const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal);
|
||||||
const { openModal: openAppSelectModal } = useModalAction(AppSelectModal);
|
const { openModal: openAppSelectModal } = useModalAction(AppSelectModal);
|
||||||
@@ -95,12 +98,13 @@ const AddMenu = () => {
|
|||||||
|
|
||||||
const handleSelectApp = useCallback(() => {
|
const handleSelectApp = useCallback(() => {
|
||||||
openAppSelectModal({
|
openAppSelectModal({
|
||||||
onSelect: (appId) => {
|
onSelect: (app) => {
|
||||||
createItem({
|
createItem({
|
||||||
kind: "app",
|
kind: "app",
|
||||||
options: { appId },
|
options: { appId: app.id },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
withCreate: session?.user.permissions.includes("app-create") ?? false,
|
||||||
});
|
});
|
||||||
}, [openAppSelectModal, createItem]);
|
}, [openAppSelectModal, createItem]);
|
||||||
|
|
||||||
@@ -165,7 +169,7 @@ const EditModeMenu = () => {
|
|||||||
open();
|
open();
|
||||||
}, [board, isEditMode, saveBoard, open]);
|
}, [board, isEditMode, saveBoard, open]);
|
||||||
|
|
||||||
useHotkeys([["mod+e", toggle]]);
|
useHotkeys([[hotkeys.toggleBoardEdit, toggle]]);
|
||||||
usePreventLeaveWithDirty(isEditMode);
|
usePreventLeaveWithDirty(isEditMode);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -120,11 +120,14 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
|
|||||||
/>
|
/>
|
||||||
{session?.user.permissions.includes("media-upload") && (
|
{session?.user.permissions.includes("media-upload") && (
|
||||||
<UploadMedia
|
<UploadMedia
|
||||||
onSuccess={({ url }) =>
|
onSuccess={(medias) => {
|
||||||
|
const first = medias.at(0);
|
||||||
|
if (!first) return;
|
||||||
|
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
form.setFieldValue("backgroundImageUrl", url);
|
form.setFieldValue("backgroundImageUrl", first.url);
|
||||||
})
|
});
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
{({ onClick, loading }) => (
|
{({ onClick, loading }) => (
|
||||||
<ActionIcon onClick={onClick} loading={loading} mt={24} size={36} variant="default">
|
<ActionIcon onClick={onClick} loading={loading} mt={24} size={36} variant="default">
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ const getBoardAndPermissionsAsync = async (params: Awaited<Props["params"]>) =>
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export const createBoardLayout = <TParams extends Params>({
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
const colorScheme = await getCurrentColorSchemeAsync();
|
const colorScheme = await getCurrentColorSchemeAsync();
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import type { SupportedLanguage } from "@homarr/translation";
|
|||||||
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
|
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
|
||||||
|
|
||||||
import { Analytics } from "~/components/layout/analytics";
|
import { Analytics } from "~/components/layout/analytics";
|
||||||
|
import { CrowdinLiveTranslation } from "~/components/layout/crowdin-live-translation";
|
||||||
import { SearchEngineOptimization } from "~/components/layout/search-engine-optimization";
|
import { SearchEngineOptimization } from "~/components/layout/search-engine-optimization";
|
||||||
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
|
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
|
||||||
import { DayJsLoader } from "./_client-providers/dayjs-loader";
|
import { DayJsLoader } from "./_client-providers/dayjs-loader";
|
||||||
@@ -118,10 +119,12 @@ export default async function Layout(props: {
|
|||||||
(innerProps) => <SpotlightProvider {...innerProps} />,
|
(innerProps) => <SpotlightProvider {...innerProps} />,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const { locale } = await props.params;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Instead of ColorSchemScript we use data-mantine-color-scheme to prevent flickering
|
// Instead of ColorSchemScript we use data-mantine-color-scheme to prevent flickering
|
||||||
<html
|
<html
|
||||||
lang={(await props.params).locale}
|
lang={locale}
|
||||||
dir={direction}
|
dir={direction}
|
||||||
data-mantine-color-scheme={colorScheme}
|
data-mantine-color-scheme={colorScheme}
|
||||||
style={{
|
style={{
|
||||||
@@ -132,6 +135,7 @@ export default async function Layout(props: {
|
|||||||
<head>
|
<head>
|
||||||
<Analytics />
|
<Analytics />
|
||||||
<SearchEngineOptimization />
|
<SearchEngineOptimization />
|
||||||
|
<CrowdinLiveTranslation locale={locale} />
|
||||||
</head>
|
</head>
|
||||||
<body className={["font-sans", fontSans.variable].join(" ")}>
|
<body className={["font-sans", fontSans.variable].join(" ")}>
|
||||||
<StackedProvider>
|
<StackedProvider>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Box, Grid, GridCol, Group, Image, Stack, Title } from "@mantine/core";
|
|||||||
|
|
||||||
import { splitToNChunks } from "@homarr/common";
|
import { splitToNChunks } from "@homarr/common";
|
||||||
import { integrationDefs } from "@homarr/definitions";
|
import { integrationDefs } from "@homarr/definitions";
|
||||||
|
import { getScopedI18n } from "@homarr/translation/server";
|
||||||
|
|
||||||
import classes from "./hero-banner.module.css";
|
import classes from "./hero-banner.module.css";
|
||||||
|
|
||||||
@@ -12,19 +13,20 @@ const icons = Object.values(integrationDefs)
|
|||||||
const countIconGroups = 3;
|
const countIconGroups = 3;
|
||||||
const animationDurationInSeconds = icons.length;
|
const animationDurationInSeconds = icons.length;
|
||||||
const arrayInChunks = splitToNChunks(icons, countIconGroups);
|
const arrayInChunks = splitToNChunks(icons, countIconGroups);
|
||||||
|
const gridSpan = 12 / countIconGroups;
|
||||||
|
|
||||||
export const HeroBanner = () => {
|
export const HeroBanner = async () => {
|
||||||
const gridSpan = 12 / countIconGroups;
|
const t = await getScopedI18n("management.page.home");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes.bannerContainer} p={{ base: "lg", md: "3rem" }} bg="dark.6" pos="relative">
|
<Box className={classes.bannerContainer} p={{ base: "lg", md: "3rem" }} bg="dark.6" pos="relative">
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
<Title fz={{ base: "h4", md: "h2" }} c="dimmed">
|
<Title fz={{ base: "h4", md: "h2" }} c="dimmed">
|
||||||
Welcome back to your
|
{t("heroBanner.title")}
|
||||||
</Title>
|
</Title>
|
||||||
<Group gap="xs" wrap="nowrap">
|
<Group gap="xs" wrap="nowrap">
|
||||||
<Image src="/logo/logo.png" w={{ base: 32, md: 40 }} h={{ base: 32, md: 40 }} />
|
<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>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Box visibleFrom="md" className={classes.scrollContainer} w={"30%"} top={0} right={0} pos="absolute">
|
<Box visibleFrom="md" className={classes.scrollContainer} w={"30%"} top={0} right={0} pos="absolute">
|
||||||
|
|||||||
@@ -11,14 +11,23 @@ import {
|
|||||||
Center,
|
Center,
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
|
Kbd,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
Stack,
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableTbody,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableThead,
|
||||||
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} 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 { getScopedI18n } from "@homarr/translation/server";
|
||||||
|
|
||||||
import { homarrLogoPath } from "~/components/layout/logo/homarr-logo";
|
import { homarrLogoPath } from "~/components/layout/logo/homarr-logo";
|
||||||
@@ -149,6 +158,45 @@ export default async function AboutPage() {
|
|||||||
</List>
|
</List>
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</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>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+7
@@ -1,9 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
|
IconCode,
|
||||||
IconGrid3x3,
|
IconGrid3x3,
|
||||||
IconKey,
|
IconKey,
|
||||||
|
IconLink,
|
||||||
IconMessage,
|
IconMessage,
|
||||||
IconPassword,
|
IconPassword,
|
||||||
IconPasswordUser,
|
IconPasswordUser,
|
||||||
|
IconPlug,
|
||||||
IconServer,
|
IconServer,
|
||||||
IconUser,
|
IconUser,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
@@ -19,6 +22,10 @@ export const integrationSecretIcons = {
|
|||||||
tokenId: IconGrid3x3,
|
tokenId: IconGrid3x3,
|
||||||
personalAccessToken: IconPasswordUser,
|
personalAccessToken: IconPasswordUser,
|
||||||
topic: IconMessage,
|
topic: IconMessage,
|
||||||
|
url: IconLink,
|
||||||
opnsenseApiKey: IconKey,
|
opnsenseApiKey: IconKey,
|
||||||
opnsenseApiSecret: IconPassword,
|
opnsenseApiSecret: IconPassword,
|
||||||
|
githubAppId: IconCode,
|
||||||
|
githubInstallationId: IconPlug,
|
||||||
|
privateKey: IconKey,
|
||||||
} satisfies Record<IntegrationSecretKind, TablerIcon>;
|
} satisfies Record<IntegrationSecretKind, TablerIcon>;
|
||||||
+32
-4
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ChangeEventHandler, FocusEventHandler } from "react";
|
import type { ChangeEventHandler, FocusEventHandler } from "react";
|
||||||
import { PasswordInput, TextInput } from "@mantine/core";
|
import { PasswordInput, Textarea, TextInput } from "@mantine/core";
|
||||||
|
|
||||||
import { integrationSecretKindObject } from "@homarr/definitions";
|
import { integrationSecretKindObject } from "@homarr/definitions";
|
||||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||||
@@ -14,9 +14,9 @@ interface IntegrationSecretInputProps {
|
|||||||
label?: string;
|
label?: string;
|
||||||
kind: IntegrationSecretKind;
|
kind: IntegrationSecretKind;
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange: ChangeEventHandler<HTMLInputElement>;
|
onChange: ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
||||||
onFocus?: FocusEventHandler<HTMLInputElement>;
|
onFocus?: FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
||||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
onBlur?: FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +31,19 @@ export const IntegrationSecretInput = (props: IntegrationSecretInputProps) => {
|
|||||||
const PublicSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
|
const PublicSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const Icon = integrationSecretIcons[kind];
|
const Icon = integrationSecretIcons[kind];
|
||||||
|
const { multiline } = integrationSecretKindObject[kind];
|
||||||
|
if (multiline) {
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
{...props}
|
||||||
|
label={props.label ?? t(`integration.secrets.kind.${kind}.label`)}
|
||||||
|
w="100%"
|
||||||
|
leftSection={<Icon size={20} stroke={1.5} />}
|
||||||
|
autosize
|
||||||
|
minRows={2}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -45,6 +58,21 @@ const PublicSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
|
|||||||
const PrivateSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
|
const PrivateSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const Icon = integrationSecretIcons[kind];
|
const Icon = integrationSecretIcons[kind];
|
||||||
|
const { multiline } = integrationSecretKindObject[kind];
|
||||||
|
|
||||||
|
if (multiline) {
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
{...props}
|
||||||
|
label={props.label ?? t(`integration.secrets.kind.${kind}.label`)}
|
||||||
|
description={t("integration.secrets.secureNotice")}
|
||||||
|
w="100%"
|
||||||
|
leftSection={<Icon size={20} stroke={1.5} />}
|
||||||
|
autosize
|
||||||
|
minRows={2}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
|
|||||||
+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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+174
-39
@@ -3,21 +3,25 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Button, Fieldset, Group, Stack, TextInput } from "@mantine/core";
|
import { Alert, Anchor, Button, ButtonGroup, Fieldset, Group, Stack, Text, TextInput } from "@mantine/core";
|
||||||
import type { z } from "zod/v4";
|
import { IconInfoCircle, IconPencil, IconPlus, IconUnlink } from "@tabler/icons-react";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import { useSession } from "@homarr/auth/client";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||||
import { getAllSecretKindOptions, getDefaultSecretKinds } from "@homarr/definitions";
|
import { getAllSecretKindOptions, getDefaultSecretKinds } from "@homarr/definitions";
|
||||||
import { useZodForm } from "@homarr/form";
|
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 { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { integrationUpdateSchema } from "@homarr/validation/integration";
|
import { integrationUpdateSchema } from "@homarr/validation/integration";
|
||||||
|
|
||||||
import { SecretCard } from "../../_components/secrets/integration-secret-card";
|
import { SecretCard } from "../../_components/secrets/integration-secret-card";
|
||||||
import { IntegrationSecretInput } from "../../_components/secrets/integration-secret-inputs";
|
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 { IntegrationTestConnectionError } from "../../_components/test-connection/integration-test-connection-error";
|
||||||
import type { AnyMappedTestConnectionError } from "../../_components/test-connection/types";
|
import type { AnyMappedTestConnectionError } from "../../_components/test-connection/types";
|
||||||
|
|
||||||
@@ -25,23 +29,41 @@ interface EditIntegrationForm {
|
|||||||
integration: RouterOutputs["integration"]["byId"];
|
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) => {
|
export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
const secretsKinds =
|
const allSecretKinds = getAllSecretKindOptions(integration.kind);
|
||||||
|
|
||||||
|
const initialSecretsKinds =
|
||||||
getAllSecretKindOptions(integration.kind).find((secretKinds) =>
|
getAllSecretKindOptions(integration.kind).find((secretKinds) =>
|
||||||
integration.secrets.every((secret) => secretKinds.includes(secret.kind)),
|
integration.secrets.every((secret) => secretKinds.includes(secret.kind)),
|
||||||
) ?? getDefaultSecretKinds(integration.kind);
|
) ?? getDefaultSecretKinds(integration.kind);
|
||||||
|
|
||||||
|
const hasUrlSecret = initialSecretsKinds.includes("url");
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useZodForm(integrationUpdateSchema.omit({ id: true }), {
|
const form = useZodForm(formSchema, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: integration.name,
|
name: integration.name,
|
||||||
url: integration.url,
|
url: integration.url,
|
||||||
secrets: secretsKinds.map((kind) => ({
|
secrets: initialSecretsKinds.map((kind) => ({
|
||||||
kind,
|
kind,
|
||||||
value: integration.secrets.find((secret) => secret.kind === kind)?.value ?? "",
|
value: integration.secrets.find((secret) => secret.kind === kind)?.value ?? "",
|
||||||
})),
|
})),
|
||||||
|
app: integration.app ?? null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { mutateAsync, isPending } = clientApi.integration.update.useMutation();
|
const { mutateAsync, isPending } = clientApi.integration.update.useMutation();
|
||||||
@@ -49,15 +71,20 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
|||||||
|
|
||||||
const secretsMap = new Map(integration.secrets.map((secret) => [secret.kind, secret]));
|
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;
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
id: integration.id,
|
id: integration.id,
|
||||||
...values,
|
...values,
|
||||||
|
url,
|
||||||
secrets: values.secrets.map((secret) => ({
|
secrets: values.secrets.map((secret) => ({
|
||||||
kind: secret.kind,
|
kind: secret.kind,
|
||||||
value: secret.value === "" ? null : secret.value,
|
value: secret.value === "" ? null : secret.value,
|
||||||
})),
|
})),
|
||||||
|
appId: app?.id ?? null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -87,48 +114,80 @@ 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 (
|
return (
|
||||||
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
|
<form onSubmit={form.onSubmit(async (values) => await handleSubmitAsync(values))}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<TextInput withAsterisk label={t("integration.field.name.label")} {...form.getInputProps("name")} />
|
<TextInput withAsterisk label={t("integration.field.name.label")} {...form.getInputProps("name")} />
|
||||||
|
|
||||||
<TextInput withAsterisk label={t("integration.field.url.label")} {...form.getInputProps("url")} />
|
{hasUrlSecret ? null : (
|
||||||
|
<TextInput withAsterisk label={t("integration.field.url.label")} {...form.getInputProps("url")} />
|
||||||
|
)}
|
||||||
|
|
||||||
<Fieldset legend={t("integration.secrets.title")}>
|
<Fieldset legend={t("integration.secrets.title")}>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
{secretsKinds.map((kind, index) => (
|
{allSecretKinds.length > 1 && (
|
||||||
<SecretCard
|
<SecretKindsSegmentedControl
|
||||||
key={kind}
|
defaultKinds={initialSecretsKinds}
|
||||||
secret={secretsMap.get(kind) ?? { kind, value: null, updatedAt: null }}
|
secretKinds={allSecretKinds}
|
||||||
onCancel={() =>
|
form={form}
|
||||||
new Promise((resolve) => {
|
/>
|
||||||
// When nothing changed, just close the secret card
|
)}
|
||||||
if ((form.values.secrets[index]?.value ?? "") === (secretsMap.get(kind)?.value ?? "")) {
|
{!isInitialSecretKinds
|
||||||
return resolve(true);
|
? 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"),
|
<IntegrationSecretInput
|
||||||
children: t("integration.secrets.reset.message"),
|
label={t(`integration.secrets.kind.${secret.kind}.newLabel`)}
|
||||||
onCancel: () => resolve(false),
|
key={secret.kind}
|
||||||
onConfirm: () => {
|
kind={secret.kind}
|
||||||
form.setFieldValue(`secrets.${index}.value`, secretsMap.get(kind)?.value ?? "");
|
{...form.getInputProps(`secrets.${index}.value`)}
|
||||||
resolve(true);
|
/>
|
||||||
},
|
</SecretCard>
|
||||||
});
|
))}
|
||||||
})
|
{isInitialSecretKinds
|
||||||
}
|
? null
|
||||||
>
|
: form.values.secrets.map(({ kind }, index) => (
|
||||||
<IntegrationSecretInput
|
<IntegrationSecretInput
|
||||||
label={t(`integration.secrets.kind.${kind}.newLabel`)}
|
withAsterisk
|
||||||
key={kind}
|
key={kind}
|
||||||
kind={kind}
|
kind={kind}
|
||||||
{...form.getInputProps(`secrets.${index}.value`)}
|
{...form.getInputProps(`secrets.${index}.value`)}
|
||||||
/>
|
/>
|
||||||
</SecretCard>
|
))}
|
||||||
))}
|
{form.values.secrets.length === 0 && (
|
||||||
|
<Alert icon={<IconInfoCircle size={"1rem"} />} color={"blue"}>
|
||||||
|
<Text c={"blue"}>{t("integration.secrets.noSecretsRequired.text")}</Text>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
|
|
||||||
|
<IntegrationLinkApp value={form.values.app} onChange={(app) => form.setFieldValue("app", app)} />
|
||||||
|
|
||||||
{error !== null && <IntegrationTestConnectionError error={error} url={form.values.url} />}
|
{error !== null && <IntegrationTestConnectionError error={error} url={form.values.url} />}
|
||||||
|
|
||||||
<Group justify="end" align="center">
|
<Group justify="end" align="center">
|
||||||
@@ -144,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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useState } from "react";
|
import { startTransition, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
@@ -10,17 +10,21 @@ import {
|
|||||||
Collapse,
|
Collapse,
|
||||||
Fieldset,
|
Fieldset,
|
||||||
Group,
|
Group,
|
||||||
|
Loader,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
|
Select,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconInfoCircle } from "@tabler/icons-react";
|
import { IconCheck, IconInfoCircle } from "@tabler/icons-react";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import { useSession } from "@homarr/auth/client";
|
||||||
import { revalidatePathActionAsync } from "@homarr/common/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 {
|
import {
|
||||||
getAllSecretKindOptions,
|
getAllSecretKindOptions,
|
||||||
getIconUrl,
|
getIconUrl,
|
||||||
@@ -28,14 +32,15 @@ import {
|
|||||||
getIntegrationName,
|
getIntegrationName,
|
||||||
integrationDefs,
|
integrationDefs,
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
import type { UseFormReturnType } from "@homarr/form";
|
import type { GetInputPropsReturnType, UseFormReturnType } from "@homarr/form";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { appHrefSchema } from "@homarr/validation/app";
|
import { appHrefSchema } from "@homarr/validation/app";
|
||||||
import { integrationCreateSchema } from "@homarr/validation/integration";
|
import { integrationCreateSchema } from "@homarr/validation/integration";
|
||||||
|
|
||||||
import { IntegrationSecretInput } from "../_components/secrets/integration-secret-inputs";
|
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 { IntegrationTestConnectionError } from "../_components/test-connection/integration-test-connection-error";
|
||||||
import type { AnyMappedTestConnectionError } from "../_components/test-connection/types";
|
import type { AnyMappedTestConnectionError } from "../_components/test-connection/types";
|
||||||
|
|
||||||
@@ -45,48 +50,70 @@ interface NewIntegrationFormProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const formSchema = integrationCreateSchema.omit({ kind: true }).and(
|
const formSchema = integrationCreateSchema.omit({ kind: true, app: true }).and(
|
||||||
z.object({
|
z.object({
|
||||||
createApp: z.boolean(),
|
hasApp: z.boolean(),
|
||||||
appHref: appHrefSchema,
|
appHref: appHrefSchema,
|
||||||
|
appId: z.string().nullable(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) => {
|
export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const secretKinds = getAllSecretKindOptions(searchParams.kind);
|
const secretKinds = getAllSecretKindOptions(searchParams.kind);
|
||||||
|
const hasUrlSecret = secretKinds.some((kinds) => kinds.includes("url"));
|
||||||
const router = useRouter();
|
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) {
|
||||||
|
// Placeholder Url, replaced with origin of the secret Url on submit
|
||||||
|
url = "http://localhost";
|
||||||
|
}
|
||||||
const form = useZodForm(formSchema, {
|
const form = useZodForm(formSchema, {
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: searchParams.name ?? getIntegrationName(searchParams.kind),
|
name: searchParams.name ?? getIntegrationName(searchParams.kind),
|
||||||
url: searchParams.url ?? getIntegrationDefaultUrl(searchParams.kind) ?? "",
|
url,
|
||||||
secrets: secretKinds[0].map((kind) => ({
|
secrets: secretKinds[0].map((kind) => ({
|
||||||
kind,
|
kind,
|
||||||
value: "",
|
value: "",
|
||||||
})),
|
})),
|
||||||
attemptSearchEngineCreation: true,
|
attemptSearchEngineCreation: true,
|
||||||
createApp: false,
|
hasApp: false,
|
||||||
appHref: "",
|
appHref: url,
|
||||||
},
|
appId: null,
|
||||||
onValuesChange(values, previous) {
|
|
||||||
if (values.createApp !== previous.createApp) {
|
|
||||||
setOpened(values.createApp);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createIntegrationAsync, isPending: isPendingIntegration } =
|
const { mutateAsync: createIntegrationAsync, isPending } = clientApi.integration.create.useMutation();
|
||||||
clientApi.integration.create.useMutation();
|
|
||||||
const { mutateAsync: createAppAsync, isPending: isPendingApp } = clientApi.app.create.useMutation();
|
|
||||||
const isPending = isPendingIntegration || isPendingApp;
|
|
||||||
const [error, setError] = useState<null | AnyMappedTestConnectionError>(null);
|
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(
|
await createIntegrationAsync(
|
||||||
{
|
{
|
||||||
kind: searchParams.kind,
|
kind: searchParams.kind,
|
||||||
...values,
|
...values,
|
||||||
|
url,
|
||||||
|
app,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
async onSuccess(data) {
|
async onSuccess(data) {
|
||||||
@@ -105,32 +132,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
|||||||
message: t("integration.page.create.notification.success.message"),
|
message: t("integration.page.create.notification.success.message"),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!values.createApp) {
|
await revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
|
||||||
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 : values.url,
|
|
||||||
iconUrl: getIconUrl(searchParams.kind),
|
|
||||||
description: null,
|
|
||||||
pingUrl: values.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"),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showErrorNotification({
|
showErrorNotification({
|
||||||
@@ -149,7 +151,9 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
|||||||
<Stack>
|
<Stack>
|
||||||
<TextInput withAsterisk label={t("integration.field.name.label")} autoFocus {...form.getInputProps("name")} />
|
<TextInput withAsterisk label={t("integration.field.name.label")} autoFocus {...form.getInputProps("name")} />
|
||||||
|
|
||||||
<TextInput withAsterisk label={t("integration.field.url.label")} {...form.getInputProps("url")} />
|
{hasUrlSecret ? null : (
|
||||||
|
<TextInput withAsterisk label={t("integration.field.url.label")} {...form.getInputProps("url")} />
|
||||||
|
)}
|
||||||
|
|
||||||
<Fieldset legend={t("integration.secrets.title")}>
|
<Fieldset legend={t("integration.secrets.title")}>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
@@ -182,15 +186,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Checkbox
|
<AppForm form={form} canCreateApps={canCreateApps} />
|
||||||
{...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>
|
|
||||||
|
|
||||||
<Group justify="end" align="center">
|
<Group justify="end" align="center">
|
||||||
<Button variant="default" component={Link} href="/manage/integrations">
|
<Button variant="default" component={Link} href="/manage/integrations">
|
||||||
@@ -205,40 +201,115 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SecretKindsSegmentedControlProps {
|
type FormType = z.infer<typeof formSchema>;
|
||||||
secretKinds: IntegrationSecretKind[][];
|
|
||||||
form: UseFormReturnType<FormType, (values: FormType) => FormType>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SecretKindsSegmentedControl = ({ secretKinds, form }: SecretKindsSegmentedControlProps) => {
|
const AppForm = ({ form, canCreateApps }: { form: UseFormReturnType<FormType>; canCreateApps: boolean }) => {
|
||||||
const t = useScopedI18n("integration.secrets");
|
const t = useI18n();
|
||||||
|
const checkboxInputProps = form.getInputProps("hasApp", { type: "checkbox" });
|
||||||
|
|
||||||
const secretKindGroups = secretKinds.map((kinds) => ({
|
return (
|
||||||
label:
|
<>
|
||||||
kinds.length === 0
|
<Checkbox
|
||||||
? t("noSecretsRequired.segmentTitle")
|
{...checkboxInputProps}
|
||||||
: kinds.map((kind) => t(`kind.${kind}.label`)).join(" & "),
|
onChange={(event) => {
|
||||||
value: kinds.length === 0 ? "empty" : kinds.join("-"),
|
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(
|
<Collapse in={form.values.hasApp}>
|
||||||
(value: string) => {
|
<Fieldset legend={t("integration.field.app.sectionTitle")}>
|
||||||
if (value === "empty") {
|
<Stack gap="sm">
|
||||||
form.setFieldValue("secrets", []);
|
{canCreateApps && (
|
||||||
return;
|
<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[];
|
{typeof form.values.appHref === "string" && canCreateApps ? (
|
||||||
const secrets = kinds.map((kind) => ({
|
<TextInput
|
||||||
kind,
|
placeholder={t("integration.field.appHref.placeholder")}
|
||||||
value: "",
|
withAsterisk
|
||||||
}));
|
label={t("integration.page.create.app.option.new.url.label")}
|
||||||
form.setFieldValue("secrets", secrets);
|
description={t("integration.page.create.app.option.new.url.description")}
|
||||||
},
|
{...form.getInputProps("appHref")}
|
||||||
[form],
|
/>
|
||||||
|
) : (
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -14,7 +14,7 @@ export const UploadMediaButton = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UploadMedia onSettled={onSettledAsync}>
|
<UploadMedia onSettled={onSettledAsync} multiple>
|
||||||
{({ onClick, loading }) => (
|
{({ onClick, loading }) => (
|
||||||
<Button onClick={onClick} loading={loading} rightSection={<IconUpload size={16} stroke={1.5} />}>
|
<Button onClick={onClick} loading={loading} rightSection={<IconUpload size={16} stroke={1.5} />}>
|
||||||
{t("media.action.upload.label")}
|
{t("media.action.upload.label")}
|
||||||
|
|||||||
@@ -1,25 +1,16 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Card, Group, SimpleGrid, Space, Stack, Text } from "@mantine/core";
|
import { Card, Group, SimpleGrid, Space, Stack, Text } from "@mantine/core";
|
||||||
import { IconArrowRight } from "@tabler/icons-react";
|
import { IconArrowRight } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { api } from "@homarr/api/server";
|
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 { getScopedI18n } from "@homarr/translation/server";
|
||||||
|
|
||||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||||
import { createMetaTitle } from "~/metadata";
|
import { createMetaTitle } from "~/metadata";
|
||||||
import { HeroBanner } from "./_components/hero-banner";
|
import { HeroBanner } from "./_components/hero-banner";
|
||||||
|
|
||||||
interface LinkProps {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
count: number;
|
|
||||||
href: string;
|
|
||||||
hidden?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateMetadata() {
|
|
||||||
const t = await getScopedI18n("management");
|
const t = await getScopedI18n("management");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -29,78 +20,32 @@ export async function generateMetadata() {
|
|||||||
|
|
||||||
export default async function ManagementPage() {
|
export default async function ManagementPage() {
|
||||||
const statistics = await api.home.getStats();
|
const statistics = await api.home.getStats();
|
||||||
const session = await auth();
|
|
||||||
const t = await getScopedI18n("management.page.home");
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<DynamicBreadcrumb />
|
<DynamicBreadcrumb />
|
||||||
<HeroBanner />
|
<HeroBanner />
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
|
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
|
||||||
{links.map(
|
{statistics.map((statistic) => (
|
||||||
(link) =>
|
<Card component={Link} href={statistic.path} key={statistic.path} radius="lg">
|
||||||
!link.hidden && (
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<Card component={Link} href={link.href} key={link.href} radius="lg">
|
<Group wrap="nowrap">
|
||||||
<Group justify="space-between" wrap="nowrap">
|
<Text size="2.4rem" fw="bolder">
|
||||||
<Group wrap="nowrap">
|
{statistic.count}
|
||||||
<Text size="2.4rem" fw="bolder">
|
</Text>
|
||||||
{link.count}
|
<Stack gap={0}>
|
||||||
</Text>
|
<Text c="red" size="xs">
|
||||||
<Stack gap={0}>
|
{t(`statisticLabel.${statistic.subtitleKey}`)}
|
||||||
<Text c="red" size="xs">
|
</Text>
|
||||||
{link.subtitle}
|
<Text fw="bold">{t(`statistic.${statistic.titleKey}`)}</Text>
|
||||||
</Text>
|
</Stack>
|
||||||
<Text fw="bold">{link.title}</Text>
|
</Group>
|
||||||
</Stack>
|
<IconArrowRight />
|
||||||
</Group>
|
</Group>
|
||||||
<IconArrowRight />
|
</Card>
|
||||||
</Group>
|
))}
|
||||||
</Card>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
|||||||
import { IconPowerOff } from "@homarr/ui/icons";
|
import { IconPowerOff } from "@homarr/ui/icons";
|
||||||
|
|
||||||
const cronExpressions = [
|
const cronExpressions = [
|
||||||
|
{
|
||||||
|
value: "*/1 * * * * *",
|
||||||
|
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.seconds", { interval: 1 }),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: "*/5 * * * * *",
|
value: "*/5 * * * * *",
|
||||||
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.seconds", { interval: 5 }),
|
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.seconds", { interval: 5 }),
|
||||||
|
|||||||
+14
-28
@@ -92,38 +92,24 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
|||||||
});
|
});
|
||||||
}, [mutate, user.id, openConfirmModal, tManageAvatar]);
|
}, [mutate, user.id, openConfirmModal, tManageAvatar]);
|
||||||
|
|
||||||
const isCredentialsUser = user.provider === "credentials";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box pos="relative">
|
<Box pos="relative">
|
||||||
<Menu
|
<Menu opened={opened} keepMounted onChange={toggle} position="bottom-start" withArrow>
|
||||||
opened={opened}
|
|
||||||
keepMounted
|
|
||||||
onChange={isCredentialsUser ? toggle : undefined}
|
|
||||||
position="bottom-start"
|
|
||||||
withArrow
|
|
||||||
>
|
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<UnstyledButton
|
<UnstyledButton onClick={toggle}>
|
||||||
component={isCredentialsUser ? undefined : "div"}
|
|
||||||
style={{ cursor: !isCredentialsUser ? "default" : undefined }}
|
|
||||||
onClick={isCredentialsUser ? toggle : undefined}
|
|
||||||
>
|
|
||||||
<UserAvatar user={user} size={200} />
|
<UserAvatar user={user} size={200} />
|
||||||
{isCredentialsUser && (
|
<Button
|
||||||
<Button
|
component="div"
|
||||||
component="div"
|
pos="absolute"
|
||||||
pos="absolute"
|
bottom={0}
|
||||||
bottom={0}
|
left={0}
|
||||||
left={0}
|
size="compact-md"
|
||||||
size="compact-md"
|
fw="normal"
|
||||||
fw="normal"
|
variant="default"
|
||||||
variant="default"
|
leftSection={<IconPencil size={18} stroke={1.5} />}
|
||||||
leftSection={<IconPencil size={18} stroke={1.5} />}
|
>
|
||||||
>
|
{t("common.action.edit")}
|
||||||
{t("common.action.edit")}
|
</Button>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
|||||||
import { useModalAction } from "@homarr/modals";
|
import { useModalAction } from "@homarr/modals";
|
||||||
import { showSuccessNotification } from "@homarr/notifications";
|
import { showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useSettings } from "@homarr/settings";
|
import { useSettings } from "@homarr/settings";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
||||||
import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
|
import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
|
||||||
import { WidgetError } from "@homarr/widgets/errors";
|
import { WidgetError } from "@homarr/widgets/errors";
|
||||||
@@ -31,7 +31,7 @@ interface WidgetPreviewPageContentProps {
|
|||||||
|
|
||||||
export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPreviewPageContentProps) => {
|
export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPreviewPageContentProps) => {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const t = useScopedI18n("widgetPreview");
|
const t = useI18n();
|
||||||
const { openModal: openWidgetEditModal } = useModalAction(WidgetEditModal);
|
const { openModal: openWidgetEditModal } = useModalAction(WidgetEditModal);
|
||||||
const { openModal: openPreviewDimensionsModal } = useModalAction(PreviewDimensionsModal);
|
const { openModal: openPreviewDimensionsModal } = useModalAction(PreviewDimensionsModal);
|
||||||
const currentDefinition = useMemo(() => widgetImports[kind].definition, [kind]);
|
const currentDefinition = useMemo(() => widgetImports[kind].definition, [kind]);
|
||||||
@@ -55,20 +55,27 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleOpenEditWidgetModal = useCallback(() => {
|
const handleOpenEditWidgetModal = useCallback(() => {
|
||||||
openWidgetEditModal({
|
openWidgetEditModal(
|
||||||
kind,
|
{
|
||||||
value: state,
|
kind,
|
||||||
onSuccessfulEdit: (value) => {
|
value: state,
|
||||||
setState(value);
|
onSuccessfulEdit: (value) => {
|
||||||
|
setState(value);
|
||||||
|
},
|
||||||
|
integrationData: integrationData.filter(
|
||||||
|
(integration) =>
|
||||||
|
"supportedIntegrations" in currentDefinition &&
|
||||||
|
(currentDefinition.supportedIntegrations as string[]).some((kind) => kind === integration.kind),
|
||||||
|
),
|
||||||
|
integrationSupport: "supportedIntegrations" in currentDefinition,
|
||||||
|
settings,
|
||||||
},
|
},
|
||||||
integrationData: integrationData.filter(
|
{
|
||||||
(integration) =>
|
title(t) {
|
||||||
"supportedIntegrations" in currentDefinition &&
|
return `${t("item.edit.title")} - ${t(`widget.${kind}.name`)}`;
|
||||||
(currentDefinition.supportedIntegrations as string[]).some((kind) => kind === integration.kind),
|
},
|
||||||
),
|
},
|
||||||
integrationSupport: "supportedIntegrations" in currentDefinition,
|
);
|
||||||
settings,
|
|
||||||
});
|
|
||||||
}, [currentDefinition, integrationData, kind, openWidgetEditModal, settings, state]);
|
}, [currentDefinition, integrationData, kind, openWidgetEditModal, settings, state]);
|
||||||
|
|
||||||
const Comp = loadWidgetDynamic(kind);
|
const Comp = loadWidgetDynamic(kind);
|
||||||
@@ -76,7 +83,7 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
|
|||||||
const toggleEditMode = useCallback(() => {
|
const toggleEditMode = useCallback(() => {
|
||||||
setEditMode((editMode) => !editMode);
|
setEditMode((editMode) => !editMode);
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
message: editMode ? t("toggle.disabled") : t("toggle.enabled"),
|
message: editMode ? t("widgetPreview.toggle.disabled") : t("widgetPreview.toggle.enabled"),
|
||||||
});
|
});
|
||||||
}, [editMode, t]);
|
}, [editMode, t]);
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export class DynamicSectionMockBuilder {
|
|||||||
options: {
|
options: {
|
||||||
title: "",
|
title: "",
|
||||||
borderColor: "",
|
borderColor: "",
|
||||||
|
customCssClasses: [],
|
||||||
},
|
},
|
||||||
layouts: [],
|
layouts: [],
|
||||||
...section,
|
...section,
|
||||||
|
|||||||
@@ -49,36 +49,43 @@ export const BoardItemMenu = ({
|
|||||||
if (!isEditMode || isPending) return null;
|
if (!isEditMode || isPending) return null;
|
||||||
|
|
||||||
const openEditModal = () => {
|
const openEditModal = () => {
|
||||||
openModal({
|
openModal(
|
||||||
kind: item.kind,
|
{
|
||||||
value: {
|
kind: item.kind,
|
||||||
advancedOptions: item.advancedOptions,
|
value: {
|
||||||
options: item.options,
|
advancedOptions: item.advancedOptions,
|
||||||
integrationIds: item.integrationIds,
|
options: item.options,
|
||||||
|
integrationIds: item.integrationIds,
|
||||||
|
},
|
||||||
|
onSuccessfulEdit: ({ options, integrationIds, advancedOptions }) => {
|
||||||
|
updateItemOptions({
|
||||||
|
itemId: item.id,
|
||||||
|
newOptions: options,
|
||||||
|
});
|
||||||
|
updateItemAdvancedOptions({
|
||||||
|
itemId: item.id,
|
||||||
|
newAdvancedOptions: advancedOptions,
|
||||||
|
});
|
||||||
|
updateItemIntegrations({
|
||||||
|
itemId: item.id,
|
||||||
|
newIntegrations: integrationIds,
|
||||||
|
});
|
||||||
|
refResetErrorBoundaryOnNextRender.current = true;
|
||||||
|
},
|
||||||
|
integrationData: (integrationData ?? []).filter(
|
||||||
|
(integration) =>
|
||||||
|
"supportedIntegrations" in currentDefinition &&
|
||||||
|
(currentDefinition.supportedIntegrations as string[]).some((kind) => kind === integration.kind),
|
||||||
|
),
|
||||||
|
integrationSupport: "supportedIntegrations" in currentDefinition,
|
||||||
|
settings,
|
||||||
},
|
},
|
||||||
onSuccessfulEdit: ({ options, integrationIds, advancedOptions }) => {
|
{
|
||||||
updateItemOptions({
|
title(t) {
|
||||||
itemId: item.id,
|
return `${t("item.edit.title")} - ${t(`widget.${item.kind}.name`)}`;
|
||||||
newOptions: options,
|
},
|
||||||
});
|
|
||||||
updateItemAdvancedOptions({
|
|
||||||
itemId: item.id,
|
|
||||||
newAdvancedOptions: advancedOptions,
|
|
||||||
});
|
|
||||||
updateItemIntegrations({
|
|
||||||
itemId: item.id,
|
|
||||||
newIntegrations: integrationIds,
|
|
||||||
});
|
|
||||||
refResetErrorBoundaryOnNextRender.current = true;
|
|
||||||
},
|
},
|
||||||
integrationData: (integrationData ?? []).filter(
|
);
|
||||||
(integration) =>
|
|
||||||
"supportedIntegrations" in currentDefinition &&
|
|
||||||
(currentDefinition.supportedIntegrations as string[]).some((kind) => kind === integration.kind),
|
|
||||||
),
|
|
||||||
integrationSupport: "supportedIntegrations" in currentDefinition,
|
|
||||||
settings,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openRemoveModal = () => {
|
const openRemoveModal = () => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Badge, Box, Card } from "@mantine/core";
|
import { Badge, Box, Card } from "@mantine/core";
|
||||||
|
import combineClasses from "clsx";
|
||||||
|
|
||||||
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
|
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ export const BoardDynamicSection = ({ section }: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
className={classes.itemCard}
|
className={combineClasses(classes.itemCard, section.options.customCssClasses.join(" "))}
|
||||||
w="100%"
|
w="100%"
|
||||||
h="100%"
|
h="100%"
|
||||||
withBorder
|
withBorder
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const addDynamicSectionCallback = () => (board: Board) => {
|
|||||||
options: {
|
options: {
|
||||||
title: "",
|
title: "",
|
||||||
borderColor: "",
|
borderColor: "",
|
||||||
|
customCssClasses: [],
|
||||||
},
|
},
|
||||||
layouts: createDynamicSectionLayouts(board, firstSection),
|
layouts: createDynamicSectionLayouts(board, firstSection),
|
||||||
} satisfies DynamicSection;
|
} satisfies DynamicSection;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { z } from "zod/v4";
|
|||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import { TextMultiSelect } from "@homarr/ui";
|
||||||
import { dynamicSectionOptionsSchema } from "@homarr/validation/shared";
|
import { dynamicSectionOptionsSchema } from "@homarr/validation/shared";
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
@@ -31,6 +32,10 @@ export const DynamicSectionEditModal = createModal<ModalProps>(({ actions, inner
|
|||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<TextInput label={t("section.dynamic.option.title.label")} {...form.getInputProps("title")} />
|
<TextInput label={t("section.dynamic.option.title.label")} {...form.getInputProps("title")} />
|
||||||
|
<TextMultiSelect
|
||||||
|
label={t("section.dynamic.option.customCssClasses.label")}
|
||||||
|
{...form.getInputProps("customCssClasses")}
|
||||||
|
/>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
label={t("section.dynamic.option.borderColor.label")}
|
label={t("section.dynamic.option.borderColor.label")}
|
||||||
format="hex"
|
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 type { SupportedLanguage } from "@homarr/translation";
|
||||||
import { localeConfigurations, supportedLanguages } from "@homarr/translation";
|
import { localeConfigurations, supportedLanguages } from "@homarr/translation";
|
||||||
|
import { LanguageIcon } from "@homarr/ui";
|
||||||
import classes from "./language-combobox.module.css";
|
|
||||||
|
|
||||||
import "flag-icons/css/flag-icons.min.css";
|
import "flag-icons/css/flag-icons.min.css";
|
||||||
|
|
||||||
@@ -84,7 +83,7 @@ const OptionItem = ({
|
|||||||
return (
|
return (
|
||||||
<Group wrap="nowrap" justify="space-between">
|
<Group wrap="nowrap" justify="space-between">
|
||||||
<Group wrap="nowrap">
|
<Group wrap="nowrap">
|
||||||
<span className={`fi fi-${localeConfigurations[localeKey].flagIcon} ${classes.flagIcon}`}></span>
|
<LanguageIcon icon={localeConfigurations[localeKey].icon} />
|
||||||
<Group wrap="nowrap" gap="xs">
|
<Group wrap="nowrap" gap="xs">
|
||||||
<Text>{localeConfigurations[localeKey].name}</Text>
|
<Text>{localeConfigurations[localeKey].name}</Text>
|
||||||
<Text size="xs" c="dimmed" inherit>
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -17,7 +17,7 @@ interface Props {
|
|||||||
|
|
||||||
export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => {
|
export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => {
|
||||||
return (
|
return (
|
||||||
<AppShellHeader>
|
<AppShellHeader maw="100vw" style={{ overflowX: "hidden" }}>
|
||||||
<Group h="100%" gap="xl" px="md" justify="apart" wrap="nowrap">
|
<Group h="100%" gap="xl" px="md" justify="apart" wrap="nowrap">
|
||||||
<Group h="100%" align="center" style={{ flex: 1 }} wrap="nowrap">
|
<Group h="100%" align="center" style={{ flex: 1 }} wrap="nowrap">
|
||||||
{hasNavigation && <ClientBurger />}
|
{hasNavigation && <ClientBurger />}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { signOut, useSession } from "@homarr/auth/client";
|
import { signOut, useSession } from "@homarr/auth/client";
|
||||||
|
import { hotkeys } from "@homarr/definitions";
|
||||||
import { createModal, useModalAction } from "@homarr/modals";
|
import { createModal, useModalAction } from "@homarr/modals";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ interface UserAvatarMenuProps {
|
|||||||
export const UserAvatarMenu = ({ children, availableUpdatesPromise }: UserAvatarMenuProps) => {
|
export const UserAvatarMenu = ({ children, availableUpdatesPromise }: UserAvatarMenuProps) => {
|
||||||
const t = useScopedI18n("common.userAvatar.menu");
|
const t = useScopedI18n("common.userAvatar.menu");
|
||||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||||
useHotkeys([["mod+J", toggleColorScheme]]);
|
useHotkeys([[hotkeys.toggleColorScheme, toggleColorScheme]]);
|
||||||
|
|
||||||
const ColorSchemeIcon = colorScheme === "dark" ? IconSun : IconMoon;
|
const ColorSchemeIcon = colorScheme === "dark" ? IconSun : IconMoon;
|
||||||
|
|
||||||
|
|||||||
+11
-9
@@ -21,6 +21,7 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/analytics": "workspace:^0.1.0",
|
"@homarr/analytics": "workspace:^0.1.0",
|
||||||
|
"@homarr/auth": "workspace:^0.1.0",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/cron-job-api": "workspace:^0.1.0",
|
"@homarr/cron-job-api": "workspace:^0.1.0",
|
||||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||||
@@ -32,25 +33,26 @@
|
|||||||
"@homarr/log": "workspace:^",
|
"@homarr/log": "workspace:^",
|
||||||
"@homarr/ping": "workspace:^0.1.0",
|
"@homarr/ping": "workspace:^0.1.0",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
|
"@homarr/request-handler": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@homarr/widgets": "workspace:^0.1.0",
|
"@homarr/widgets": "workspace:^0.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.18",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.3",
|
||||||
"fastify": "^5.5.0",
|
"fastify": "^5.6.1",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.3",
|
||||||
"undici": "7.14.0"
|
"undici": "7.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node": "^22.17.2",
|
"@types/node": "^24.9.1",
|
||||||
"dotenv-cli": "^10.0.0",
|
"dotenv-cli": "^10.0.0",
|
||||||
"esbuild": "^0.25.9",
|
"esbuild": "^0.25.11",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.38.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"tsx": "4.20.4",
|
"tsx": "4.20.4",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,6 @@ export class JobManager implements IJobManager {
|
|||||||
logger.info(`Updating cron job interval name="${name}" expression="${cron}"`);
|
logger.info(`Updating cron job interval name="${name}" expression="${cron}"`);
|
||||||
const job = this.jobGroup.getJobRegistry().get(name);
|
const job = this.jobGroup.getJobRegistry().get(name);
|
||||||
if (!job) throw new Error(`Job ${name} not found`);
|
if (!job) throw new Error(`Job ${name} not found`);
|
||||||
if (job.cronExpression === "never") throw new Error(`Job ${name} cannot be updated as it is set to "never"`);
|
|
||||||
if (!validateCron(cron)) {
|
if (!validateCron(cron)) {
|
||||||
throw new Error(`Invalid cron expression: ${cron}`);
|
throw new Error(`Invalid cron expression: ${cron}`);
|
||||||
}
|
}
|
||||||
@@ -45,7 +44,6 @@ export class JobManager implements IJobManager {
|
|||||||
logger.info(`Disabling cron job name="${name}"`);
|
logger.info(`Disabling cron job name="${name}"`);
|
||||||
const job = this.jobGroup.getJobRegistry().get(name);
|
const job = this.jobGroup.getJobRegistry().get(name);
|
||||||
if (!job) throw new Error(`Job ${name} not found`);
|
if (!job) throw new Error(`Job ${name} not found`);
|
||||||
if (job.cronExpression === "never") throw new Error(`Job ${name} cannot be disabled as it is set to "never"`);
|
|
||||||
|
|
||||||
await this.updateConfigurationAsync(name, { isEnabled: false });
|
await this.updateConfigurationAsync(name, { isEnabled: false });
|
||||||
await this.jobGroup.stopAsync(name);
|
await this.jobGroup.stopAsync(name);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// This import has to be the first import in the file so that the agent is overridden before any other modules are imported.
|
// This import has to be the first import in the file so that the agent is overridden before any other modules are imported.
|
||||||
import "./undici-log-agent-override";
|
import "./overrides";
|
||||||
|
|
||||||
import type { FastifyTRPCPluginOptions } from "@trpc/server/adapters/fastify";
|
import type { FastifyTRPCPluginOptions } from "@trpc/server/adapters/fastify";
|
||||||
import { fastifyTRPCPlugin } from "@trpc/server/adapters/fastify";
|
import { fastifyTRPCPlugin } from "@trpc/server/adapters/fastify";
|
||||||
@@ -13,6 +13,7 @@ import { db } from "@homarr/db";
|
|||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
import { JobManager } from "./job-manager";
|
import { JobManager } from "./job-manager";
|
||||||
|
import { onStartAsync } from "./on-start";
|
||||||
|
|
||||||
const server = fastify({
|
const server = fastify({
|
||||||
maxParamLength: 5000,
|
maxParamLength: 5000,
|
||||||
@@ -32,6 +33,7 @@ server.register(fastifyTRPCPlugin, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
|
await onStartAsync();
|
||||||
await jobGroup.initializeAsync();
|
await jobGroup.initializeAsync();
|
||||||
await jobGroup.startAllAsync();
|
await jobGroup.startAllAsync();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { invalidateUpdateCheckerCacheAsync } from "./invalidate-update-checker-cache";
|
||||||
|
import { cleanupSessionsAsync } from "./session-cleanup";
|
||||||
|
|
||||||
|
export async function onStartAsync() {
|
||||||
|
await cleanupSessionsAsync();
|
||||||
|
await invalidateUpdateCheckerCacheAsync();
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { logger } from "@homarr/log";
|
||||||
|
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
|
||||||
|
|
||||||
|
const localLogger = logger.child({ module: "invalidateUpdateCheckerCache" });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates the update checker cache on startup to ensure fresh data.
|
||||||
|
* It is important as we want to avoid showing pending updates after the update to latest version.
|
||||||
|
*/
|
||||||
|
export async function invalidateUpdateCheckerCacheAsync() {
|
||||||
|
try {
|
||||||
|
const handler = updateCheckerRequestHandler.handler({});
|
||||||
|
await handler.invalidateAsync();
|
||||||
|
localLogger.debug("Update checker cache invalidated");
|
||||||
|
} catch (error) {
|
||||||
|
localLogger.error(new Error("Failed to invalidate update checker cache", { cause: error }));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { env } from "@homarr/auth/env";
|
||||||
|
import { db, eq, inArray } from "@homarr/db";
|
||||||
|
import { sessions, users } from "@homarr/db/schema";
|
||||||
|
import { supportedAuthProviders } from "@homarr/definitions";
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
|
const localLogger = logger.child({ module: "sessionCleanup" });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes sessions for users that have inactive auth providers.
|
||||||
|
* Sessions from other providers are deleted so they can no longer be used.
|
||||||
|
*/
|
||||||
|
export async function cleanupSessionsAsync() {
|
||||||
|
try {
|
||||||
|
const currentAuthProviders = env.AUTH_PROVIDERS;
|
||||||
|
|
||||||
|
const inactiveAuthProviders = supportedAuthProviders.filter((provider) => !currentAuthProviders.includes(provider));
|
||||||
|
const subQuery = db
|
||||||
|
.select({ id: users.id })
|
||||||
|
.from(users)
|
||||||
|
.where(inArray(users.provider, inactiveAuthProviders))
|
||||||
|
.as("sq");
|
||||||
|
const sessionsWithInactiveProviders = await db
|
||||||
|
.select({ userId: sessions.userId })
|
||||||
|
.from(sessions)
|
||||||
|
.rightJoin(subQuery, eq(sessions.userId, subQuery.id));
|
||||||
|
|
||||||
|
const userIds = sessionsWithInactiveProviders.map(({ userId }) => userId).filter((value) => value !== null);
|
||||||
|
await db.delete(sessions).where(inArray(sessions.userId, userIds));
|
||||||
|
|
||||||
|
if (sessionsWithInactiveProviders.length > 0) {
|
||||||
|
localLogger.info(`Deleted sessions for inactive providers count=${userIds.length}`);
|
||||||
|
} else {
|
||||||
|
localLogger.debug("No sessions to delete");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
localLogger.error(new Error("Failed to clean up sessions", { cause: error }));
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -25,7 +25,7 @@
|
|||||||
"@homarr/log": "workspace:^",
|
"@homarr/log": "workspace:^",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.3",
|
||||||
"tsx": "4.20.4",
|
"tsx": "4.20.4",
|
||||||
"ws": "^8.18.3"
|
"ws": "^8.18.3"
|
||||||
},
|
},
|
||||||
@@ -34,9 +34,9 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"esbuild": "^0.25.9",
|
"esbuild": "^0.25.11",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.38.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+28
-12
@@ -30,6 +30,8 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- 🖌️ Highly customizable with an extensive drag and drop grid system
|
- 🖌️ Highly customizable with an extensive drag and drop grid system
|
||||||
@@ -132,6 +134,13 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
|
<a href="https://homarr.dev/docs/integrations/ical" target="_blank" rel="noreferrer noopener">
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/ical.svg" alt="iCal" width="90" height="90" />
|
||||||
|
<br/>
|
||||||
|
<p align="center">iCal</p>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/jellyfin" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/jellyfin" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg" alt="Jellyfin" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg" alt="Jellyfin" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -144,15 +153,15 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">Jellyseerr</p>
|
<p align="center">Jellyseerr</p>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
<td align="center">
|
<tr><td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/lidarr" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/lidarr" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/lidarr.svg" alt="Lidarr" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/lidarr.svg" alt="Lidarr" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">Lidarr</p>
|
<p align="center">Lidarr</p>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr><td align="center">
|
<td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/linux-server-io" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/linux-server-io" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/linuxserver-io.svg" alt="LinuxServer.io" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/linuxserver-io.svg" alt="LinuxServer.io" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -193,15 +202,15 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">OpenMediaVault</p>
|
<p align="center">OpenMediaVault</p>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
<td align="center">
|
<tr><td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/opnsense" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/opnsense" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/opnsense.svg" alt="OPNsense" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/opnsense.svg" alt="OPNsense" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">OPNsense</p>
|
<p align="center">OPNsense</p>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr><td align="center">
|
<td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/overseerr" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/overseerr" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/overseerr.svg" alt="Overseerr" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/overseerr.svg" alt="Overseerr" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -242,15 +251,15 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">qBittorrent</p>
|
<p align="center">qBittorrent</p>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
<td align="center">
|
<tr><td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/quay" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/quay" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/quay.png" alt="Quay" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/quay.png" alt="Quay" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">Quay</p>
|
<p align="center">Quay</p>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr><td align="center">
|
<td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/radarr" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/radarr" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/radarr.svg" alt="Radarr" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/radarr.svg" alt="Radarr" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -291,6 +300,13 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">Transmission</p>
|
<p align="center">Transmission</p>
|
||||||
</a>
|
</a>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td align="center">
|
||||||
|
<a href="https://homarr.dev/docs/integrations/truenas" target="_blank" rel="noreferrer noopener">
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/truenas.svg" alt="TrueNAS" width="90" height="90" />
|
||||||
|
<br/>
|
||||||
|
<p align="center">TrueNAS</p>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/unifi-controller" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/unifi-controller" target="_blank" rel="noreferrer noopener">
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 313 KiB |
+30
-29
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "1.35.1",
|
"version": "1.43.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env CI=true turbo build",
|
"build": "cross-env CI=true turbo build",
|
||||||
@@ -39,28 +39,28 @@
|
|||||||
"@semantic-release/changelog": "^6.0.3",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/commit-analyzer": "^13.0.1",
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/git": "^10.0.1",
|
||||||
"@semantic-release/github": "^11.0.4",
|
"@semantic-release/github": "^12.0.0",
|
||||||
"@semantic-release/npm": "^12.0.2",
|
"@semantic-release/npm": "^13.1.1",
|
||||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
"@semantic-release/release-notes-generator": "^14.1.0",
|
||||||
"@testcontainers/redis": "^11.5.1",
|
"@testcontainers/redis": "^11.7.2",
|
||||||
"@turbo/gen": "^2.5.6",
|
"@turbo/gen": "^2.5.8",
|
||||||
"@vitejs/plugin-react": "^5.0.1",
|
"@vitejs/plugin-react": "^5.1.0",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
||||||
"cross-env": "^10.0.0",
|
"cross-env": "^10.1.0",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^27.0.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"semantic-release": "^24.2.7",
|
"semantic-release": "^25.0.1",
|
||||||
"testcontainers": "^11.5.1",
|
"testcontainers": "^11.7.2",
|
||||||
"turbo": "^2.5.6",
|
"turbo": "^2.5.8",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.3",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.15.0",
|
"packageManager": "pnpm@10.19.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.18.0"
|
"node": ">=22.21.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
@@ -75,24 +75,25 @@
|
|||||||
"tree-sitter-json"
|
"tree-sitter-json"
|
||||||
],
|
],
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@babel/helpers@<7.26.10": ">=7.26.10",
|
"@babel/helpers@<7.26.10": ">=7.28.4",
|
||||||
"@babel/runtime@<7.26.10": ">=7.26.10",
|
"@babel/runtime@<7.26.10": ">=7.28.4",
|
||||||
"axios@>=1.0.0 <1.8.2": ">=1.8.2",
|
"axios@>=1.0.0 <1.8.2": ">=1.12.2",
|
||||||
"brace-expansion@>=2.0.0 <=2.0.1": ">=2.0.2",
|
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
|
||||||
"brace-expansion@>=1.0.0 <=1.1.11": ">=1.1.12",
|
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
|
||||||
"esbuild@<=0.24.2": ">=0.25.0",
|
"esbuild@<=0.24.2": ">=0.25.11",
|
||||||
"form-data@>=4.0.0 <4.0.4": ">=4.0.4",
|
"form-data@>=4.0.0 <4.0.4": ">=4.0.4",
|
||||||
"hono@<4.6.5": ">=4.6.5",
|
"hono@<4.6.5": ">=4.10.3",
|
||||||
"linkifyjs@<4.3.2": ">=4.3.2",
|
"linkifyjs@<4.3.2": ">=4.3.2",
|
||||||
"nanoid@>=4.0.0 <5.0.9": ">=5.0.9",
|
"nanoid@>=4.0.0 <5.0.9": ">=5.1.6",
|
||||||
"prismjs@<1.30.0": ">=1.30.0",
|
"prismjs@<1.30.0": ">=1.30.0",
|
||||||
"proxmox-api>undici": "7.14.0",
|
"proxmox-api>undici": "7.16.0",
|
||||||
"rollup@>=4.0.0 <4.22.4": ">=4.22.4",
|
"react-is": "^19.2.0",
|
||||||
|
"rollup@>=4.0.0 <4.22.4": ">=4.52.5",
|
||||||
"sha.js@<=2.4.11": ">=2.4.12",
|
"sha.js@<=2.4.11": ">=2.4.12",
|
||||||
"tar-fs@>=3.0.0 <3.0.9": ">=3.0.9",
|
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
|
||||||
"tar-fs@>=2.0.0 <2.1.3": ">=2.1.3",
|
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
|
||||||
"tmp@<=0.2.3": ">=0.2.4",
|
"tmp@<=0.2.3": ">=0.2.5",
|
||||||
"vite@>=5.0.0 <=5.4.18": ">=5.4.19"
|
"vite@>=5.0.0 <=5.4.18": ">=7.1.12"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@types/node-unifi": "patches/@types__node-unifi.patch",
|
"@types/node-unifi": "patches/@types__node-unifi.patch",
|
||||||
|
|||||||
@@ -26,13 +26,13 @@
|
|||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@umami/node": "^0.4.0",
|
"@umami/node": "^0.4.0",
|
||||||
"superjson": "2.2.2"
|
"superjson": "2.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.38.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+15
-14
@@ -40,27 +40,28 @@
|
|||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"@homarr/request-handler": "workspace:^0.1.0",
|
"@homarr/request-handler": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@kubernetes/client-node": "^1.3.0",
|
"@kubernetes/client-node": "^1.4.0",
|
||||||
"@tanstack/react-query": "^5.85.5",
|
"@tanstack/react-query": "^5.90.5",
|
||||||
"@trpc/client": "^11.4.4",
|
"@trpc/client": "^11.6.0",
|
||||||
"@trpc/react-query": "^11.4.4",
|
"@trpc/react-query": "^11.6.0",
|
||||||
"@trpc/server": "^11.4.4",
|
"@trpc/server": "^11.6.0",
|
||||||
"@trpc/tanstack-react-query": "^11.4.4",
|
"@trpc/tanstack-react-query": "^11.6.0",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"next": "15.5.0",
|
"next": "15.5.6",
|
||||||
"react": "19.1.1",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.2.0",
|
||||||
"superjson": "2.2.2",
|
"superjson": "2.2.3",
|
||||||
"trpc-to-openapi": "^3.0.1",
|
"trpc-to-openapi": "^3.1.0",
|
||||||
"zod": "^4.0.17"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.38.0",
|
||||||
"prettier": "^3.6.2",
|
"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({
|
const integration = await ctx.db.query.integrations.findFirst({
|
||||||
where: and(eq(integrations.id, input.integrationId), inArray(integrations.kind, kinds)),
|
where: and(eq(integrations.id, input.integrationId), inArray(integrations.kind, kinds)),
|
||||||
with: {
|
with: {
|
||||||
|
app: true,
|
||||||
secrets: true,
|
secrets: true,
|
||||||
groupPermissions: true,
|
groupPermissions: true,
|
||||||
userPermissions: true,
|
userPermissions: true,
|
||||||
@@ -65,6 +66,7 @@ export const createOneIntegrationMiddleware = <TKind extends IntegrationKind>(
|
|||||||
ctx: {
|
ctx: {
|
||||||
integration: {
|
integration: {
|
||||||
...rest,
|
...rest,
|
||||||
|
externalUrl: rest.app?.href ?? null,
|
||||||
kind: kind as TKind,
|
kind: kind as TKind,
|
||||||
decryptedSecrets: secrets.map((secret) => ({
|
decryptedSecrets: secrets.map((secret) => ({
|
||||||
...secret,
|
...secret,
|
||||||
@@ -96,6 +98,7 @@ export const createManyIntegrationMiddleware = <TKind extends IntegrationKind>(
|
|||||||
? await ctx.db.query.integrations.findMany({
|
? await ctx.db.query.integrations.findMany({
|
||||||
where: and(inArray(integrations.id, input.integrationIds), inArray(integrations.kind, kinds)),
|
where: and(inArray(integrations.id, input.integrationIds), inArray(integrations.kind, kinds)),
|
||||||
with: {
|
with: {
|
||||||
|
app: true,
|
||||||
secrets: true,
|
secrets: true,
|
||||||
items: {
|
items: {
|
||||||
with: {
|
with: {
|
||||||
@@ -125,6 +128,7 @@ export const createManyIntegrationMiddleware = <TKind extends IntegrationKind>(
|
|||||||
integrations: dbIntegrations.map(
|
integrations: dbIntegrations.map(
|
||||||
({ secrets, kind, items: _ignore1, groupPermissions: _ignore2, userPermissions: _ignore3, ...rest }) => ({
|
({ secrets, kind, items: _ignore1, groupPermissions: _ignore2, userPermissions: _ignore3, ...rest }) => ({
|
||||||
...rest,
|
...rest,
|
||||||
|
externalUrl: rest.app?.href ?? null,
|
||||||
kind: kind as TKind,
|
kind: kind as TKind,
|
||||||
decryptedSecrets: secrets.map((secret) => ({
|
decryptedSecrets: secrets.map((secret) => ({
|
||||||
...secret,
|
...secret,
|
||||||
|
|||||||
@@ -118,20 +118,22 @@ export const appRouter = createTRPCRouter({
|
|||||||
create: permissionRequiredProcedure
|
create: permissionRequiredProcedure
|
||||||
.requiresPermission("app-create")
|
.requiresPermission("app-create")
|
||||||
.input(appManageSchema)
|
.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 } })
|
.meta({ openapi: { method: "POST", path: "/api/apps", tags: ["apps"], protect: true } })
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const id = createId();
|
const id = createId();
|
||||||
await ctx.db.insert(apps).values({
|
const insertValues = {
|
||||||
id,
|
id,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
description: input.description,
|
description: input.description,
|
||||||
iconUrl: input.iconUrl,
|
iconUrl: input.iconUrl,
|
||||||
href: input.href,
|
href: input.href,
|
||||||
pingUrl: input.pingUrl === "" ? null : input.pingUrl,
|
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
|
createMany: permissionRequiredProcedure
|
||||||
.requiresPermission("app-create")
|
.requiresPermission("app-create")
|
||||||
|
|||||||
+136
-20
@@ -1,31 +1,147 @@
|
|||||||
import type { AnySQLiteTable } from "drizzle-orm/sqlite-core";
|
|
||||||
|
|
||||||
import { isProviderEnabled } from "@homarr/auth/server";
|
import { isProviderEnabled } from "@homarr/auth/server";
|
||||||
import type { Database } from "@homarr/db";
|
import { db, eq, inArray, or } from "@homarr/db";
|
||||||
import { apps, boards, groups, integrations, invites, users } from "@homarr/db/schema";
|
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";
|
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({
|
export const homeRouter = createTRPCRouter({
|
||||||
getStats: publicProcedure.query(async ({ ctx }) => {
|
getStats: publicProcedure.query(async ({ ctx }) => {
|
||||||
const isAdmin = ctx.session?.user.permissions.includes("admin") ?? false;
|
const isAdmin = ctx.session?.user.permissions.includes("admin") ?? false;
|
||||||
const isCredentialsEnabled = isProviderEnabled("credentials");
|
const isCredentialsEnabled = isProviderEnabled("credentials");
|
||||||
|
|
||||||
return {
|
const statistics: HomeStatistic[] = [];
|
||||||
countBoards: await getCountForTableAsync(ctx.db, boards, true),
|
|
||||||
countUsers: await getCountForTableAsync(ctx.db, users, isAdmin),
|
const boardIds: string[] = [];
|
||||||
countGroups: await getCountForTableAsync(ctx.db, groups, true),
|
if (ctx.session?.user && !ctx.session.user.permissions.includes("board-view-all")) {
|
||||||
countInvites: await getCountForTableAsync(ctx.db, invites, isAdmin),
|
const permissionsOfCurrentUserWhenPresent = await ctx.db.query.boardUserPermissions.findMany({
|
||||||
countIntegrations: await getCountForTableAsync(ctx.db, integrations, isCredentialsEnabled && isAdmin),
|
where: eq(boardUserPermissions.userId, ctx.session.user.id),
|
||||||
countApps: await getCountForTableAsync(ctx.db, apps, true),
|
});
|
||||||
};
|
|
||||||
|
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 { createId, objectEntries } from "@homarr/common";
|
||||||
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
||||||
import type { Database } from "@homarr/db";
|
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 {
|
import {
|
||||||
|
apps,
|
||||||
groupMembers,
|
groupMembers,
|
||||||
groupPermissions,
|
groupPermissions,
|
||||||
integrationGroupPermissions,
|
integrationGroupPermissions,
|
||||||
@@ -212,6 +213,14 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
updatedAt: true,
|
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,
|
value: integrationSecretKindObject[secret.kind].isPublic ? decryptSecret(secret.value) : null,
|
||||||
updatedAt: secret.updatedAt,
|
updatedAt: secret.updatedAt,
|
||||||
})),
|
})),
|
||||||
|
app: integration.app,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
create: permissionRequiredProcedure
|
create: permissionRequiredProcedure
|
||||||
@@ -245,6 +255,13 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
url: input.url,
|
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({
|
const result = await testConnectionAsync({
|
||||||
id: "new",
|
id: "new",
|
||||||
name: input.name,
|
name: input.name,
|
||||||
@@ -267,12 +284,15 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appId = await createAppIfNecessaryAsync(ctx.db, input.app);
|
||||||
|
|
||||||
const integrationId = createId();
|
const integrationId = createId();
|
||||||
await ctx.db.insert(integrations).values({
|
await ctx.db.insert(integrations).values({
|
||||||
id: integrationId,
|
id: integrationId,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
url: input.url,
|
url: input.url,
|
||||||
kind: input.kind,
|
kind: input.kind,
|
||||||
|
appId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (input.secrets.length >= 1) {
|
if (input.secrets.length >= 1) {
|
||||||
@@ -358,6 +378,7 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
.set({
|
.set({
|
||||||
name: input.name,
|
name: input.name,
|
||||||
url: input.url,
|
url: input.url,
|
||||||
|
appId: input.appId,
|
||||||
})
|
})
|
||||||
.where(eq(integrations.id, input.id));
|
.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", {
|
logger.info("Updated integration", {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
@@ -637,3 +673,30 @@ const addSecretAsync = async (db: Database, input: AddSecretInput) => {
|
|||||||
integrationId: input.integrationId,
|
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 { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
type FormIntegration = Integration & {
|
type FormIntegration = Omit<Integration, "appId"> & {
|
||||||
secrets: {
|
secrets: {
|
||||||
kind: IntegrationSecretKind;
|
kind: IntegrationSecretKind;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
@@ -25,16 +25,6 @@ export const testConnectionAsync = async (
|
|||||||
integrationUrl: integration.url,
|
integrationUrl: integration.url,
|
||||||
});
|
});
|
||||||
|
|
||||||
const formSecrets = integration.secrets
|
|
||||||
.filter((secret) => secret.value !== null)
|
|
||||||
.map((secret) => ({
|
|
||||||
...secret,
|
|
||||||
// We ensured above that the value is not null
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
value: secret.value!,
|
|
||||||
source: "form" as const,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const decryptedDbSecrets = dbSecrets
|
const decryptedDbSecrets = dbSecrets
|
||||||
.map((secret) => {
|
.map((secret) => {
|
||||||
try {
|
try {
|
||||||
@@ -55,6 +45,15 @@ export const testConnectionAsync = async (
|
|||||||
})
|
})
|
||||||
.filter((secret) => secret !== null);
|
.filter((secret) => secret !== null);
|
||||||
|
|
||||||
|
const formSecrets = integration.secrets
|
||||||
|
.map((secret) => ({
|
||||||
|
...secret,
|
||||||
|
// If the value is not defined in the form (because we only changed other values) we use the existing value from the db if it exists
|
||||||
|
value: secret.value ?? decryptedDbSecrets.find((dbSecret) => dbSecret.kind === secret.kind)?.value ?? null,
|
||||||
|
source: "form" as const,
|
||||||
|
}))
|
||||||
|
.filter((secret): secret is SourcedIntegrationSecret<"form"> => secret.value !== null);
|
||||||
|
|
||||||
const sourcedSecrets = [...formSecrets, ...decryptedDbSecrets];
|
const sourcedSecrets = [...formSecrets, ...decryptedDbSecrets];
|
||||||
const secretKinds = getSecretKindOption(integration.kind, sourcedSecrets);
|
const secretKinds = getSecretKindOption(integration.kind, sourcedSecrets);
|
||||||
|
|
||||||
@@ -76,6 +75,7 @@ export const testConnectionAsync = async (
|
|||||||
const integrationInstance = await createIntegrationAsync({
|
const integrationInstance = await createIntegrationAsync({
|
||||||
...baseIntegration,
|
...baseIntegration,
|
||||||
decryptedSecrets,
|
decryptedSecrets,
|
||||||
|
externalUrl: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await integrationInstance.testConnectionAsync();
|
const result = await integrationInstance.testConnectionAsync();
|
||||||
@@ -89,10 +89,10 @@ export const testConnectionAsync = async (
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SourcedIntegrationSecret {
|
interface SourcedIntegrationSecret<TSource extends string = "db" | "form"> {
|
||||||
kind: IntegrationSecretKind;
|
kind: IntegrationSecretKind;
|
||||||
value: string;
|
value: string;
|
||||||
source: "db" | "form";
|
source: TSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedIntegrationSecret[]) => {
|
const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedIntegrationSecret[]) => {
|
||||||
@@ -111,7 +111,9 @@ const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedInteg
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onlyFormSecretsKindOptions = matchingSecretKindOptions.filter((secretKinds) =>
|
const onlyFormSecretsKindOptions = matchingSecretKindOptions.filter((secretKinds) =>
|
||||||
sourcedSecrets.filter((secret) => secretKinds.includes(secret.kind)).every((secret) => secret.source === "form"),
|
secretKinds.every((secretKind) =>
|
||||||
|
sourcedSecrets.find((secret) => secret.kind === secretKind && secret.source === "form"),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (onlyFormSecretsKindOptions.length >= 1) {
|
if (onlyFormSecretsKindOptions.length >= 1) {
|
||||||
|
|||||||
@@ -184,12 +184,23 @@ function getProviders(versionInfo: VersionInfo, nodes: V1NodeList) {
|
|||||||
if (versionInfo.gitVersion.includes("aks")) providers.add("AKS");
|
if (versionInfo.gitVersion.includes("aks")) providers.add("AKS");
|
||||||
|
|
||||||
nodes.items.forEach((node) => {
|
nodes.items.forEach((node) => {
|
||||||
const nodeProviderLabel =
|
const labels = node.metadata?.labels ?? {};
|
||||||
node.metadata?.labels?.["node.kubernetes.io/instance-type"] ?? node.metadata?.labels?.provider ?? "";
|
const nodeProviderLabel = labels["node.kubernetes.io/instance-type"] ?? labels.provider ?? "";
|
||||||
|
|
||||||
if (nodeProviderLabel.includes("aws")) providers.add("EKS");
|
if (nodeProviderLabel.includes("aws")) providers.add("EKS");
|
||||||
if (nodeProviderLabel.includes("azure")) providers.add("AKS");
|
if (nodeProviderLabel.includes("azure")) providers.add("AKS");
|
||||||
if (nodeProviderLabel.includes("gce")) providers.add("GKE");
|
if (nodeProviderLabel.includes("gce")) providers.add("GKE");
|
||||||
if (nodeProviderLabel.includes("k3s")) providers.add("k3s");
|
if (nodeProviderLabel.includes("k3s")) providers.add("k3s");
|
||||||
|
|
||||||
|
const nodeInfo = node.status?.nodeInfo;
|
||||||
|
if (nodeInfo) {
|
||||||
|
const osImage = nodeInfo.osImage.toLowerCase();
|
||||||
|
const kernelVersion = nodeInfo.kernelVersion.toLowerCase();
|
||||||
|
|
||||||
|
if (osImage.includes("talos") || kernelVersion.includes("talos")) {
|
||||||
|
providers.add("Talos");
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(providers).join(", ");
|
return Array.from(providers).join(", ");
|
||||||
|
|||||||
@@ -55,34 +55,47 @@ export const mediaRouter = createTRPCRouter({
|
|||||||
.requiresPermission("media-upload")
|
.requiresPermission("media-upload")
|
||||||
.input(mediaUploadSchema)
|
.input(mediaUploadSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const content = Buffer.from(await input.file.arrayBuffer());
|
const files = await Promise.all(
|
||||||
const id = createId();
|
input.files.map(async (file) => ({
|
||||||
const media = {
|
id: createId(),
|
||||||
id,
|
meta: file,
|
||||||
creatorId: ctx.session.user.id,
|
content: Buffer.from(await file.arrayBuffer()),
|
||||||
content,
|
})),
|
||||||
size: input.file.size,
|
);
|
||||||
contentType: input.file.type,
|
const insertMedias = files.map(
|
||||||
name: input.file.name,
|
(file): InferInsertModel<typeof medias> => ({
|
||||||
} satisfies InferInsertModel<typeof medias>;
|
id: file.id,
|
||||||
await ctx.db.insert(medias).values(media);
|
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({
|
const localIconRepository = await ctx.db.query.iconRepositories.findFirst({
|
||||||
where: eq(iconRepositories.slug, LOCAL_ICON_REPOSITORY_SLUG),
|
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(
|
||||||
await ctx.db.insert(icons).values({
|
insertMedias.map((media) => {
|
||||||
id: createId(),
|
const icon = mapMediaToIcon(media);
|
||||||
checksum: icon.checksum,
|
|
||||||
name: icon.fileNameWithExtension,
|
|
||||||
url: icon.imageUrl,
|
|
||||||
iconRepositoryId: localIconRepository.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
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 }) => {
|
deleteMedia: protectedProcedure.input(byIdSchema).mutation(async ({ ctx, input }) => {
|
||||||
const dbMedia = await ctx.db.query.medias.findFirst({
|
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 type { Session } from "@homarr/auth";
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
import { encryptSecret } from "@homarr/common/server";
|
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 { createDb } from "@homarr/db/test";
|
||||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
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 () => {
|
test("without create integration access should throw permission error", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
@@ -273,6 +369,36 @@ describe("create should create a new integration", () => {
|
|||||||
// Assert
|
// Assert
|
||||||
await expect(actAsync()).rejects.toThrow("Permission denied");
|
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", () => {
|
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 lastWeek = new Date("2023-06-24T00:00:00Z");
|
||||||
|
const appId = createId();
|
||||||
const integrationId = createId();
|
const integrationId = createId();
|
||||||
const toInsert = {
|
const toInsert = {
|
||||||
id: integrationId,
|
id: integrationId,
|
||||||
@@ -293,6 +420,11 @@ describe("update should update an integration", () => {
|
|||||||
url: "http://hole.local",
|
url: "http://hole.local",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await db.insert(apps).values({
|
||||||
|
id: appId,
|
||||||
|
name: "Previous",
|
||||||
|
iconUrl: "logo.png",
|
||||||
|
});
|
||||||
await db.insert(integrations).values(toInsert);
|
await db.insert(integrations).values(toInsert);
|
||||||
|
|
||||||
const usernameToInsert = {
|
const usernameToInsert = {
|
||||||
@@ -320,6 +452,7 @@ describe("update should update an integration", () => {
|
|||||||
{ kind: "password" as const, value: null },
|
{ kind: "password" as const, value: null },
|
||||||
{ kind: "apiKey" as const, value: "1234567890" },
|
{ kind: "apiKey" as const, value: "1234567890" },
|
||||||
],
|
],
|
||||||
|
appId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const fakeNow = new Date("2023-07-01T00:00:00Z");
|
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!.name).toBe(input.name);
|
||||||
expect(dbIntegration!.kind).toBe(input.kind);
|
expect(dbIntegration!.kind).toBe(input.kind);
|
||||||
expect(dbIntegration!.url).toBe(input.url);
|
expect(dbIntegration!.url).toBe(input.url);
|
||||||
|
expect(dbIntegration!.appId).toBe(appId);
|
||||||
|
|
||||||
expect(dbSecrets.length).toBe(3);
|
expect(dbSecrets.length).toBe(3);
|
||||||
const username = expectToBeDefined(dbSecrets.find((secret) => secret.kind === "username"));
|
const username = expectToBeDefined(dbSecrets.find((secret) => secret.kind === "username"));
|
||||||
@@ -365,6 +499,7 @@ describe("update should update an integration", () => {
|
|||||||
name: "Pi Hole",
|
name: "Pi Hole",
|
||||||
url: "http://hole.local",
|
url: "http://hole.local",
|
||||||
secrets: [],
|
secrets: [],
|
||||||
|
appId: null,
|
||||||
});
|
});
|
||||||
await expect(actAsync()).rejects.toThrow("Integration not found");
|
await expect(actAsync()).rejects.toThrow("Integration not found");
|
||||||
});
|
});
|
||||||
@@ -385,6 +520,7 @@ describe("update should update an integration", () => {
|
|||||||
name: "Pi Hole",
|
name: "Pi Hole",
|
||||||
url: "http://hole.local",
|
url: "http://hole.local",
|
||||||
secrets: [],
|
secrets: [],
|
||||||
|
appId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ describe("testConnectionAsync should run test connection of integration", () =>
|
|||||||
value: "secret",
|
value: "secret",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
externalUrl: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,6 +105,7 @@ describe("testConnectionAsync should run test connection of integration", () =>
|
|||||||
value: "dbSecret",
|
value: "dbSecret",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
externalUrl: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -153,6 +155,7 @@ describe("testConnectionAsync should run test connection of integration", () =>
|
|||||||
value: "secret",
|
value: "secret",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
externalUrl: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -206,6 +209,7 @@ describe("testConnectionAsync should run test connection of integration", () =>
|
|||||||
value: "secret",
|
value: "secret",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
externalUrl: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -263,6 +267,81 @@ describe("testConnectionAsync should run test connection of integration", () =>
|
|||||||
value: "dbPassword",
|
value: "dbPassword",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
externalUrl: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("with input of existing github app", async () => {
|
||||||
|
// Arrange
|
||||||
|
const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync");
|
||||||
|
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
||||||
|
factorySpy.mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
testConnectionAsync: async () => await Promise.resolve({ success: true }),
|
||||||
|
} as homarrIntegrations.PiHoleIntegrationV6),
|
||||||
|
);
|
||||||
|
optionsSpy.mockReturnValue([[], ["githubAppId", "githubInstallationId", "privateKey"]]);
|
||||||
|
|
||||||
|
const integration = {
|
||||||
|
id: "new",
|
||||||
|
name: "GitHub",
|
||||||
|
url: "https://api.github.com",
|
||||||
|
kind: "github" as const,
|
||||||
|
secrets: [
|
||||||
|
{
|
||||||
|
kind: "githubAppId" as const,
|
||||||
|
value: "345",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "githubInstallationId" as const,
|
||||||
|
value: "456",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "privateKey" as const,
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const dbSecrets = [
|
||||||
|
{
|
||||||
|
kind: "githubAppId" as const,
|
||||||
|
value: "123.encrypted" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "githubInstallationId" as const,
|
||||||
|
value: "234.encrypted" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "privateKey" as const,
|
||||||
|
value: "privateKey.encrypted" as const,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await testConnectionAsync(integration, dbSecrets);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(factorySpy).toHaveBeenCalledWith({
|
||||||
|
id: "new",
|
||||||
|
name: "GitHub",
|
||||||
|
url: "https://api.github.com",
|
||||||
|
kind: "github" as const,
|
||||||
|
decryptedSecrets: [
|
||||||
|
expect.objectContaining({
|
||||||
|
kind: "githubAppId",
|
||||||
|
value: "345",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
kind: "githubInstallationId",
|
||||||
|
value: "456",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
kind: "privateKey",
|
||||||
|
value: "privateKey",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
externalUrl: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -114,11 +114,10 @@ export const userRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
// Max image size of 256KB, only png and jpeg are allowed
|
|
||||||
image: z
|
image: z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^data:image\/(png|jpeg|gif|webp);base64,[A-Za-z0-9/+]+=*$/g)
|
.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(),
|
.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
|
await ctx.db
|
||||||
.update(users)
|
.update(users)
|
||||||
.set({
|
.set({
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
import { observable } from "@trpc/server/observable";
|
||||||
import { z } from "zod/v4";
|
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 { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
|
import type { CalendarEvent } from "@homarr/integrations/types";
|
||||||
import { radarrReleaseTypes } from "@homarr/integrations/types";
|
import { radarrReleaseTypes } from "@homarr/integrations/types";
|
||||||
import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar";
|
import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar";
|
||||||
|
|
||||||
@@ -19,14 +24,56 @@ export const calendarRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar")))
|
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar")))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const results = await Promise.all(
|
return await Promise.all(
|
||||||
ctx.integrations.map(async (integration) => {
|
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 });
|
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();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -9,7 +9,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
|
|||||||
|
|
||||||
export const healthMonitoringRouter = createTRPCRouter({
|
export const healthMonitoringRouter = createTRPCRouter({
|
||||||
getSystemHealthStatus: publicProcedure
|
getSystemHealthStatus: publicProcedure
|
||||||
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "mock"))
|
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "mock"))
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
ctx.integrations.map(async (integration) => {
|
ctx.integrations.map(async (integration) => {
|
||||||
@@ -26,7 +26,7 @@ export const healthMonitoringRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
subscribeSystemHealthStatus: publicProcedure
|
subscribeSystemHealthStatus: publicProcedure
|
||||||
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "mock"))
|
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "mock"))
|
||||||
.subscription(({ ctx }) => {
|
.subscription(({ ctx }) => {
|
||||||
return observable<{ integrationId: string; healthInfo: SystemHealthMonitoring; timestamp: Date }>((emit) => {
|
return observable<{ integrationId: string; healthInfo: SystemHealthMonitoring; timestamp: Date }>((emit) => {
|
||||||
const unsubscribes: (() => void)[] = [];
|
const unsubscribes: (() => void)[] = [];
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { observable } from "@trpc/server/observable";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
|
import type { MediaTranscoding } from "@homarr/request-handler/media-transcoding";
|
||||||
import { mediaTranscodingRequestHandler } from "@homarr/request-handler/media-transcoding";
|
import { mediaTranscodingRequestHandler } from "@homarr/request-handler/media-transcoding";
|
||||||
import { paginatedSchema } from "@homarr/validation/common";
|
import { paginatedSchema } from "@homarr/validation/common";
|
||||||
|
|
||||||
@@ -15,7 +18,7 @@ export const mediaTranscodingRouter = createTRPCRouter({
|
|||||||
.input(paginatedSchema.pick({ page: true, pageSize: true }))
|
.input(paginatedSchema.pick({ page: true, pageSize: true }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const innerHandler = mediaTranscodingRequestHandler.handler(ctx.integration, {
|
const innerHandler = mediaTranscodingRequestHandler.handler(ctx.integration, {
|
||||||
pageOffset: input.page,
|
pageOffset: (input.page - 1) * input.pageSize,
|
||||||
pageSize: input.pageSize,
|
pageSize: input.pageSize,
|
||||||
});
|
});
|
||||||
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
@@ -25,4 +28,19 @@ export const mediaTranscodingRouter = createTRPCRouter({
|
|||||||
data,
|
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;
|
||||||
|
});
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { observable } from "@trpc/server/observable";
|
||||||
import { z } from "zod/v4";
|
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";
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
@@ -9,45 +11,19 @@ const atLocationInput = z.object({
|
|||||||
latitude: z.number(),
|
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({
|
export const weatherRouter = createTRPCRouter({
|
||||||
atLocation: publicProcedure.input(atLocationInput).query(async ({ input }) => {
|
atLocation: publicProcedure.input(atLocationInput).query(async ({ input }) => {
|
||||||
const res = await fetchWithTimeout(
|
const handler = weatherRequestHandler.handler(input);
|
||||||
`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`,
|
return await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false }).then((result) => result.data);
|
||||||
);
|
}),
|
||||||
const json: unknown = await res.json();
|
subscribeAtLocation: publicProcedure.input(atLocationInput).subscription(({ input }) => {
|
||||||
const weather = await atLocationOutput.parseAsync(json);
|
return observable<Weather>((emit) => {
|
||||||
return {
|
const handler = weatherRequestHandler.handler(input);
|
||||||
current: weather.current_weather,
|
const unsubscribe = handler.subscribe((data) => {
|
||||||
daily: weather.daily.time.map((value, index) => {
|
emit.next(data);
|
||||||
return {
|
});
|
||||||
time: value,
|
|
||||||
weatherCode: weather.daily.weathercode[index] ?? 404,
|
return unsubscribe;
|
||||||
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],
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -20,6 +20,7 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
|
|||||||
where: eq(users.id, user.id),
|
where: eq(users.id, user.id),
|
||||||
columns: {
|
columns: {
|
||||||
name: true,
|
name: true,
|
||||||
|
image: true,
|
||||||
colorScheme: 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}'`,
|
`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()}`);
|
logger.info(`User '${dbUser.name}' logged in at ${dayjs().format()}`);
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "^0.40.0",
|
"@auth/core": "^0.41.0",
|
||||||
"@auth/drizzle-adapter": "^1.10.0",
|
"@auth/drizzle-adapter": "^1.11.0",
|
||||||
"@homarr/certificates": "workspace:^0.1.0",
|
"@homarr/certificates": "workspace:^0.1.0",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
@@ -35,20 +35,20 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"cookies": "^0.9.1",
|
"cookies": "^0.9.1",
|
||||||
"ldapts": "8.0.9",
|
"ldapts": "8.0.9",
|
||||||
"next": "15.5.0",
|
"next": "15.5.6",
|
||||||
"next-auth": "5.0.0-beta.29",
|
"next-auth": "5.0.0-beta.29",
|
||||||
"react": "19.1.1",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.2.0",
|
||||||
"zod": "^4.0.17"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/bcrypt": "6.0.0",
|
"@types/bcrypt": "6.0.0",
|
||||||
"@types/cookies": "0.9.1",
|
"@types/cookies": "0.9.2",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.38.0",
|
||||||
"prettier": "^3.6.2",
|
"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,
|
id: profile.sub,
|
||||||
name,
|
name,
|
||||||
email: profile.email,
|
email: profile.email,
|
||||||
|
image: typeof profile.picture === "string" ? profile.picture : null,
|
||||||
provider: "oidc",
|
provider: "oidc",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,14 +25,14 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/api": "workspace:^0.1.0",
|
"@homarr/api": "workspace:^0.1.0",
|
||||||
"react": "19.1.1",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.1.1"
|
"react-dom": "19.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.38.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,13 +24,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"undici": "7.14.0"
|
"undici": "7.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.38.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,14 +28,14 @@
|
|||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"dotenv": "^17.2.1"
|
"dotenv": "^17.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"esbuild": "^0.25.9",
|
"esbuild": "^0.25.11",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.38.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,10 +24,12 @@ export const env = createEnv({
|
|||||||
message: `SECRET_ENCRYPTION_KEY must only contain hex characters${errorSuffix}`,
|
message: `SECRET_ENCRYPTION_KEY must only contain hex characters${errorSuffix}`,
|
||||||
}),
|
}),
|
||||||
NO_EXTERNAL_CONNECTION: createBooleanSchema(false),
|
NO_EXTERNAL_CONNECTION: createBooleanSchema(false),
|
||||||
|
ENABLE_DNS_CACHING: createBooleanSchema(false),
|
||||||
},
|
},
|
||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
SECRET_ENCRYPTION_KEY: process.env.SECRET_ENCRYPTION_KEY,
|
SECRET_ENCRYPTION_KEY: process.env.SECRET_ENCRYPTION_KEY,
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
NO_EXTERNAL_CONNECTION: process.env.NO_EXTERNAL_CONNECTION,
|
NO_EXTERNAL_CONNECTION: process.env.NO_EXTERNAL_CONNECTION,
|
||||||
|
ENABLE_DNS_CACHING: process.env.ENABLE_DNS_CACHING,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
".": "./index.ts",
|
".": "./index.ts",
|
||||||
"./types": "./src/types.ts",
|
"./types": "./src/types.ts",
|
||||||
"./server": "./src/server.ts",
|
"./server": "./src/server.ts",
|
||||||
|
"./init-dns": "./src/dns.ts",
|
||||||
"./client": "./src/client.ts",
|
"./client": "./src/client.ts",
|
||||||
"./env": "./env.ts"
|
"./env": "./env.ts"
|
||||||
},
|
},
|
||||||
@@ -28,20 +29,22 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@paralleldrive/cuid2": "^2.2.2",
|
"@paralleldrive/cuid2": "^3.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.18",
|
||||||
"next": "15.5.0",
|
"dns-caching": "^0.2.7",
|
||||||
"react": "19.1.1",
|
"next": "15.5.6",
|
||||||
"react-dom": "19.1.1",
|
"octokit": "^5.0.4",
|
||||||
"undici": "7.14.0",
|
"react": "19.2.0",
|
||||||
"zod": "^4.0.17",
|
"react-dom": "19.2.0",
|
||||||
"zod-validation-error": "^4.0.1"
|
"undici": "7.16.0",
|
||||||
|
"zod": "^4.1.12",
|
||||||
|
"zod-validation-error": "^4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.38.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { DnsCacheManager } from "dns-caching";
|
||||||
|
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
|
import { env } from "../env";
|
||||||
|
|
||||||
|
// Add global type augmentation for homarr
|
||||||
|
declare global {
|
||||||
|
var homarr: {
|
||||||
|
dnsCacheManager?: DnsCacheManager;
|
||||||
|
// add other properties if needed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize global.homarr if not present
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
global.homarr ??= {};
|
||||||
|
global.homarr.dnsCacheManager ??= new DnsCacheManager({
|
||||||
|
cacheMaxEntries: 1000,
|
||||||
|
forceMinTtl: 5 * 60 * 1000, // 5 minutes
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (env.ENABLE_DNS_CACHING) {
|
||||||
|
global.homarr.dnsCacheManager.initialize();
|
||||||
|
}
|
||||||
@@ -3,3 +3,4 @@ export * from "./fetch-http-error-handler";
|
|||||||
export * from "./ofetch-http-error-handler";
|
export * from "./ofetch-http-error-handler";
|
||||||
export * from "./axios-http-error-handler";
|
export * from "./axios-http-error-handler";
|
||||||
export * from "./tsdav-http-error-handler";
|
export * from "./tsdav-http-error-handler";
|
||||||
|
export * from "./octokit-http-error-handler";
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { RequestError as OctokitRequestError } from "octokit";
|
||||||
|
|
||||||
|
import type { AnyRequestError } from "../request-error";
|
||||||
|
import { ResponseError } from "../response-error";
|
||||||
|
import { HttpErrorHandler } from "./http-error-handler";
|
||||||
|
|
||||||
|
export class OctokitHttpErrorHandler extends HttpErrorHandler {
|
||||||
|
/**
|
||||||
|
* I wasn't able to get a request error triggered. Therefore we ignore them for now
|
||||||
|
* and just forward them as unknown errors
|
||||||
|
*/
|
||||||
|
handleRequestError(_: unknown): AnyRequestError | undefined {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResponseError(error: unknown): ResponseError | undefined {
|
||||||
|
if (!(error instanceof OctokitRequestError)) return undefined;
|
||||||
|
|
||||||
|
return new ResponseError({
|
||||||
|
status: error.status,
|
||||||
|
url: error.response?.url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,13 +3,20 @@ import { Agent } from "undici";
|
|||||||
|
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
|
// The below import statement initializes dns-caching
|
||||||
|
import "./dns";
|
||||||
|
|
||||||
export class LoggingAgent extends Agent {
|
export class LoggingAgent extends Agent {
|
||||||
constructor(...props: ConstructorParameters<typeof Agent>) {
|
constructor(...props: ConstructorParameters<typeof Agent>) {
|
||||||
super(...props);
|
super(...props);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean {
|
dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean {
|
||||||
const url = new URL(`${options.origin as string}${options.path}`);
|
const path = options.path
|
||||||
|
.split("/")
|
||||||
|
.map((segment) => (segment.length >= 32 && !segment.startsWith("?") ? "REDACTED" : segment))
|
||||||
|
.join("/");
|
||||||
|
const url = new URL(`${options.origin as string}${path}`);
|
||||||
|
|
||||||
// The below code should prevent sensitive data from being logged as
|
// The below code should prevent sensitive data from being logged as
|
||||||
// some integrations use query parameters for auth
|
// some integrations use query parameters for auth
|
||||||
|
|||||||
@@ -11,13 +11,17 @@ const calculateTimeAgo = (timestamp: Date) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useTimeAgo = (timestamp: Date, updateFrequency = 1000) => {
|
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(() => {
|
useEffect(() => {
|
||||||
const intervalId = setInterval(() => setTimeAgo(calculateTimeAgo(timestamp)), updateFrequency);
|
const intervalId = setInterval(() => setTimeAgo(calculateTimeAgo(timestamp)), updateFrequency);
|
||||||
|
|
||||||
return () => clearInterval(intervalId); // clear interval on hook unmount
|
return () => clearInterval(intervalId); // clear interval on hook unmount
|
||||||
}, [timestamp]);
|
}, [timestamp, updateFrequency]);
|
||||||
|
|
||||||
return timeAgo;
|
return timeAgo;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,3 +47,8 @@ export const humanFileSize = (size: number, concat = ""): string => {
|
|||||||
}
|
}
|
||||||
return "∞";
|
return "∞";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const IMPERIAL_MULTIPLIER = 1.609344;
|
||||||
|
|
||||||
|
export const metricToImperial = (metricValue: number) => metricValue / IMPERIAL_MULTIPLIER;
|
||||||
|
export const imperialToMetric = (imperialValue: number) => imperialValue * IMPERIAL_MULTIPLIER;
|
||||||
@@ -66,6 +66,7 @@ describe("LoggingAgent should log all requests", () => {
|
|||||||
["/?one=a1&two=b2&three=c3", `/?one=${REDACTED}&two=${REDACTED}&three=${REDACTED}`],
|
["/?one=a1&two=b2&three=c3", `/?one=${REDACTED}&two=${REDACTED}&three=${REDACTED}`],
|
||||||
["/?numberWith13Chars=1234567890123", `/?numberWith13Chars=${REDACTED}`],
|
["/?numberWith13Chars=1234567890123", `/?numberWith13Chars=${REDACTED}`],
|
||||||
[`/?stringWith13Chars=${"a".repeat(13)}`, `/?stringWith13Chars=${REDACTED}`],
|
[`/?stringWith13Chars=${"a".repeat(13)}`, `/?stringWith13Chars=${REDACTED}`],
|
||||||
|
[`/${"a".repeat(32)}/?param=123`, `/${REDACTED}/?param=123`],
|
||||||
])("should redact sensitive data in url https://homarr.dev%s", (path, expected) => {
|
])("should redact sensitive data in url https://homarr.dev%s", (path, expected) => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const infoLogSpy = vi.spyOn(logger, "debug");
|
const infoLogSpy = vi.spyOn(logger, "debug");
|
||||||
|
|||||||
@@ -25,14 +25,14 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@t3-oss/env-nextjs": "^0.13.8",
|
"@t3-oss/env-nextjs": "^0.13.8",
|
||||||
"ioredis": "5.7.0",
|
"ioredis": "5.8.2",
|
||||||
"zod": "^4.0.17"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.38.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@ export const createRedisClient = () =>
|
|||||||
...defaultRedisOptions,
|
...defaultRedisOptions,
|
||||||
host: redisEnv.HOST,
|
host: redisEnv.HOST,
|
||||||
port: redisEnv.PORT,
|
port: redisEnv.PORT,
|
||||||
|
db: redisEnv.DATABASE_INDEX,
|
||||||
tls: redisEnv.TLS_CA
|
tls: redisEnv.TLS_CA
|
||||||
? {
|
? {
|
||||||
ca: redisEnv.TLS_CA,
|
ca: redisEnv.TLS_CA,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const redisEnv = createEnv({
|
|||||||
TLS_CA: z.string().optional(),
|
TLS_CA: z.string().optional(),
|
||||||
USERNAME: z.string().optional(),
|
USERNAME: z.string().optional(),
|
||||||
PASSWORD: z.string().optional(),
|
PASSWORD: z.string().optional(),
|
||||||
|
DATABASE_INDEX: z.coerce.number().optional(),
|
||||||
},
|
},
|
||||||
runtimeEnv: runtimeEnvWithPrefix("REDIS_"),
|
runtimeEnv: runtimeEnvWithPrefix("REDIS_"),
|
||||||
});
|
});
|
||||||
@@ -29,21 +29,21 @@
|
|||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^0.1.0",
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
"@tanstack/react-query": "^5.85.5",
|
"@tanstack/react-query": "^5.90.5",
|
||||||
"@trpc/client": "^11.4.4",
|
"@trpc/client": "^11.6.0",
|
||||||
"@trpc/server": "^11.4.4",
|
"@trpc/server": "^11.6.0",
|
||||||
"@trpc/tanstack-react-query": "^11.4.4",
|
"@trpc/tanstack-react-query": "^11.6.0",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"react": "19.1.1",
|
"react": "19.2.0",
|
||||||
"zod": "^4.0.17"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/react": "19.1.10",
|
"@types/react": "19.2.2",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.38.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.38.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.38.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,8 +72,6 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
|
|||||||
where: (cronJobConfigurations, { eq }) => eq(cronJobConfigurations.name, name),
|
where: (cronJobConfigurations, { eq }) => eq(cronJobConfigurations.name, name),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (defaultCronExpression === "never") return null;
|
|
||||||
|
|
||||||
const scheduledTask = createTask(
|
const scheduledTask = createTask(
|
||||||
configuration?.cronExpression ?? defaultCronExpression,
|
configuration?.cronExpression ?? defaultCronExpression,
|
||||||
() => void catchingCallbackAsync(),
|
() => void catchingCallbackAsync(),
|
||||||
@@ -120,7 +118,7 @@ export const createCronJobCreator = <TAllowedNames extends string = string>(
|
|||||||
options: CreateCronJobOptions = { runOnStart: false },
|
options: CreateCronJobOptions = { runOnStart: false },
|
||||||
) => {
|
) => {
|
||||||
creatorOptions.logger.logDebug(`Validating cron expression '${defaultCronExpression}' for job: ${name}`);
|
creatorOptions.logger.logDebug(`Validating cron expression '${defaultCronExpression}' for job: ${name}`);
|
||||||
if (defaultCronExpression !== "never" && !validate(defaultCronExpression)) {
|
if (!validate(defaultCronExpression)) {
|
||||||
throw new Error(`Invalid cron expression '${defaultCronExpression}' for job '${name}'`);
|
throw new Error(`Invalid cron expression '${defaultCronExpression}' for job '${name}'`);
|
||||||
}
|
}
|
||||||
creatorOptions.logger.logDebug(`Cron job expression '${defaultCronExpression}' for job ${name} is valid`);
|
creatorOptions.logger.logDebug(`Cron job expression '${defaultCronExpression}' for job ${name} is valid`);
|
||||||
@@ -132,8 +130,6 @@ export const createCronJobCreator = <TAllowedNames extends string = string>(
|
|||||||
// This is a type guard to check if the cron expression is valid and give the user a type hint
|
// This is a type guard to check if the cron expression is valid and give the user a type hint
|
||||||
return returnValue as unknown as ValidateCron<TExpression> extends true
|
return returnValue as unknown as ValidateCron<TExpression> extends true
|
||||||
? typeof returnValue
|
? typeof returnValue
|
||||||
: TExpression extends "never"
|
: "Invalid cron expression";
|
||||||
? typeof returnValue
|
|
||||||
: "Invalid cron expression";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -8,4 +8,3 @@ export const EVERY_10_MINUTES = checkCron("*/10 * * * *") satisfies string;
|
|||||||
export const EVERY_HOUR = checkCron("0 * * * *") satisfies string;
|
export const EVERY_HOUR = checkCron("0 * * * *") satisfies string;
|
||||||
export const EVERY_DAY = checkCron("0 0 * * */1") satisfies string;
|
export const EVERY_DAY = checkCron("0 0 * * */1") satisfies string;
|
||||||
export const EVERY_WEEK = checkCron("0 0 * * 1") satisfies string;
|
export const EVERY_WEEK = checkCron("0 0 * * 1") satisfies string;
|
||||||
export const NEVER = "never";
|
|
||||||
@@ -45,7 +45,6 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scheduledTask = await job.createTaskAsync();
|
const scheduledTask = await job.createTaskAsync();
|
||||||
if (!scheduledTask) continue;
|
|
||||||
|
|
||||||
tasks.set(job.name, scheduledTask);
|
tasks.set(job.name, scheduledTask);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.38.0",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,8 +21,8 @@ import { refreshNotificationsJob } from "./jobs/integrations/notifications";
|
|||||||
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
|
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
|
||||||
import { pingJob } from "./jobs/ping";
|
import { pingJob } from "./jobs/ping";
|
||||||
import { rssFeedsJob } from "./jobs/rss-feeds";
|
import { rssFeedsJob } from "./jobs/rss-feeds";
|
||||||
import { sessionCleanupJob } from "./jobs/session-cleanup";
|
|
||||||
import { updateCheckerJob } from "./jobs/update-checker";
|
import { updateCheckerJob } from "./jobs/update-checker";
|
||||||
|
import { weatherJob } from "./jobs/weather";
|
||||||
import { createCronJobGroup } from "./lib";
|
import { createCronJobGroup } from "./lib";
|
||||||
|
|
||||||
export const jobGroup = createCronJobGroup({
|
export const jobGroup = createCronJobGroup({
|
||||||
@@ -39,7 +39,6 @@ export const jobGroup = createCronJobGroup({
|
|||||||
rssFeeds: rssFeedsJob,
|
rssFeeds: rssFeedsJob,
|
||||||
indexerManager: indexerManagerJob,
|
indexerManager: indexerManagerJob,
|
||||||
healthMonitoring: healthMonitoringJob,
|
healthMonitoring: healthMonitoringJob,
|
||||||
sessionCleanup: sessionCleanupJob,
|
|
||||||
updateChecker: updateCheckerJob,
|
updateChecker: updateCheckerJob,
|
||||||
mediaTranscoding: mediaTranscodingJob,
|
mediaTranscoding: mediaTranscodingJob,
|
||||||
minecraftServerStatus: minecraftServerStatusJob,
|
minecraftServerStatus: minecraftServerStatusJob,
|
||||||
@@ -50,6 +49,7 @@ export const jobGroup = createCronJobGroup({
|
|||||||
firewallVersion: firewallVersionJob,
|
firewallVersion: firewallVersionJob,
|
||||||
firewallInterfaces: firewallInterfacesJob,
|
firewallInterfaces: firewallInterfacesJob,
|
||||||
refreshNotifications: refreshNotificationsJob,
|
refreshNotifications: refreshNotificationsJob,
|
||||||
|
weather: weatherJob,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
|
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { env } from "@homarr/auth/env";
|
|
||||||
import { NEVER } from "@homarr/cron-jobs-core/expressions";
|
|
||||||
import { db, eq, inArray } from "@homarr/db";
|
|
||||||
import { sessions, users } from "@homarr/db/schema";
|
|
||||||
import { supportedAuthProviders } from "@homarr/definitions";
|
|
||||||
import { logger } from "@homarr/log";
|
|
||||||
|
|
||||||
import { createCronJob } from "../lib";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes sessions for users that have inactive auth providers.
|
|
||||||
* Sessions from other providers are deleted so they can no longer be used.
|
|
||||||
*/
|
|
||||||
export const sessionCleanupJob = createCronJob("sessionCleanup", NEVER, {
|
|
||||||
runOnStart: true,
|
|
||||||
}).withCallback(async () => {
|
|
||||||
const currentAuthProviders = env.AUTH_PROVIDERS;
|
|
||||||
|
|
||||||
const inactiveAuthProviders = supportedAuthProviders.filter((provider) => !currentAuthProviders.includes(provider));
|
|
||||||
const subQuery = db
|
|
||||||
.select({ id: users.id })
|
|
||||||
.from(users)
|
|
||||||
.where(inArray(users.provider, inactiveAuthProviders))
|
|
||||||
.as("sq");
|
|
||||||
const sessionsWithInactiveProviders = await db
|
|
||||||
.select({ userId: sessions.userId })
|
|
||||||
.from(sessions)
|
|
||||||
.rightJoin(subQuery, eq(sessions.userId, subQuery.id));
|
|
||||||
|
|
||||||
const userIds = sessionsWithInactiveProviders.map(({ userId }) => userId).filter((value) => value !== null);
|
|
||||||
await db.delete(sessions).where(inArray(sessions.userId, userIds));
|
|
||||||
|
|
||||||
if (sessionsWithInactiveProviders.length > 0) {
|
|
||||||
logger.info(`Deleted sessions for inactive providers count=${userIds.length}`);
|
|
||||||
} else {
|
|
||||||
logger.debug("No sessions to delete");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -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
Loaded 100 of 259 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user