Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
@@ -33,6 +33,8 @@ body:
|
||||
options:
|
||||
# The below comment is used to insert a new version with on-release.yml
|
||||
#NEXT_VERSION#
|
||||
- 1.37.0
|
||||
- 1.36.1
|
||||
- 1.36.0
|
||||
- 1.35.1
|
||||
- 1.35.0
|
||||
|
||||
@@ -3,6 +3,8 @@ on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
approve-automatic-prs:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -12,10 +14,12 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
- name: Obtain token
|
||||
id: obtainToken
|
||||
uses: tibdex/github-app-token@v2
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
private_key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
||||
app_id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
||||
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
||||
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
||||
permission-pull-requests: write # required to approve pull request
|
||||
|
||||
- name: Install GitHub CLI
|
||||
run: sudo apt-get install -y gh
|
||||
- name: Approve automatic PRs
|
||||
|
||||
@@ -5,6 +5,9 @@ on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *" # every day at midnight
|
||||
|
||||
permissions:
|
||||
contents: read # required for code checkout
|
||||
|
||||
jobs:
|
||||
download-crowdin-translations:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -15,10 +18,12 @@ jobs:
|
||||
|
||||
- name: Obtain token
|
||||
id: obtainToken
|
||||
uses: tibdex/github-app-token@v2
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
private_key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
|
||||
app_id: ${{ secrets.CROWDIN_APP_ID }}
|
||||
private-key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
|
||||
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
|
||||
id: crowdin-download
|
||||
|
||||
@@ -49,18 +49,23 @@ jobs:
|
||||
- name: Obtain token
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
id: obtainToken
|
||||
uses: tibdex/github-app-token@v2
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
private_key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||
app_id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||
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
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
with:
|
||||
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'
|
||||
with:
|
||||
node-version: 22.19.0
|
||||
cache: "pnpm"
|
||||
- run: npm i -g pnpm
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
- 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 }})"
|
||||
- name: Obtain token
|
||||
id: obtainApprovalToken
|
||||
uses: tibdex/github-app-token@v2
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
private_key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
||||
app_id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
||||
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
||||
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
||||
permission-pull-requests: write
|
||||
- name: Approve PR
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
|
||||
@@ -60,10 +61,12 @@ jobs:
|
||||
gh pr review --approve --body "Automatically approved by GitHub Action"
|
||||
- name: Obtain token
|
||||
id: obtainMergeToken
|
||||
uses: tibdex/github-app-token@v2
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
private_key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||
app_id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||
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
|
||||
if: ${{ steps.semver.outputs.bump != 'major' }}
|
||||
name: automerge
|
||||
|
||||
@@ -11,12 +11,14 @@ jobs:
|
||||
steps:
|
||||
- name: Obtain token
|
||||
id: obtainToken
|
||||
uses: tibdex/github-app-token@v2
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
private_key: ${{ secrets.HOMARR_DOCS_RELEASE_APP_PRIVATE_KEY }}
|
||||
app_id: ${{ vars.HOMARR_DOCS_RELEASE_APP_ID }}
|
||||
installation_retrieval_mode: repository
|
||||
installation_retrieval_payload: homarr-labs/documentation
|
||||
private-key: ${{ secrets.HOMARR_DOCS_RELEASE_APP_PRIVATE_KEY }}
|
||||
app-id: ${{ vars.HOMARR_DOCS_RELEASE_APP_ID }}
|
||||
owner: homarr-labs
|
||||
repositories: |
|
||||
documentation
|
||||
permission-contents: write # required to dispatch repository workflow
|
||||
- name: Trigger documentation release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||
@@ -40,10 +42,12 @@ jobs:
|
||||
steps:
|
||||
- name: Obtain token
|
||||
id: obtainToken
|
||||
uses: tibdex/github-app-token@v2
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
private_key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||
app_id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||
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
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -57,6 +61,7 @@ jobs:
|
||||
run: |
|
||||
git config --global user.email "175486441+homarr-releases[bot]@users.noreply.github.com"
|
||||
git config --global user.name "Releases Homarr"
|
||||
git checkout -b update-bug-report-template
|
||||
git add .
|
||||
git commit -m "chore: update bug report template"
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@ permissions:
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches-ignore: "renovate/*"
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
skip-stability-days:
|
||||
if: ${{ !startsWith(github.head_ref, 'renovate/') }}
|
||||
name: Skip Stability Days
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -9,9 +9,6 @@ env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
CROWDIN_TOKEN: "${{ secrets.CROWDIN_UPDATE_CONTRIBUTORS_TOKEN }}"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-contributors:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -21,20 +18,24 @@ jobs:
|
||||
steps:
|
||||
- name: Obtain token
|
||||
id: obtainToken
|
||||
uses: tibdex/github-app-token@v2
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
private_key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
||||
app_id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
||||
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
||||
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
|
||||
uses: actions/checkout@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Run update script
|
||||
run: node ./scripts/update-contributors.mjs
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Update integration list
|
||||
|
||||
on:
|
||||
workflow_dispatch: { }
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
paths:
|
||||
- packages/definitions/src/integration.ts
|
||||
@@ -20,10 +20,12 @@ jobs:
|
||||
steps:
|
||||
- name: Obtain token
|
||||
id: obtainToken
|
||||
uses: tibdex/github-app-token@v2
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
private_key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
||||
app_id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
||||
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
||||
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
|
||||
uses: actions/checkout@v5
|
||||
env:
|
||||
@@ -59,4 +61,4 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||
run: |
|
||||
gh pr merge ${{steps.create-pull-request.outputs.pull-request-number}} --auto --squash
|
||||
gh pr merge ${{steps.create-pull-request.outputs.pull-request-number}} --auto --squash
|
||||
@@ -1,3 +1,68 @@
|
||||
## [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
|
||||
|
||||
@@ -42,9 +42,11 @@ const nextConfig: NextConfig = {
|
||||
headers: [
|
||||
{
|
||||
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: `
|
||||
default-src 'self';
|
||||
script-src * 'unsafe-inline' 'unsafe-eval';
|
||||
worker-src * blob:;
|
||||
base-uri 'self';
|
||||
connect-src *;
|
||||
style-src * 'unsafe-inline';
|
||||
@@ -53,7 +55,7 @@ const nextConfig: NextConfig = {
|
||||
form-action 'self';
|
||||
img-src * data:;
|
||||
font-src * data:;
|
||||
media-src * data:;
|
||||
media-src * data: blob:;
|
||||
`
|
||||
.replace(/\s{2,}/g, " ")
|
||||
.trim(),
|
||||
|
||||
+22
-22
@@ -50,57 +50,57 @@
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@homarr/widgets": "workspace:^0.1.0",
|
||||
"@mantine/colors-generator": "^8.2.8",
|
||||
"@mantine/core": "^8.2.8",
|
||||
"@mantine/dropzone": "^8.2.8",
|
||||
"@mantine/hooks": "^8.2.8",
|
||||
"@mantine/modals": "^8.2.8",
|
||||
"@mantine/tiptap": "^8.2.8",
|
||||
"@mantine/colors-generator": "^8.3.1",
|
||||
"@mantine/core": "^8.3.1",
|
||||
"@mantine/dropzone": "^8.3.1",
|
||||
"@mantine/hooks": "^8.3.1",
|
||||
"@mantine/modals": "^8.3.1",
|
||||
"@mantine/tiptap": "^8.3.1",
|
||||
"@million/lint": "1.0.14",
|
||||
"@tabler/icons-react": "^3.34.1",
|
||||
"@tanstack/react-query": "^5.87.1",
|
||||
"@tanstack/react-query-devtools": "^5.87.1",
|
||||
"@tanstack/react-query-next-experimental": "^5.87.1",
|
||||
"@trpc/client": "^11.5.0",
|
||||
"@trpc/next": "^11.5.0",
|
||||
"@trpc/react-query": "^11.5.0",
|
||||
"@trpc/server": "^11.5.0",
|
||||
"@tanstack/react-query": "^5.87.4",
|
||||
"@tanstack/react-query-devtools": "^5.87.4",
|
||||
"@tanstack/react-query-next-experimental": "^5.87.4",
|
||||
"@trpc/client": "^11.5.1",
|
||||
"@trpc/next": "^11.5.1",
|
||||
"@trpc/react-query": "^11.5.1",
|
||||
"@trpc/server": "^11.5.1",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"chroma-js": "^3.1.2",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.18",
|
||||
"dotenv": "^17.2.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"flag-icons": "^7.5.0",
|
||||
"glob": "^11.0.3",
|
||||
"jotai": "^2.13.1",
|
||||
"jotai": "^2.14.0",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "15.5.2",
|
||||
"next": "15.5.3",
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"sass": "^1.91.0",
|
||||
"sass": "^1.92.1",
|
||||
"superjson": "2.2.2",
|
||||
"swagger-ui-react": "^5.28.0",
|
||||
"swagger-ui-react": "^5.29.0",
|
||||
"use-deep-compare-effect": "^1.8.1",
|
||||
"zod": "^4.1.5"
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/chroma-js": "3.1.1",
|
||||
"@types/node": "^22.18.0",
|
||||
"@types/node": "^22.18.3",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react": "19.1.13",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"node-loader": "^2.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
|
||||
@@ -61,6 +61,10 @@ const getBoardAndPermissionsAsync = async (params: Awaited<Props["params"]>) =>
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,6 +45,10 @@ export const createBoardLayout = <TParams extends Params>({
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (error instanceof TRPCError && error.code === "BAD_REQUEST") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
const colorScheme = await getCurrentColorSchemeAsync();
|
||||
|
||||
+7
@@ -1,9 +1,12 @@
|
||||
import {
|
||||
IconCode,
|
||||
IconGrid3x3,
|
||||
IconKey,
|
||||
IconLink,
|
||||
IconMessage,
|
||||
IconPassword,
|
||||
IconPasswordUser,
|
||||
IconPlug,
|
||||
IconServer,
|
||||
IconUser,
|
||||
} from "@tabler/icons-react";
|
||||
@@ -19,6 +22,10 @@ export const integrationSecretIcons = {
|
||||
tokenId: IconGrid3x3,
|
||||
personalAccessToken: IconPasswordUser,
|
||||
topic: IconMessage,
|
||||
url: IconLink,
|
||||
opnsenseApiKey: IconKey,
|
||||
opnsenseApiSecret: IconPassword,
|
||||
githubAppId: IconCode,
|
||||
githubInstallationId: IconPlug,
|
||||
privateKey: IconKey,
|
||||
} satisfies Record<IntegrationSecretKind, TablerIcon>;
|
||||
+32
-4
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
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 type { IntegrationSecretKind } from "@homarr/definitions";
|
||||
@@ -14,9 +14,9 @@ interface IntegrationSecretInputProps {
|
||||
label?: string;
|
||||
kind: IntegrationSecretKind;
|
||||
value?: string;
|
||||
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||
onFocus?: FocusEventHandler<HTMLInputElement>;
|
||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||
onChange: ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
||||
onFocus?: FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
||||
onBlur?: FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,19 @@ export const IntegrationSecretInput = (props: IntegrationSecretInputProps) => {
|
||||
const PublicSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
|
||||
const t = useI18n();
|
||||
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 (
|
||||
<TextInput
|
||||
@@ -45,6 +58,21 @@ const PublicSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
|
||||
const PrivateSecretInput = ({ kind, ...props }: IntegrationSecretInputProps) => {
|
||||
const t = useI18n();
|
||||
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 (
|
||||
<PasswordInput
|
||||
|
||||
+9
-1
@@ -33,6 +33,8 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
integration.secrets.every((secret) => secretKinds.includes(secret.kind)),
|
||||
) ?? getDefaultSecretKinds(integration.kind);
|
||||
|
||||
const hasUrlSecret = secretsKinds.includes("url");
|
||||
|
||||
const router = useRouter();
|
||||
const form = useZodForm(integrationUpdateSchema.omit({ id: true }), {
|
||||
initialValues: {
|
||||
@@ -50,10 +52,14 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
const secretsMap = new Map(integration.secrets.map((secret) => [secret.kind, secret]));
|
||||
|
||||
const handleSubmitAsync = async (values: FormType) => {
|
||||
const url = hasUrlSecret
|
||||
? new URL(values.secrets.find((secret) => secret.kind === "url")?.value ?? values.url).origin
|
||||
: values.url;
|
||||
await mutateAsync(
|
||||
{
|
||||
id: integration.id,
|
||||
...values,
|
||||
url,
|
||||
secrets: values.secrets.map((secret) => ({
|
||||
kind: secret.kind,
|
||||
value: secret.value === "" ? null : secret.value,
|
||||
@@ -92,7 +98,9 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
|
||||
<Stack>
|
||||
<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")}>
|
||||
<Stack gap="sm">
|
||||
|
||||
@@ -55,12 +55,19 @@ const formSchema = integrationCreateSchema.omit({ kind: true }).and(
|
||||
export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) => {
|
||||
const t = useI18n();
|
||||
const secretKinds = getAllSecretKindOptions(searchParams.kind);
|
||||
const hasUrlSecret = secretKinds.some((kinds) => kinds.includes("url"));
|
||||
const router = useRouter();
|
||||
const [opened, setOpened] = useState(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, {
|
||||
initialValues: {
|
||||
name: searchParams.name ?? getIntegrationName(searchParams.kind),
|
||||
url: searchParams.url ?? getIntegrationDefaultUrl(searchParams.kind) ?? "",
|
||||
url,
|
||||
secrets: secretKinds[0].map((kind) => ({
|
||||
kind,
|
||||
value: "",
|
||||
@@ -83,10 +90,14 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
const [error, setError] = useState<null | AnyMappedTestConnectionError>(null);
|
||||
|
||||
const handleSubmitAsync = async (values: FormType) => {
|
||||
const url = hasUrlSecret
|
||||
? new URL(values.secrets.find((secret) => secret.kind === "url")?.value ?? values.url).origin
|
||||
: values.url;
|
||||
await createIntegrationAsync(
|
||||
{
|
||||
kind: searchParams.kind,
|
||||
...values,
|
||||
url,
|
||||
},
|
||||
{
|
||||
async onSuccess(data) {
|
||||
@@ -114,10 +125,10 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
await createAppAsync(
|
||||
{
|
||||
name: values.name,
|
||||
href: hasCustomHref ? values.appHref : values.url,
|
||||
href: hasCustomHref ? values.appHref : url,
|
||||
iconUrl: getIconUrl(searchParams.kind),
|
||||
description: null,
|
||||
pingUrl: values.url,
|
||||
pingUrl: url,
|
||||
},
|
||||
{
|
||||
async onSettled() {
|
||||
@@ -149,7 +160,9 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
<Stack>
|
||||
<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")}>
|
||||
<Stack gap="sm">
|
||||
|
||||
@@ -17,6 +17,10 @@ import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
|
||||
import { IconPowerOff } from "@homarr/ui/icons";
|
||||
|
||||
const cronExpressions = [
|
||||
{
|
||||
value: "*/1 * * * * *",
|
||||
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.seconds", { interval: 1 }),
|
||||
},
|
||||
{
|
||||
value: "*/5 * * * * *",
|
||||
label: (t: TranslationFunction) => t("management.page.tool.tasks.interval.seconds", { interval: 5 }),
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/analytics": "workspace:^0.1.0",
|
||||
"@homarr/auth": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/cron-job-api": "workspace:^0.1.0",
|
||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||
@@ -32,23 +33,24 @@
|
||||
"@homarr/log": "workspace:^",
|
||||
"@homarr/ping": "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/validation": "workspace:^0.1.0",
|
||||
"@homarr/widgets": "workspace:^0.1.0",
|
||||
"dayjs": "^1.11.18",
|
||||
"dotenv": "^17.2.1",
|
||||
"fastify": "^5.5.0",
|
||||
"dotenv": "^17.2.2",
|
||||
"fastify": "^5.6.0",
|
||||
"superjson": "2.2.2",
|
||||
"undici": "7.15.0"
|
||||
"undici": "7.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node": "^22.18.0",
|
||||
"@types/node": "^22.18.3",
|
||||
"dotenv-cli": "^10.0.0",
|
||||
"esbuild": "^0.25.9",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tsx": "4.20.4",
|
||||
"typescript": "^5.9.2"
|
||||
|
||||
@@ -26,7 +26,6 @@ export class JobManager implements IJobManager {
|
||||
logger.info(`Updating cron job interval name="${name}" expression="${cron}"`);
|
||||
const job = this.jobGroup.getJobRegistry().get(name);
|
||||
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)) {
|
||||
throw new Error(`Invalid cron expression: ${cron}`);
|
||||
}
|
||||
@@ -45,7 +44,6 @@ export class JobManager implements IJobManager {
|
||||
logger.info(`Disabling cron job name="${name}"`);
|
||||
const job = this.jobGroup.getJobRegistry().get(name);
|
||||
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.jobGroup.stopAsync(name);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { db } from "@homarr/db";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { JobManager } from "./job-manager";
|
||||
import { onStartAsync } from "./on-start";
|
||||
|
||||
const server = fastify({
|
||||
maxParamLength: 5000,
|
||||
@@ -32,6 +33,7 @@ server.register(fastifyTRPCPlugin, {
|
||||
});
|
||||
|
||||
void (async () => {
|
||||
await onStartAsync();
|
||||
await jobGroup.initializeAsync();
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
"@homarr/log": "workspace:^",
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"dotenv": "^17.2.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"tsx": "4.20.4",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
@@ -35,7 +35,7 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"esbuild": "^0.25.9",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
|
||||
+28
-12
@@ -30,6 +30,8 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
- 🖌️ Highly customizable with an extensive drag and drop grid system
|
||||
@@ -132,6 +134,13 @@
|
||||
</a>
|
||||
</td>
|
||||
<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">
|
||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg" alt="Jellyfin" width="90" height="90" />
|
||||
<br/>
|
||||
@@ -144,15 +153,15 @@
|
||||
<br/>
|
||||
<p align="center">Jellyseerr</p>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
</td></tr>
|
||||
<tr><td align="center">
|
||||
<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" />
|
||||
<br/>
|
||||
<p align="center">Lidarr</p>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr><td align="center">
|
||||
</td>
|
||||
<td align="center">
|
||||
<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" />
|
||||
<br/>
|
||||
@@ -193,15 +202,15 @@
|
||||
<br/>
|
||||
<p align="center">OpenMediaVault</p>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
</td></tr>
|
||||
<tr><td align="center">
|
||||
<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" />
|
||||
<br/>
|
||||
<p align="center">OPNsense</p>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr><td align="center">
|
||||
</td>
|
||||
<td align="center">
|
||||
<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" />
|
||||
<br/>
|
||||
@@ -242,15 +251,15 @@
|
||||
<br/>
|
||||
<p align="center">qBittorrent</p>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
</td></tr>
|
||||
<tr><td align="center">
|
||||
<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" />
|
||||
<br/>
|
||||
<p align="center">Quay</p>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr><td align="center">
|
||||
</td>
|
||||
<td align="center">
|
||||
<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" />
|
||||
<br/>
|
||||
@@ -291,6 +300,13 @@
|
||||
<br/>
|
||||
<p align="center">Transmission</p>
|
||||
</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 align="center">
|
||||
<a href="https://homarr.dev/docs/integrations/unifi-controller" target="_blank" rel="noreferrer noopener">
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 313 KiB |
+16
-14
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "1.36.1",
|
||||
"version": "1.38.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env CI=true turbo build",
|
||||
@@ -39,9 +39,9 @@
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/commit-analyzer": "^13.0.1",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/github": "^11.0.5",
|
||||
"@semantic-release/github": "^11.0.6",
|
||||
"@semantic-release/npm": "^12.0.2",
|
||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||
"@semantic-release/release-notes-generator": "^14.1.0",
|
||||
"@testcontainers/redis": "^11.5.1",
|
||||
"@turbo/gen": "^2.5.6",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
@@ -49,16 +49,16 @@
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
||||
"cross-env": "^10.0.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"jsdom": "^27.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"semantic-release": "^24.2.7",
|
||||
"semantic-release": "^24.2.8",
|
||||
"testcontainers": "^11.5.1",
|
||||
"turbo": "^2.5.6",
|
||||
"typescript": "^5.9.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"packageManager": "pnpm@10.15.0",
|
||||
"packageManager": "pnpm@10.16.1",
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
},
|
||||
@@ -75,27 +75,29 @@
|
||||
"tree-sitter-json"
|
||||
],
|
||||
"overrides": {
|
||||
"@babel/helpers@<7.26.10": ">=7.28.3",
|
||||
"@babel/runtime@<7.26.10": ">=7.28.3",
|
||||
"axios@>=1.0.0 <1.8.2": ">=1.11.0",
|
||||
"@babel/helpers@<7.26.10": ">=7.28.4",
|
||||
"@babel/runtime@<7.26.10": ">=7.28.4",
|
||||
"axios@>=1.0.0 <1.8.2": ">=1.12.2",
|
||||
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
|
||||
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
|
||||
"esbuild@<=0.24.2": ">=0.25.9",
|
||||
"form-data@>=4.0.0 <4.0.4": ">=4.0.4",
|
||||
"hono@<4.6.5": ">=4.9.6",
|
||||
"hono@<4.6.5": ">=4.9.7",
|
||||
"linkifyjs@<4.3.2": ">=4.3.2",
|
||||
"nanoid@>=4.0.0 <5.0.9": ">=5.1.5",
|
||||
"prismjs@<1.30.0": ">=1.30.0",
|
||||
"proxmox-api>undici": "7.15.0",
|
||||
"rollup@>=4.0.0 <4.22.4": ">=4.50.0",
|
||||
"proxmox-api>undici": "7.16.0",
|
||||
"react-is": "^19.1.1",
|
||||
"rollup@>=4.0.0 <4.22.4": ">=4.50.1",
|
||||
"sha.js@<=2.4.11": ">=2.4.12",
|
||||
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.0",
|
||||
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.0",
|
||||
"tmp@<=0.2.3": ">=0.2.5",
|
||||
"vite@>=5.0.0 <=5.4.18": ">=7.1.3"
|
||||
"vite@>=5.0.0 <=5.4.18": ">=7.1.5"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@types/node-unifi": "patches/@types__node-unifi.patch"
|
||||
"@types/node-unifi": "patches/@types__node-unifi.patch",
|
||||
"trpc-to-openapi": "patches/trpc-to-openapi.patch"
|
||||
},
|
||||
"allowUnusedPatches": true,
|
||||
"ignoredBuiltDependencies": [
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -42,24 +42,24 @@
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@kubernetes/client-node": "^1.3.0",
|
||||
"@tanstack/react-query": "^5.87.1",
|
||||
"@trpc/client": "^11.5.0",
|
||||
"@trpc/react-query": "^11.5.0",
|
||||
"@trpc/server": "^11.5.0",
|
||||
"@trpc/tanstack-react-query": "^11.5.0",
|
||||
"@tanstack/react-query": "^5.87.4",
|
||||
"@trpc/client": "^11.5.1",
|
||||
"@trpc/react-query": "^11.5.1",
|
||||
"@trpc/server": "^11.5.1",
|
||||
"@trpc/tanstack-react-query": "^11.5.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"next": "15.5.2",
|
||||
"next": "15.5.3",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"superjson": "2.2.2",
|
||||
"trpc-to-openapi": "^3.0.1",
|
||||
"zod": "^4.1.5"
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
|
||||
@@ -25,16 +25,6 @@ export const testConnectionAsync = async (
|
||||
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
|
||||
.map((secret) => {
|
||||
try {
|
||||
@@ -55,6 +45,15 @@ export const testConnectionAsync = async (
|
||||
})
|
||||
.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 secretKinds = getSecretKindOption(integration.kind, sourcedSecrets);
|
||||
|
||||
@@ -89,10 +88,10 @@ export const testConnectionAsync = async (
|
||||
return result;
|
||||
};
|
||||
|
||||
interface SourcedIntegrationSecret {
|
||||
interface SourcedIntegrationSecret<TSource extends string = "db" | "form"> {
|
||||
kind: IntegrationSecretKind;
|
||||
value: string;
|
||||
source: "db" | "form";
|
||||
source: TSource;
|
||||
}
|
||||
|
||||
const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedIntegrationSecret[]) => {
|
||||
@@ -111,7 +110,9 @@ const getSecretKindOption = (kind: IntegrationKind, sourcedSecrets: SourcedInteg
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -265,4 +265,77 @@ describe("testConnectionAsync should run test connection of integration", () =>
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
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",
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const healthMonitoringRouter = createTRPCRouter({
|
||||
getSystemHealthStatus: publicProcedure
|
||||
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "mock"))
|
||||
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "mock"))
|
||||
.query(async ({ ctx }) => {
|
||||
return await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
@@ -26,7 +26,7 @@ export const healthMonitoringRouter = createTRPCRouter({
|
||||
);
|
||||
}),
|
||||
subscribeSystemHealthStatus: publicProcedure
|
||||
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "mock"))
|
||||
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "mock"))
|
||||
.subscription(({ ctx }) => {
|
||||
return observable<{ integrationId: string; healthInfo: SystemHealthMonitoring; timestamp: Date }>((emit) => {
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
|
||||
@@ -35,11 +35,11 @@
|
||||
"bcrypt": "^6.0.0",
|
||||
"cookies": "^0.9.1",
|
||||
"ldapts": "8.0.9",
|
||||
"next": "15.5.2",
|
||||
"next": "15.5.3",
|
||||
"next-auth": "5.0.0-beta.29",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"zod": "^4.1.5"
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
@@ -47,7 +47,7 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/bcrypt": "6.0.0",
|
||||
"@types/cookies": "0.9.1",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -24,13 +24,13 @@
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"undici": "7.15.0"
|
||||
"undici": "7.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -28,14 +28,14 @@
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"dotenv": "^17.2.1"
|
||||
"dotenv": "^17.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"esbuild": "^0.25.9",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -32,18 +32,19 @@
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"dayjs": "^1.11.18",
|
||||
"dns-caching": "^0.2.5",
|
||||
"next": "15.5.2",
|
||||
"next": "15.5.3",
|
||||
"octokit": "^5.0.3",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"undici": "7.15.0",
|
||||
"zod": "^4.1.5",
|
||||
"undici": "7.16.0",
|
||||
"zod": "^4.1.8",
|
||||
"zod-validation-error": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export * from "./fetch-http-error-handler";
|
||||
export * from "./ofetch-http-error-handler";
|
||||
export * from "./axios-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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,11 @@ export class LoggingAgent extends Agent {
|
||||
}
|
||||
|
||||
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
|
||||
// some integrations use query parameters for auth
|
||||
|
||||
@@ -66,6 +66,7 @@ describe("LoggingAgent should log all requests", () => {
|
||||
["/?one=a1&two=b2&three=c3", `/?one=${REDACTED}&two=${REDACTED}&three=${REDACTED}`],
|
||||
["/?numberWith13Chars=1234567890123", `/?numberWith13Chars=${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) => {
|
||||
// Arrange
|
||||
const infoLogSpy = vi.spyOn(logger, "debug");
|
||||
|
||||
@@ -26,13 +26,13 @@
|
||||
"dependencies": {
|
||||
"@t3-oss/env-nextjs": "^0.13.8",
|
||||
"ioredis": "5.7.0",
|
||||
"zod": "^4.1.5"
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export const createRedisClient = () =>
|
||||
...defaultRedisOptions,
|
||||
host: redisEnv.HOST,
|
||||
port: redisEnv.PORT,
|
||||
db: redisEnv.DATABASE_INDEX,
|
||||
tls: redisEnv.TLS_CA
|
||||
? {
|
||||
ca: redisEnv.TLS_CA,
|
||||
|
||||
@@ -12,6 +12,7 @@ export const redisEnv = createEnv({
|
||||
TLS_CA: z.string().optional(),
|
||||
USERNAME: z.string().optional(),
|
||||
PASSWORD: z.string().optional(),
|
||||
DATABASE_INDEX: z.coerce.number().optional(),
|
||||
},
|
||||
runtimeEnv: runtimeEnvWithPrefix("REDIS_"),
|
||||
});
|
||||
@@ -29,21 +29,21 @@
|
||||
"@homarr/core": "workspace:^0.1.0",
|
||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@tanstack/react-query": "^5.87.1",
|
||||
"@trpc/client": "^11.5.0",
|
||||
"@trpc/server": "^11.5.0",
|
||||
"@trpc/tanstack-react-query": "^11.5.0",
|
||||
"@tanstack/react-query": "^5.87.4",
|
||||
"@trpc/client": "^11.5.1",
|
||||
"@trpc/server": "^11.5.1",
|
||||
"@trpc/tanstack-react-query": "^11.5.1",
|
||||
"node-cron": "^4.2.1",
|
||||
"react": "19.1.1",
|
||||
"zod": "^4.1.5"
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/react": "19.1.12",
|
||||
"eslint": "^9.34.0",
|
||||
"@types/react": "19.1.13",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -72,8 +72,6 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
|
||||
where: (cronJobConfigurations, { eq }) => eq(cronJobConfigurations.name, name),
|
||||
});
|
||||
|
||||
if (defaultCronExpression === "never") return null;
|
||||
|
||||
const scheduledTask = createTask(
|
||||
configuration?.cronExpression ?? defaultCronExpression,
|
||||
() => void catchingCallbackAsync(),
|
||||
@@ -120,7 +118,7 @@ export const createCronJobCreator = <TAllowedNames extends string = string>(
|
||||
options: CreateCronJobOptions = { runOnStart: false },
|
||||
) => {
|
||||
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}'`);
|
||||
}
|
||||
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
|
||||
return returnValue as unknown as ValidateCron<TExpression> extends true
|
||||
? typeof returnValue
|
||||
: TExpression extends "never"
|
||||
? typeof returnValue
|
||||
: "Invalid cron expression";
|
||||
: "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_DAY = 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();
|
||||
if (!scheduledTask) continue;
|
||||
|
||||
tasks.set(job.name, scheduledTask);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import { refreshNotificationsJob } from "./jobs/integrations/notifications";
|
||||
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
|
||||
import { pingJob } from "./jobs/ping";
|
||||
import { rssFeedsJob } from "./jobs/rss-feeds";
|
||||
import { sessionCleanupJob } from "./jobs/session-cleanup";
|
||||
import { updateCheckerJob } from "./jobs/update-checker";
|
||||
import { createCronJobGroup } from "./lib";
|
||||
|
||||
@@ -39,7 +38,6 @@ export const jobGroup = createCronJobGroup({
|
||||
rssFeeds: rssFeedsJob,
|
||||
indexerManager: indexerManagerJob,
|
||||
healthMonitoring: healthMonitoringJob,
|
||||
sessionCleanup: sessionCleanupJob,
|
||||
updateChecker: updateCheckerJob,
|
||||
mediaTranscoding: mediaTranscodingJob,
|
||||
minecraftServerStatus: minecraftServerStatusJob,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
@@ -49,16 +49,16 @@
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.2.8",
|
||||
"@mantine/core": "^8.3.1",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@testcontainers/mysql": "^11.5.1",
|
||||
"@testcontainers/postgresql": "^11.5.1",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"dotenv": "^17.2.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"drizzle-zod": "^0.8.3",
|
||||
"mysql2": "3.14.3",
|
||||
"mysql2": "3.14.5",
|
||||
"pg": "^8.16.3",
|
||||
"superjson": "2.2.2"
|
||||
},
|
||||
@@ -70,7 +70,7 @@
|
||||
"@types/pg": "^8.15.5",
|
||||
"dotenv-cli": "^10.0.0",
|
||||
"esbuild": "^0.25.9",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tsx": "4.20.4",
|
||||
"typescript": "^5.9.2"
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"fast-xml-parser": "^5.2.5",
|
||||
"zod": "^4.1.5"
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"tsx": "4.20.4",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
|
||||
@@ -183,6 +183,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/integrations/sonarr"
|
||||
| "/docs/integrations/tdarr"
|
||||
| "/docs/integrations/transmission"
|
||||
| "/docs/integrations/truenas"
|
||||
| "/docs/integrations/unifi-controller"
|
||||
| "/docs/management/api"
|
||||
| "/docs/management/apps"
|
||||
|
||||
@@ -4,16 +4,20 @@ import type { AtLeastOneOf } from "@homarr/common/types";
|
||||
import { createDocumentationLink } from "./docs";
|
||||
|
||||
export const integrationSecretKindObject = {
|
||||
apiKey: { isPublic: false },
|
||||
username: { isPublic: true },
|
||||
password: { isPublic: false },
|
||||
tokenId: { isPublic: true },
|
||||
realm: { isPublic: true },
|
||||
personalAccessToken: { isPublic: false },
|
||||
topic: { isPublic: true },
|
||||
opnsenseApiKey: { isPublic: false },
|
||||
opnsenseApiSecret: { isPublic: false },
|
||||
} satisfies Record<string, { isPublic: boolean }>;
|
||||
apiKey: { isPublic: false, multiline: false },
|
||||
username: { isPublic: true, multiline: false },
|
||||
password: { isPublic: false, multiline: false },
|
||||
tokenId: { isPublic: true, multiline: false },
|
||||
realm: { isPublic: true, multiline: false },
|
||||
personalAccessToken: { isPublic: false, multiline: false },
|
||||
topic: { isPublic: true, multiline: false },
|
||||
opnsenseApiKey: { isPublic: false, multiline: false },
|
||||
opnsenseApiSecret: { isPublic: false, multiline: false },
|
||||
url: { isPublic: false, multiline: false },
|
||||
privateKey: { isPublic: false, multiline: true },
|
||||
githubAppId: { isPublic: true, multiline: false },
|
||||
githubInstallationId: { isPublic: true, multiline: false },
|
||||
} satisfies Record<string, { isPublic: boolean; multiline: boolean }>;
|
||||
|
||||
export const integrationSecretKinds = objectKeys(integrationSecretKindObject);
|
||||
|
||||
@@ -211,7 +215,7 @@ export const integrationDefs = {
|
||||
},
|
||||
github: {
|
||||
name: "Github",
|
||||
secretKinds: [[], ["personalAccessToken"]],
|
||||
secretKinds: [[], ["personalAccessToken"], ["githubAppId", "githubInstallationId", "privateKey"]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg",
|
||||
category: ["releasesProvider"],
|
||||
defaultUrl: "https://api.github.com",
|
||||
@@ -259,7 +263,7 @@ export const integrationDefs = {
|
||||
},
|
||||
gitHubContainerRegistry: {
|
||||
name: "GitHub Container Registry",
|
||||
secretKinds: [[], ["personalAccessToken"]],
|
||||
secretKinds: [[], ["personalAccessToken"], ["githubAppId", "githubInstallationId", "privateKey"]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg",
|
||||
category: ["releasesProvider"],
|
||||
defaultUrl: "https://api.github.com",
|
||||
@@ -280,6 +284,20 @@ export const integrationDefs = {
|
||||
category: ["notifications"],
|
||||
documentationUrl: createDocumentationLink("/docs/integrations/ntfy"),
|
||||
},
|
||||
ical: {
|
||||
name: "iCal",
|
||||
secretKinds: [["url"]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/ical.svg",
|
||||
category: ["calendar"],
|
||||
documentationUrl: createDocumentationLink("/docs/integrations/ical"),
|
||||
},
|
||||
truenas: {
|
||||
name: "TrueNAS",
|
||||
secretKinds: [["username", "password"]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/truenas.svg",
|
||||
category: ["healthMonitoring"],
|
||||
documentationUrl: createDocumentationLink("/docs/integrations/truenas"),
|
||||
},
|
||||
// This integration only returns mock data, it is used during development (but can also be used in production by directly going to the create page)
|
||||
mock: {
|
||||
name: "Mock",
|
||||
|
||||
@@ -26,14 +26,14 @@
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/core": "workspace:^0.1.0",
|
||||
"dockerode": "^4.0.7"
|
||||
"dockerode": "^4.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/dockerode": "^3.3.43",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -26,15 +26,15 @@
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/form": "^8.2.8",
|
||||
"@mantine/form": "^8.3.1",
|
||||
"mantine-form-zod-resolver": "^1.3.0",
|
||||
"zod": "^4.1.5"
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -29,15 +29,15 @@
|
||||
"@homarr/notifications": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.2.8",
|
||||
"@mantine/core": "^8.3.1",
|
||||
"react": "19.1.1",
|
||||
"zod": "^4.1.5"
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/bcrypt": "6.0.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,10 @@
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@ctrl/deluge": "^7.1.1",
|
||||
"@ctrl/qbittorrent": "^9.6.0",
|
||||
"@ctrl/transmission": "^7.2.0",
|
||||
"@gitbeaker/rest": "^43.4.0",
|
||||
"@ctrl/deluge": "^7.2.0",
|
||||
"@ctrl/qbittorrent": "^9.7.0",
|
||||
"@ctrl/transmission": "^7.3.0",
|
||||
"@gitbeaker/rest": "^43.5.0",
|
||||
"@homarr/certificates": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
@@ -40,14 +40,16 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@jellyfin/sdk": "^0.11.0",
|
||||
"@octokit/auth-app": "^8.1.0",
|
||||
"ical.js": "^2.2.1",
|
||||
"maria2": "^0.4.1",
|
||||
"node-ical": "^0.20.1",
|
||||
"octokit": "^5.0.3",
|
||||
"proxmox-api": "1.1.1",
|
||||
"tsdav": "^2.1.5",
|
||||
"undici": "7.15.0",
|
||||
"undici": "7.16.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"zod": "^4.1.5"
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
@@ -55,7 +57,7 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-unifi": "^2.5.1",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ export const statusResponseSchema = z.object({
|
||||
dns_addresses: z.array(z.string()),
|
||||
dns_port: z.number().positive(),
|
||||
http_port: z.number().positive(),
|
||||
protection_disabled_duration: z.number(),
|
||||
protection_enabled: z.boolean(),
|
||||
dhcp_available: z.boolean(),
|
||||
running: z.boolean(),
|
||||
|
||||
@@ -18,6 +18,7 @@ import { GitHubContainerRegistryIntegration } from "../github-container-registry
|
||||
import { GithubIntegration } from "../github/github-integration";
|
||||
import { GitlabIntegration } from "../gitlab/gitlab-integration";
|
||||
import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration";
|
||||
import { ICalIntegration } from "../ical/ical-integration";
|
||||
import { JellyfinIntegration } from "../jellyfin/jellyfin-integration";
|
||||
import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";
|
||||
import { LinuxServerIOIntegration } from "../linuxserverio/linuxserverio-integration";
|
||||
@@ -38,6 +39,7 @@ import { PlexIntegration } from "../plex/plex-integration";
|
||||
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
|
||||
import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
|
||||
import { QuayIntegration } from "../quay/quay-integration";
|
||||
import { TrueNasIntegration } from "../truenas/truenas-integration";
|
||||
import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration";
|
||||
import type { Integration, IntegrationInput } from "./integration";
|
||||
|
||||
@@ -111,9 +113,11 @@ export const integrationCreators = {
|
||||
codeberg: CodebergIntegration,
|
||||
linuxServerIO: LinuxServerIOIntegration,
|
||||
gitHubContainerRegistry: GitHubContainerRegistryIntegration,
|
||||
ical: ICalIntegration,
|
||||
quay: QuayIntegration,
|
||||
ntfy: NTFYIntegration,
|
||||
mock: MockIntegration,
|
||||
truenas: TrueNasIntegration,
|
||||
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
|
||||
|
||||
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
AxiosHttpErrorHandler,
|
||||
FetchHttpErrorHandler,
|
||||
OctokitHttpErrorHandler,
|
||||
OFetchHttpErrorHandler,
|
||||
TsdavHttpErrorHandler,
|
||||
} from "@homarr/common/server";
|
||||
@@ -11,3 +12,4 @@ export const integrationFetchHttpErrorHandler = new IntegrationHttpErrorHandler(
|
||||
export const integrationOFetchHttpErrorHandler = new IntegrationHttpErrorHandler(new OFetchHttpErrorHandler());
|
||||
export const integrationAxiosHttpErrorHandler = new IntegrationHttpErrorHandler(new AxiosHttpErrorHandler());
|
||||
export const integrationTsdavHttpErrorHandler = new IntegrationHttpErrorHandler(new TsdavHttpErrorHandler());
|
||||
export const integrationOctokitHttpErrorHandler = new IntegrationHttpErrorHandler(new OctokitHttpErrorHandler());
|
||||
+41
-17
@@ -1,11 +1,14 @@
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { Octokit, RequestError } from "octokit";
|
||||
import type { fetch } from "undici";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { HandleIntegrationErrors } from "../base/errors/decorator";
|
||||
import { integrationOctokitHttpErrorHandler } from "../base/errors/http";
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
@@ -18,23 +21,21 @@ import type {
|
||||
|
||||
const localLogger = logger.child({ module: "GitHubContainerRegistryIntegration" });
|
||||
|
||||
@HandleIntegrationErrors([integrationOctokitHttpErrorHandler])
|
||||
export class GitHubContainerRegistryIntegration extends Integration implements ReleasesProviderIntegration {
|
||||
private static readonly userAgent = "Homarr-Lab/Homarr:GitHubContainerRegistryIntegration";
|
||||
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const headers: RequestInit["headers"] = {
|
||||
"User-Agent": GitHubContainerRegistryIntegration.userAgent,
|
||||
};
|
||||
const api = this.getApi(input.fetchAsync);
|
||||
|
||||
if (this.hasSecretValue("personalAccessToken"))
|
||||
headers.Authorization = `Bearer ${this.getSecretValue("personalAccessToken")}`;
|
||||
|
||||
const response = await input.fetchAsync(this.url("/octocat"), {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return TestConnectionError.StatusResult(response);
|
||||
if (this.hasSecretValue("personalAccessToken")) {
|
||||
await api.rest.users.getAuthenticated();
|
||||
} else if (this.hasSecretValue("githubAppId")) {
|
||||
await api.rest.apps.getInstallation({
|
||||
installation_id: Number(this.getSecretValue("githubInstallationId")),
|
||||
});
|
||||
} else {
|
||||
await api.request("GET /octocat");
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -131,15 +132,38 @@ export class GitHubContainerRegistryIntegration extends Integration implements R
|
||||
}
|
||||
}
|
||||
|
||||
private getApi() {
|
||||
private getAuthProperties(): Pick<OctokitOptions, "auth" | "authStrategy"> {
|
||||
if (this.hasSecretValue("personalAccessToken"))
|
||||
return {
|
||||
auth: this.getSecretValue("personalAccessToken"),
|
||||
};
|
||||
|
||||
if (this.hasSecretValue("githubAppId"))
|
||||
return {
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: this.getSecretValue("githubAppId"),
|
||||
installationId: this.getSecretValue("githubInstallationId"),
|
||||
privateKey: this.getSecretValue("privateKey"),
|
||||
} satisfies Parameters<typeof createAppAuth>[0],
|
||||
};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private getApi(customFetch?: typeof fetch) {
|
||||
return new Octokit({
|
||||
baseUrl: this.url("/").origin,
|
||||
request: {
|
||||
fetch: fetchWithTrustedCertificatesAsync,
|
||||
fetch: customFetch ?? fetchWithTrustedCertificatesAsync,
|
||||
},
|
||||
userAgent: GitHubContainerRegistryIntegration.userAgent,
|
||||
throttle: { enabled: false }, // Disable throttling for this integration, Octokit will retry by default after a set time, thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request.
|
||||
...(this.hasSecretValue("personalAccessToken") ? { auth: this.getSecretValue("personalAccessToken") } : {}),
|
||||
// Disable throttling for this integration, Octokit will retry by default after a set time,
|
||||
// thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request.
|
||||
throttle: { enabled: false },
|
||||
...this.getAuthProperties(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type OctokitOptions = Exclude<ConstructorParameters<typeof Octokit>[0], undefined>;
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Octokit, RequestError } from "octokit";
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { Octokit, RequestError as OctokitRequestError } from "octokit";
|
||||
import type { fetch } from "undici";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { HandleIntegrationErrors } from "../base/errors/decorator";
|
||||
import { integrationOctokitHttpErrorHandler } from "../base/errors/http";
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
@@ -18,23 +21,21 @@ import type {
|
||||
|
||||
const localLogger = logger.child({ module: "GithubIntegration" });
|
||||
|
||||
@HandleIntegrationErrors([integrationOctokitHttpErrorHandler])
|
||||
export class GithubIntegration extends Integration implements ReleasesProviderIntegration {
|
||||
private static readonly userAgent = "Homarr-Lab/Homarr:GithubIntegration";
|
||||
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const headers: RequestInit["headers"] = {
|
||||
"User-Agent": GithubIntegration.userAgent,
|
||||
};
|
||||
const api = this.getApi(input.fetchAsync);
|
||||
|
||||
if (this.hasSecretValue("personalAccessToken"))
|
||||
headers.Authorization = `Bearer ${this.getSecretValue("personalAccessToken")}`;
|
||||
|
||||
const response = await input.fetchAsync(this.url("/octocat"), {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return TestConnectionError.StatusResult(response);
|
||||
if (this.hasSecretValue("personalAccessToken")) {
|
||||
await api.rest.users.getAuthenticated();
|
||||
} else if (this.hasSecretValue("githubAppId")) {
|
||||
await api.rest.apps.getInstallation({
|
||||
installation_id: Number(this.getSecretValue("githubInstallationId")),
|
||||
});
|
||||
} else {
|
||||
await api.request("GET /octocat");
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -91,7 +92,7 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
|
||||
|
||||
return getLatestRelease(releasesProviderResponse, repository, details);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof RequestError ? error.message : String(error);
|
||||
const errorMessage = error instanceof OctokitRequestError ? error.message : String(error);
|
||||
|
||||
localLogger.warn(`Failed to get releases for ${owner}\\${name} with Github integration`, {
|
||||
owner,
|
||||
@@ -131,21 +132,44 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
|
||||
localLogger.warn(`Failed to get details for ${owner}\\${name} with Github integration`, {
|
||||
owner,
|
||||
name,
|
||||
error: error instanceof RequestError ? error.message : String(error),
|
||||
error: error instanceof OctokitRequestError ? error.message : String(error),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getApi() {
|
||||
private getAuthProperties(): Pick<OctokitOptions, "auth" | "authStrategy"> {
|
||||
if (this.hasSecretValue("personalAccessToken"))
|
||||
return {
|
||||
auth: this.getSecretValue("personalAccessToken"),
|
||||
};
|
||||
|
||||
if (this.hasSecretValue("githubAppId"))
|
||||
return {
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: this.getSecretValue("githubAppId"),
|
||||
installationId: this.getSecretValue("githubInstallationId"),
|
||||
privateKey: this.getSecretValue("privateKey"),
|
||||
} satisfies Parameters<typeof createAppAuth>[0],
|
||||
};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private getApi(customFetch?: typeof fetch) {
|
||||
return new Octokit({
|
||||
baseUrl: this.url("/").origin,
|
||||
request: {
|
||||
fetch: fetchWithTrustedCertificatesAsync,
|
||||
fetch: customFetch ?? fetchWithTrustedCertificatesAsync,
|
||||
},
|
||||
userAgent: GithubIntegration.userAgent,
|
||||
throttle: { enabled: false }, // Disable throttling for this integration, Octokit will retry by default after a set time, thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request.
|
||||
...(this.hasSecretValue("personalAccessToken") ? { auth: this.getSecretValue("personalAccessToken") } : {}),
|
||||
// Disable throttling for this integration, Octokit will retry by default after a set time,
|
||||
// thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request.
|
||||
throttle: { enabled: false },
|
||||
...this.getAuthProperties(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type OctokitOptions = Exclude<ConstructorParameters<typeof Octokit>[0], undefined>;
|
||||
@@ -0,0 +1,67 @@
|
||||
import ICAL from "ical.js";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { ICalendarIntegration } from "../interfaces/calendar/calendar-integration";
|
||||
import type { CalendarEvent } from "../interfaces/calendar/calendar-types";
|
||||
|
||||
export class ICalIntegration extends Integration implements ICalendarIntegration {
|
||||
async getCalendarEventsAsync(start: Date, end: Date): Promise<CalendarEvent[]> {
|
||||
const response = await fetchWithTrustedCertificatesAsync(super.getSecretValue("url"));
|
||||
const result = await response.text();
|
||||
const jcal = ICAL.parse(result) as unknown[];
|
||||
const comp = new ICAL.Component(jcal);
|
||||
|
||||
return comp.getAllSubcomponents("vevent").reduce((prev, vevent) => {
|
||||
const event = new ICAL.Event(vevent);
|
||||
const startDate = event.startDate.toJSDate();
|
||||
const endDate = event.endDate.toJSDate();
|
||||
|
||||
if (startDate > end) return prev;
|
||||
if (endDate < start) return prev;
|
||||
|
||||
return prev.concat({
|
||||
title: event.summary,
|
||||
subTitle: null,
|
||||
description: event.description,
|
||||
startDate,
|
||||
endDate,
|
||||
image: null,
|
||||
location: event.location,
|
||||
indicatorColor: "red",
|
||||
links: [],
|
||||
});
|
||||
}, [] as CalendarEvent[]);
|
||||
}
|
||||
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const response = await input.fetchAsync(super.getSecretValue("url"));
|
||||
if (!response.ok) return TestConnectionError.StatusResult(response);
|
||||
|
||||
const result = await response.text();
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const jcal = ICAL.parse(result);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const comp = new ICAL.Component(jcal);
|
||||
return comp.getAllSubcomponents("vevent").length > 0
|
||||
? { success: true }
|
||||
: TestConnectionError.ParseResult({
|
||||
name: "Calendar parse error",
|
||||
message: "No events found",
|
||||
cause: new Error("No events found"),
|
||||
});
|
||||
} catch (error) {
|
||||
return TestConnectionError.ParseResult({
|
||||
name: "Calendar parse error",
|
||||
message: "Failed to parse calendar",
|
||||
cause: error as Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,9 @@ export { PiHoleIntegrationV5 } from "./pi-hole/v5/pi-hole-integration-v5";
|
||||
export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6";
|
||||
export { PlexIntegration } from "./plex/plex-integration";
|
||||
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
|
||||
export { TrueNasIntegration } from "./truenas/truenas-integration";
|
||||
export { OPNsenseIntegration } from "./opnsense/opnsense-integration";
|
||||
export { ICalIntegration } from "./ical/ical-integration";
|
||||
|
||||
// Types
|
||||
export type { IntegrationInput } from "./base/integration";
|
||||
|
||||
@@ -1,24 +1,41 @@
|
||||
export const radarrReleaseTypes = ["inCinemas", "digitalRelease", "physicalRelease"] as const;
|
||||
export type RadarrReleaseType = (typeof radarrReleaseTypes)[number];
|
||||
|
||||
export interface CalendarEvent {
|
||||
name: string;
|
||||
subName: string;
|
||||
date: Date;
|
||||
dates?: { type: RadarrReleaseType; date: Date }[];
|
||||
description?: string;
|
||||
thumbnail?: string;
|
||||
mediaInformation?: {
|
||||
type: "audio" | "video" | "tv" | "movie";
|
||||
seasonNumber?: number;
|
||||
episodeNumber?: number;
|
||||
};
|
||||
links: {
|
||||
href: string;
|
||||
name: string;
|
||||
color: string | undefined;
|
||||
notificationColor?: string | undefined;
|
||||
isDark: boolean | undefined;
|
||||
logo: string;
|
||||
}[];
|
||||
export interface RadarrMetadata {
|
||||
type: "radarr";
|
||||
releaseType: RadarrReleaseType;
|
||||
}
|
||||
|
||||
export type CalendarMetadata = RadarrMetadata;
|
||||
|
||||
export interface CalendarLink {
|
||||
name: string;
|
||||
isDark: boolean;
|
||||
href: string;
|
||||
color?: string;
|
||||
logo?: string;
|
||||
}
|
||||
|
||||
export interface CalendarImageBadge {
|
||||
content: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface CalendarImage {
|
||||
src: string;
|
||||
badge?: CalendarImageBadge;
|
||||
aspectRatio?: { width: number; height: number };
|
||||
}
|
||||
|
||||
export interface CalendarEvent {
|
||||
title: string;
|
||||
subTitle: string | null;
|
||||
description: string | null;
|
||||
startDate: Date;
|
||||
endDate: Date | null;
|
||||
image: CalendarImage | null;
|
||||
location: string | null;
|
||||
metadata?: CalendarMetadata;
|
||||
indicatorColor: string;
|
||||
links: CalendarLink[];
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export interface SystemHealthMonitoring {
|
||||
"1min": number;
|
||||
"5min": number;
|
||||
"15min": number;
|
||||
};
|
||||
} | null;
|
||||
rebootRequired: boolean;
|
||||
availablePkgUpdates: number;
|
||||
cpuTemp: number | undefined;
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { IntegrationTestingInput } from "../../base/integration";
|
||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||
import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration";
|
||||
import type { CalendarEvent } from "../../interfaces/calendar/calendar-types";
|
||||
import type { CalendarEvent, CalendarLink } from "../../interfaces/calendar/calendar-types";
|
||||
import { mediaOrganizerPriorities } from "../media-organizer";
|
||||
|
||||
export class LidarrIntegration extends Integration implements ICalendarIntegration {
|
||||
@@ -44,22 +44,28 @@ export class LidarrIntegration extends Integration implements ICalendarIntegrati
|
||||
const lidarrCalendarEvents = await z.array(lidarrCalendarEventSchema).parseAsync(await response.json());
|
||||
|
||||
return lidarrCalendarEvents.map((lidarrCalendarEvent): CalendarEvent => {
|
||||
const imageSrc = this.chooseBestImage(lidarrCalendarEvent);
|
||||
return {
|
||||
name: lidarrCalendarEvent.title,
|
||||
subName: lidarrCalendarEvent.artist.artistName,
|
||||
description: lidarrCalendarEvent.overview,
|
||||
thumbnail: this.chooseBestImageAsURL(lidarrCalendarEvent),
|
||||
date: lidarrCalendarEvent.releaseDate,
|
||||
mediaInformation: {
|
||||
type: "audio",
|
||||
},
|
||||
title: lidarrCalendarEvent.title,
|
||||
subTitle: lidarrCalendarEvent.artist.artistName,
|
||||
description: lidarrCalendarEvent.overview ?? null,
|
||||
startDate: lidarrCalendarEvent.releaseDate,
|
||||
endDate: null,
|
||||
image: imageSrc
|
||||
? {
|
||||
src: imageSrc.remoteUrl,
|
||||
aspectRatio: { width: 7, height: 12 },
|
||||
}
|
||||
: null,
|
||||
location: null,
|
||||
indicatorColor: "cyan",
|
||||
links: this.getLinksForLidarrCalendarEvent(lidarrCalendarEvent),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getLinksForLidarrCalendarEvent = (event: z.infer<typeof lidarrCalendarEventSchema>) => {
|
||||
const links: CalendarEvent["links"] = [];
|
||||
const links: CalendarLink[] = [];
|
||||
|
||||
for (const link of event.artist.links) {
|
||||
switch (link.name) {
|
||||
@@ -70,7 +76,6 @@ export class LidarrIntegration extends Integration implements ICalendarIntegrati
|
||||
color: "#f5c518",
|
||||
isDark: false,
|
||||
logo: "/images/apps/vgmdb.svg",
|
||||
notificationColor: "cyan",
|
||||
});
|
||||
break;
|
||||
case "imdb":
|
||||
@@ -80,7 +85,6 @@ export class LidarrIntegration extends Integration implements ICalendarIntegrati
|
||||
color: "#f5c518",
|
||||
isDark: false,
|
||||
logo: "/images/apps/imdb.png",
|
||||
notificationColor: "cyan",
|
||||
});
|
||||
break;
|
||||
case "last":
|
||||
@@ -90,7 +94,6 @@ export class LidarrIntegration extends Integration implements ICalendarIntegrati
|
||||
color: "#cf222a",
|
||||
isDark: false,
|
||||
logo: "/images/apps/lastfm.svg",
|
||||
notificationColor: "cyan",
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import type { AtLeastOneOf } from "@homarr/common/types";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { Integration } from "../../base/integration";
|
||||
import type { IntegrationTestingInput } from "../../base/integration";
|
||||
import { Integration } from "../../base/integration";
|
||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||
import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration";
|
||||
import type { CalendarEvent } from "../../interfaces/calendar/calendar-types";
|
||||
import type { CalendarEvent, CalendarLink } from "../../interfaces/calendar/calendar-types";
|
||||
import { radarrReleaseTypes } from "../../interfaces/calendar/calendar-types";
|
||||
import { mediaOrganizerPriorities } from "../media-organizer";
|
||||
|
||||
@@ -34,33 +33,44 @@ export class RadarrIntegration extends Integration implements ICalendarIntegrati
|
||||
});
|
||||
const radarrCalendarEvents = await z.array(radarrCalendarEventSchema).parseAsync(await response.json());
|
||||
|
||||
return radarrCalendarEvents.map((radarrCalendarEvent): CalendarEvent => {
|
||||
const dates = radarrReleaseTypes
|
||||
.map((type) => (radarrCalendarEvent[type] ? { type, date: radarrCalendarEvent[type] } : undefined))
|
||||
.filter((date) => date) as AtLeastOneOf<Exclude<CalendarEvent["dates"], undefined>[number]>;
|
||||
return {
|
||||
name: radarrCalendarEvent.title,
|
||||
subName: radarrCalendarEvent.originalTitle,
|
||||
description: radarrCalendarEvent.overview,
|
||||
thumbnail: this.chooseBestImageAsURL(radarrCalendarEvent),
|
||||
date: dates[0].date,
|
||||
dates,
|
||||
mediaInformation: {
|
||||
type: "movie",
|
||||
},
|
||||
links: this.getLinksForRadarrCalendarEvent(radarrCalendarEvent),
|
||||
};
|
||||
return radarrCalendarEvents.flatMap((radarrCalendarEvent): CalendarEvent[] => {
|
||||
const imageSrc = this.chooseBestImageAsURL(radarrCalendarEvent);
|
||||
|
||||
return radarrReleaseTypes
|
||||
.map((releaseType) => ({ type: releaseType, date: radarrCalendarEvent[releaseType] }))
|
||||
.filter((item) => item.date !== undefined)
|
||||
.map((item) => ({
|
||||
title: radarrCalendarEvent.title,
|
||||
subTitle: radarrCalendarEvent.originalTitle,
|
||||
description: radarrCalendarEvent.overview ?? null,
|
||||
// Check is done above in the filter
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
startDate: item.date!,
|
||||
endDate: null,
|
||||
image: imageSrc
|
||||
? {
|
||||
src: imageSrc,
|
||||
aspectRatio: { width: 7, height: 12 },
|
||||
}
|
||||
: null,
|
||||
location: null,
|
||||
metadata: {
|
||||
type: "radarr",
|
||||
releaseType: item.type,
|
||||
},
|
||||
indicatorColor: "yellow",
|
||||
links: this.getLinksForRadarrCalendarEvent(radarrCalendarEvent),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private getLinksForRadarrCalendarEvent = (event: z.infer<typeof radarrCalendarEventSchema>) => {
|
||||
const links: CalendarEvent["links"] = [
|
||||
const links: CalendarLink[] = [
|
||||
{
|
||||
href: this.url(`/movie/${event.titleSlug}`).toString(),
|
||||
name: "Radarr",
|
||||
logo: "/images/apps/radarr.svg",
|
||||
color: undefined,
|
||||
notificationColor: "yellow",
|
||||
isDark: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { IntegrationTestingInput } from "../../base/integration";
|
||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||
import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration";
|
||||
import type { CalendarEvent } from "../../interfaces/calendar/calendar-types";
|
||||
import type { CalendarEvent, CalendarLink } from "../../interfaces/calendar/calendar-types";
|
||||
import { mediaOrganizerPriorities } from "../media-organizer";
|
||||
|
||||
export class ReadarrIntegration extends Integration implements ICalendarIntegration {
|
||||
@@ -50,15 +50,22 @@ export class ReadarrIntegration extends Integration implements ICalendarIntegrat
|
||||
const readarrCalendarEvents = await z.array(readarrCalendarEventSchema).parseAsync(await response.json());
|
||||
|
||||
return readarrCalendarEvents.map((readarrCalendarEvent): CalendarEvent => {
|
||||
const imageSrc = this.chooseBestImageAsURL(readarrCalendarEvent);
|
||||
|
||||
return {
|
||||
name: readarrCalendarEvent.title,
|
||||
subName: readarrCalendarEvent.author.authorName,
|
||||
description: readarrCalendarEvent.overview,
|
||||
thumbnail: this.chooseBestImageAsURL(readarrCalendarEvent),
|
||||
date: readarrCalendarEvent.releaseDate,
|
||||
mediaInformation: {
|
||||
type: "audio",
|
||||
},
|
||||
title: readarrCalendarEvent.title,
|
||||
subTitle: readarrCalendarEvent.author.authorName,
|
||||
description: readarrCalendarEvent.overview ?? null,
|
||||
startDate: readarrCalendarEvent.releaseDate,
|
||||
endDate: null,
|
||||
image: imageSrc
|
||||
? {
|
||||
src: imageSrc,
|
||||
aspectRatio: { width: 7, height: 12 },
|
||||
}
|
||||
: null,
|
||||
location: null,
|
||||
indicatorColor: "#f5c518",
|
||||
links: this.getLinksForReadarrCalendarEvent(readarrCalendarEvent),
|
||||
};
|
||||
});
|
||||
@@ -72,9 +79,8 @@ export class ReadarrIntegration extends Integration implements ICalendarIntegrat
|
||||
isDark: false,
|
||||
logo: "/images/apps/readarr.svg",
|
||||
name: "Readarr",
|
||||
notificationColor: "#f5c518",
|
||||
},
|
||||
] satisfies CalendarEvent["links"];
|
||||
] satisfies CalendarLink[];
|
||||
};
|
||||
|
||||
private chooseBestImage = (
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { IntegrationTestingInput } from "../../base/integration";
|
||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||
import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration";
|
||||
import type { CalendarEvent } from "../../interfaces/calendar/calendar-types";
|
||||
import type { CalendarEvent, CalendarLink } from "../../interfaces/calendar/calendar-types";
|
||||
import { mediaOrganizerPriorities } from "../media-organizer";
|
||||
|
||||
export class SonarrIntegration extends Integration implements ICalendarIntegration {
|
||||
@@ -33,33 +33,36 @@ export class SonarrIntegration extends Integration implements ICalendarIntegrati
|
||||
"X-Api-Key": super.getSecretValue("apiKey"),
|
||||
},
|
||||
});
|
||||
const sonarCalendarEvents = await z.array(sonarrCalendarEventSchema).parseAsync(await response.json());
|
||||
const sonarrCalendarEvents = await z.array(sonarrCalendarEventSchema).parseAsync(await response.json());
|
||||
|
||||
return sonarCalendarEvents.map(
|
||||
(sonarCalendarEvent): CalendarEvent => ({
|
||||
name: sonarCalendarEvent.title,
|
||||
subName: sonarCalendarEvent.series.title,
|
||||
description: sonarCalendarEvent.series.overview,
|
||||
thumbnail: this.chooseBestImageAsURL(sonarCalendarEvent),
|
||||
date: sonarCalendarEvent.airDateUtc,
|
||||
mediaInformation: {
|
||||
type: "tv",
|
||||
episodeNumber: sonarCalendarEvent.episodeNumber,
|
||||
seasonNumber: sonarCalendarEvent.seasonNumber,
|
||||
},
|
||||
links: this.getLinksForSonarCalendarEvent(sonarCalendarEvent),
|
||||
}),
|
||||
);
|
||||
return sonarrCalendarEvents.map((event): CalendarEvent => {
|
||||
const imageSrc = this.chooseBestImageAsURL(event);
|
||||
return {
|
||||
title: event.title,
|
||||
subTitle: event.series.title,
|
||||
description: event.series.overview ?? null,
|
||||
startDate: event.airDateUtc,
|
||||
endDate: null,
|
||||
image: imageSrc
|
||||
? {
|
||||
src: imageSrc,
|
||||
aspectRatio: { width: 7, height: 12 },
|
||||
}
|
||||
: null,
|
||||
location: null,
|
||||
indicatorColor: "blue",
|
||||
links: this.getLinksForSonarrCalendarEvent(event),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getLinksForSonarCalendarEvent = (event: z.infer<typeof sonarrCalendarEventSchema>) => {
|
||||
const links: CalendarEvent["links"] = [
|
||||
private getLinksForSonarrCalendarEvent = (event: z.infer<typeof sonarrCalendarEventSchema>) => {
|
||||
const links: CalendarLink[] = [
|
||||
{
|
||||
href: this.url(`/series/${event.series.titleSlug}`).toString(),
|
||||
name: "Sonarr",
|
||||
logo: "/images/apps/sonarr.svg",
|
||||
color: undefined,
|
||||
notificationColor: "blue",
|
||||
isDark: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -8,32 +8,46 @@ export class CalendarMockService implements ICalendarIntegration {
|
||||
}
|
||||
}
|
||||
|
||||
const homarrMeetup = (start: Date, end: Date): CalendarEvent => ({
|
||||
name: "Homarr Meetup",
|
||||
subName: "",
|
||||
description: "Yearly meetup of the Homarr community",
|
||||
date: randomDateBetween(start, end),
|
||||
links: [
|
||||
{
|
||||
href: "https://homarr.dev",
|
||||
name: "Homarr",
|
||||
logo: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg",
|
||||
color: "#000000",
|
||||
notificationColor: "#fa5252",
|
||||
isDark: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const homarrMeetup = (start: Date, end: Date): CalendarEvent => {
|
||||
const startDate = randomDateBetween(start, end);
|
||||
const endDate = new Date(startDate.getTime() + 2 * 60 * 60 * 1000); // 2 hours later
|
||||
return {
|
||||
title: "Homarr Meetup",
|
||||
subTitle: "",
|
||||
description: "Yearly meetup of the Homarr community",
|
||||
startDate,
|
||||
endDate,
|
||||
image: null,
|
||||
location: "Mountains",
|
||||
indicatorColor: "#fa5252",
|
||||
links: [
|
||||
{
|
||||
href: "https://homarr.dev",
|
||||
name: "Homarr",
|
||||
logo: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg",
|
||||
color: "#000000",
|
||||
isDark: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const titanicRelease = (start: Date, end: Date): CalendarEvent => ({
|
||||
name: "Titanic",
|
||||
subName: "A classic movie",
|
||||
title: "Titanic",
|
||||
subTitle: "A classic movie",
|
||||
description: "A tragic love story set on the ill-fated RMS Titanic.",
|
||||
date: randomDateBetween(start, end),
|
||||
thumbnail: "https://image.tmdb.org/t/p/original/5bTWA20cL9LCIGNpde4Epc2Ijzn.jpg",
|
||||
mediaInformation: {
|
||||
type: "movie",
|
||||
startDate: randomDateBetween(start, end),
|
||||
endDate: null,
|
||||
image: {
|
||||
src: "https://image.tmdb.org/t/p/original/5bTWA20cL9LCIGNpde4Epc2Ijzn.jpg",
|
||||
aspectRatio: { width: 7, height: 12 },
|
||||
},
|
||||
location: null,
|
||||
metadata: {
|
||||
type: "radarr",
|
||||
releaseType: "inCinemas",
|
||||
},
|
||||
indicatorColor: "cyan",
|
||||
links: [
|
||||
{
|
||||
href: "https://www.imdb.com/title/tt0120338/",
|
||||
@@ -41,22 +55,26 @@ const titanicRelease = (start: Date, end: Date): CalendarEvent => ({
|
||||
color: "#f5c518",
|
||||
isDark: false,
|
||||
logo: "/images/apps/imdb.svg",
|
||||
notificationColor: "cyan",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const seriesRelease = (start: Date, end: Date): CalendarEvent => ({
|
||||
name: "The Mandalorian",
|
||||
subName: "A Star Wars Series",
|
||||
title: "The Mandalorian",
|
||||
subTitle: "A Star Wars Series",
|
||||
description: "A lone bounty hunter in the outer reaches of the galaxy.",
|
||||
date: randomDateBetween(start, end),
|
||||
thumbnail: "https://image.tmdb.org/t/p/original/ztvm7C7hiUpS3CZRXFmJxljICzK.jpg",
|
||||
mediaInformation: {
|
||||
type: "tv",
|
||||
seasonNumber: 1,
|
||||
episodeNumber: 1,
|
||||
startDate: randomDateBetween(start, end),
|
||||
endDate: null,
|
||||
image: {
|
||||
src: "https://image.tmdb.org/t/p/original/sWgBv7LV2PRoQgkxwlibdGXKz1S.jpg",
|
||||
aspectRatio: { width: 7, height: 12 },
|
||||
badge: {
|
||||
content: "S1:E1",
|
||||
color: "red",
|
||||
},
|
||||
},
|
||||
location: null,
|
||||
indicatorColor: "blue",
|
||||
links: [
|
||||
{
|
||||
href: "https://www.imdb.com/title/tt8111088/",
|
||||
@@ -64,7 +82,6 @@ const seriesRelease = (start: Date, end: Date): CalendarEvent => ({
|
||||
color: "#f5c518",
|
||||
isDark: false,
|
||||
logo: "/images/apps/imdb.svg",
|
||||
notificationColor: "blue",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -63,17 +63,20 @@ export class NextcloudIntegration extends Integration implements ICalendarIntegr
|
||||
);
|
||||
|
||||
return {
|
||||
name: veventObject.summary,
|
||||
date,
|
||||
subName: "",
|
||||
title: veventObject.summary,
|
||||
subTitle: null,
|
||||
description: veventObject.description,
|
||||
startDate: date,
|
||||
endDate: veventObject.end,
|
||||
image: null,
|
||||
location: veventObject.location || null,
|
||||
indicatorColor: "#ff8600",
|
||||
links: [
|
||||
{
|
||||
href: url.toString(),
|
||||
name: "Nextcloud",
|
||||
logo: "/images/apps/nextcloud.svg",
|
||||
color: undefined,
|
||||
notificationColor: "#ff8600",
|
||||
isDark: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -265,7 +265,9 @@ const mapType = (type: string): "movie" | "tv" | "unknown" => {
|
||||
switch (type) {
|
||||
case "movie":
|
||||
return "movie";
|
||||
case "tv":
|
||||
case "show":
|
||||
case "season":
|
||||
case "episode":
|
||||
return "tv";
|
||||
default:
|
||||
return "unknown";
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
import dayjs from "dayjs";
|
||||
import z from "zod";
|
||||
|
||||
import { createId } from "@homarr/common";
|
||||
import { RequestError, ResponseError } from "@homarr/common/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-monitoring/health-monitoring-integration";
|
||||
import type { SystemHealthMonitoring } from "../interfaces/health-monitoring/health-monitoring-types";
|
||||
|
||||
const localLogger = logger.child({ module: "TrueNasIntegration" });
|
||||
|
||||
const NETWORK_MULTIPLIER = 100;
|
||||
|
||||
export class TrueNasIntegration extends Integration implements ISystemHealthMonitoringIntegration {
|
||||
private static webSocketMap = new Map<string, WebSocket>();
|
||||
|
||||
private wsUrl() {
|
||||
const url = super.url("/websocket");
|
||||
url.protocol = url.protocol.replace("http", "ws");
|
||||
return url;
|
||||
}
|
||||
|
||||
private get webSocket() {
|
||||
return TrueNasIntegration.webSocketMap.get(this.integration.id) ?? null;
|
||||
}
|
||||
|
||||
protected async testingAsync(_input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const webSocket = await this.connectWebSocketAsync();
|
||||
await this.registerSessionAsync(webSocket);
|
||||
await this.authenticateWebSocketAsync(webSocket);
|
||||
|
||||
// Remove current socket connection so we can authenticate with updated credentials
|
||||
TrueNasIntegration.webSocketMap.delete(this.integration.id);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* TrueNAS API uses WebSocket. This function connects to the socket
|
||||
* and resolves the promise if the connection was successful.
|
||||
* @see https://www.truenas.com/docs/api/scale_websocket_api.html
|
||||
*/
|
||||
private async connectWebSocketAsync(): Promise<WebSocket> {
|
||||
localLogger.debug("Connecting to websocket server", {
|
||||
url: this.wsUrl(),
|
||||
});
|
||||
const webSocket = new WebSocket(this.wsUrl());
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
webSocket.onopen = () => {
|
||||
localLogger.debug("Connected to websocket server", {
|
||||
url: this.wsUrl(),
|
||||
});
|
||||
resolve(webSocket);
|
||||
};
|
||||
|
||||
webSocket.onerror = () => {
|
||||
reject(new Error("Failed to connect"));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Before authentication, a session must be obtained from the server using the "connect" event.
|
||||
* @see https://www.truenas.com/docs/api/scale_websocket_api.html#websocket_protocol
|
||||
*/
|
||||
private async registerSessionAsync(webSocket: WebSocket): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const subscribe = (event: MessageEvent<string>) => {
|
||||
const data = JSON.parse(event.data) as { msg: string };
|
||||
if (data.msg === "connected") {
|
||||
webSocket.removeEventListener("message", subscribe);
|
||||
resolve();
|
||||
} else if (data.msg === "failed") {
|
||||
webSocket.removeEventListener("message", subscribe);
|
||||
reject(new Error("Unable to establish connection"));
|
||||
}
|
||||
};
|
||||
|
||||
webSocket.addEventListener("message", subscribe);
|
||||
webSocket.send(
|
||||
JSON.stringify({
|
||||
msg: "connect",
|
||||
version: "1", // this must be number, not string
|
||||
support: ["1"], // this must be number, not string
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* After a session was obtained, the session can be authenticated.
|
||||
* @see https://www.truenas.com/docs/api/scale_websocket_api.html#websocket_protocol
|
||||
*/
|
||||
private async authenticateWebSocketAsync(webSocket?: WebSocket): Promise<void> {
|
||||
localLogger.debug("Authenticating with username and password", {
|
||||
url: this.wsUrl(),
|
||||
});
|
||||
const response = await this.requestAsync(
|
||||
"auth.login",
|
||||
[this.getSecretValue("username"), this.getSecretValue("password")],
|
||||
webSocket,
|
||||
);
|
||||
const result = await z.boolean().parseAsync(response);
|
||||
if (!result) throw new ResponseError({ status: 401 });
|
||||
localLogger.debug("Authenticated successfully with username and password", {
|
||||
url: this.wsUrl(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves data using the reporting method
|
||||
* @see https://www.truenas.com/docs/api/scale_websocket_api.html#reporting
|
||||
*/
|
||||
private async getReportingAsync(): Promise<ReportingItem[]> {
|
||||
localLogger.debug("Retrieving reporting data", {
|
||||
url: this.wsUrl(),
|
||||
});
|
||||
|
||||
const response = await this.requestAsync("reporting.get_data", [
|
||||
[
|
||||
{
|
||||
name: "cpu",
|
||||
},
|
||||
{
|
||||
name: "memory",
|
||||
},
|
||||
{
|
||||
name: "cputemp",
|
||||
},
|
||||
],
|
||||
{
|
||||
aggregate: true,
|
||||
start: dayjs().add(-5, "minutes").unix(),
|
||||
end: dayjs().unix(),
|
||||
},
|
||||
]);
|
||||
const result = await z.array(reportingItemSchema).parseAsync(response);
|
||||
|
||||
localLogger.debug("Retrieved reporting data", {
|
||||
url: this.wsUrl(),
|
||||
count: result.length,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of all available network interfaces
|
||||
* @see https://www.truenas.com/docs/core/13.0/api/core_websocket_api.html#interface
|
||||
*/
|
||||
private async getNetworkInterfacesAsync(): Promise<z.infer<typeof networkInterfaceSchema>> {
|
||||
localLogger.debug("Retrieving available network-interfaces", {
|
||||
url: this.wsUrl(),
|
||||
});
|
||||
|
||||
const response = await this.requestAsync("interface.query", [
|
||||
[], // no filters
|
||||
{},
|
||||
]);
|
||||
const result = await networkInterfaceSchema.parseAsync(response);
|
||||
|
||||
localLogger.debug("Retrieved available network-interfaces", {
|
||||
url: this.wsUrl(),
|
||||
count: result.length,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves reporting network data of the last 5 minutes
|
||||
* @see https://www.truenas.com/docs/api/scale_websocket_api.html#reporting
|
||||
*/
|
||||
private async getReportingNetdataAsync(): Promise<z.infer<typeof reportingNetDataSchema>> {
|
||||
const networkInterfaces = await this.getNetworkInterfacesAsync();
|
||||
|
||||
localLogger.debug("Retrieving reporting network data", {
|
||||
url: this.wsUrl(),
|
||||
});
|
||||
|
||||
const response = await this.requestAsync("reporting.netdata_get_data", [
|
||||
networkInterfaces.map((networkInterface) => ({
|
||||
name: "interface",
|
||||
identifier: networkInterface.id,
|
||||
})),
|
||||
{
|
||||
start: dayjs().add(-5, "minutes").unix(),
|
||||
end: dayjs().unix(),
|
||||
},
|
||||
]);
|
||||
const result = await reportingNetDataSchema.parseAsync(response);
|
||||
|
||||
localLogger.debug("Retrieved reporting-network-data", {
|
||||
url: this.wsUrl(),
|
||||
count: result.length,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves information about the system
|
||||
* @see https://www.truenas.com/docs/api/scale_websocket_api.html#system
|
||||
*/
|
||||
private async getSystemInformationAsync(): Promise<z.infer<typeof systemInfoSchema>> {
|
||||
localLogger.debug("Retrieving system-information", {
|
||||
url: this.wsUrl(),
|
||||
});
|
||||
|
||||
const response = await this.requestAsync("system.info");
|
||||
const result = await systemInfoSchema.parseAsync(response);
|
||||
|
||||
localLogger.debug("Retrieved system-information", {
|
||||
url: this.wsUrl(),
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getSystemInfoAsync(): Promise<SystemHealthMonitoring> {
|
||||
const systemInformation = await this.getSystemInformationAsync();
|
||||
const reporting = await this.getReportingAsync();
|
||||
|
||||
const cpuData = this.extractLatestReportingData(reporting, "cpu");
|
||||
const cpuTempData = this.extractLatestReportingData(reporting, "cputemp");
|
||||
const memoryData = this.extractLatestReportingData(reporting, "memory");
|
||||
|
||||
const netdata = await this.getReportingNetdataAsync();
|
||||
|
||||
const upload = this.extractNetworkTrafficData(netdata, 2); // Index 2 is "sent"
|
||||
const download = this.extractNetworkTrafficData(netdata, 1); // Index 1 is "received"
|
||||
|
||||
return {
|
||||
cpuUtilization: cpuData.reduce((acc, item) => acc + (item > 100 ? 0 : item), 0) / cpuData.length,
|
||||
cpuTemp: Math.max(...cpuTempData.filter((_item, i) => i > 0)),
|
||||
memAvailableInBytes: systemInformation.physmem,
|
||||
memUsedInBytes: memoryData[1] ?? 0, // Index 0 is UNIX timestamp, Index 1 is free space in bytes
|
||||
fileSystem: [],
|
||||
availablePkgUpdates: 0,
|
||||
network: {
|
||||
up: upload * NETWORK_MULTIPLIER,
|
||||
down: download * NETWORK_MULTIPLIER,
|
||||
},
|
||||
loadAverage: null,
|
||||
smart: [],
|
||||
uptime: systemInformation.uptime_seconds,
|
||||
version: systemInformation.version,
|
||||
cpuModelName: systemInformation.model,
|
||||
rebootRequired: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request through websocket and return response
|
||||
* Times out after 5 seconds when no response was received.
|
||||
* @param method json-rpc method to call
|
||||
* @param params array of parameters
|
||||
* @param webSocketOverride override of webSocket, helpful for not storing the connection
|
||||
* @returns result of json-rpc call
|
||||
*/
|
||||
private async requestAsync(method: string, params: unknown[] = [], webSocketOverride?: WebSocket) {
|
||||
let webSocket = webSocketOverride ?? this.webSocket;
|
||||
if (!webSocket || webSocket.readyState !== WebSocket.OPEN) {
|
||||
localLogger.debug("Connecting to websocket", {
|
||||
url: this.wsUrl(),
|
||||
});
|
||||
// We can only land here with static webSocket
|
||||
webSocket = await this.connectWebSocketAsync();
|
||||
await this.registerSessionAsync(webSocket);
|
||||
|
||||
TrueNasIntegration.webSocketMap.set(this.integration.id, webSocket);
|
||||
await this.authenticateWebSocketAsync();
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
const id = createId();
|
||||
const handler = (event: MessageEvent<string>) => {
|
||||
const data = JSON.parse(event.data) as Record<string, unknown>;
|
||||
if (data.msg !== "result") return;
|
||||
if (data.id !== id) return;
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
webSocket.removeEventListener("message", handler);
|
||||
localLogger.debug("Received method response", {
|
||||
id,
|
||||
method,
|
||||
url: this.wsUrl(),
|
||||
});
|
||||
resolve(data.result);
|
||||
};
|
||||
const timeoutId = setTimeout(() => {
|
||||
webSocket.removeEventListener("message", handler);
|
||||
reject(
|
||||
new RequestError(
|
||||
{
|
||||
type: "timeout",
|
||||
reason: "aborted",
|
||||
code: "ECONNABORTED",
|
||||
},
|
||||
{ cause: new Error("Canceled request after 5 seconds") },
|
||||
),
|
||||
);
|
||||
}, 5000);
|
||||
|
||||
webSocket.addEventListener("message", handler);
|
||||
|
||||
localLogger.debug("Sending method request", {
|
||||
id,
|
||||
method,
|
||||
url: this.wsUrl(),
|
||||
});
|
||||
|
||||
webSocket.send(
|
||||
JSON.stringify({
|
||||
id,
|
||||
msg: "method",
|
||||
method,
|
||||
params,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private extractNetworkTrafficData = (data: z.infer<typeof reportingNetDataSchema>, index = 1 | 2) => {
|
||||
return data.reduce((acc, current) => acc + (current.data.at(-1)?.at(index) ?? 0), 0);
|
||||
};
|
||||
|
||||
private extractLatestReportingData(data: ReportingItem[], key: ReportingItem["identifier"]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const dataObject = data.find((item) => item.identifier === key)!;
|
||||
// TODO: check why the below sorting is done, because right now it compares number[] with number[]?
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return dataObject.data.sort((item1, item2) => (item1 > item2 ? -1 : 1))[0]!;
|
||||
}
|
||||
}
|
||||
|
||||
const reportingItemSchema = z.object({
|
||||
name: z.enum(["cpu", "memory", "cputemp"]),
|
||||
identifier: z.enum(["cpu", "memory", "cputemp"]),
|
||||
aggregations: z.object({
|
||||
min: z.record(z.string(), z.unknown()),
|
||||
mean: z.record(z.string(), z.unknown()),
|
||||
max: z.record(z.string(), z.unknown()),
|
||||
}),
|
||||
start: z.number().min(0),
|
||||
end: z.number().min(0),
|
||||
legend: z.array(z.string()),
|
||||
data: z.array(z.array(z.number())),
|
||||
});
|
||||
|
||||
type ReportingItem = z.infer<typeof reportingItemSchema>;
|
||||
|
||||
const reportingNetDataSchema = z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
identifier: z.string(),
|
||||
data: z.array(z.array(z.number())),
|
||||
}),
|
||||
);
|
||||
|
||||
const systemInfoSchema = z.object({
|
||||
version: z.string(),
|
||||
hostname: z.string(),
|
||||
physmem: z.number().min(0), // pysical memory
|
||||
model: z.string(), // cpu model
|
||||
uptime_seconds: z.number().min(0),
|
||||
});
|
||||
|
||||
const networkInterfaceSchema = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
);
|
||||
@@ -27,13 +27,13 @@
|
||||
"@homarr/core": "workspace:^0.1.0",
|
||||
"superjson": "2.2.2",
|
||||
"winston": "3.17.0",
|
||||
"zod": "^4.1.5"
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -33,19 +33,19 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.2.8",
|
||||
"@mantine/core": "^8.3.1",
|
||||
"@tabler/icons-react": "^3.34.1",
|
||||
"dayjs": "^1.11.18",
|
||||
"next": "15.5.2",
|
||||
"next": "15.5.3",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"zod": "^4.1.5"
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export const ImportBoardModal = createModal(({ actions }) => {
|
||||
}
|
||||
|
||||
setFileValid(true);
|
||||
form.setFieldValue("configuration.name", result.data.configProperties.name);
|
||||
form.setFieldValue("configuration.name", result.data.configProperties.name.replaceAll(" ", "-"));
|
||||
})();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -24,15 +24,15 @@
|
||||
"dependencies": {
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.2.8",
|
||||
"@mantine/hooks": "^8.2.8",
|
||||
"@mantine/core": "^8.3.1",
|
||||
"@mantine/hooks": "^8.3.1",
|
||||
"react": "19.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,14 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/notifications": "^8.2.8",
|
||||
"@mantine/notifications": "^8.3.1",
|
||||
"@tabler/icons-react": "^3.34.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -37,14 +37,14 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.2.8",
|
||||
"@mantine/hooks": "^8.2.8",
|
||||
"@mantine/core": "^8.3.1",
|
||||
"@mantine/hooks": "^8.3.1",
|
||||
"adm-zip": "0.5.16",
|
||||
"next": "15.5.2",
|
||||
"next": "15.5.3",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"superjson": "2.2.2",
|
||||
"zod": "^4.1.5",
|
||||
"zod": "^4.1.8",
|
||||
"zod-form-data": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -52,7 +52,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/adm-zip": "0.5.7",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export const analyseOldmarrImportAsync = async (file: File) => {
|
||||
}
|
||||
|
||||
return {
|
||||
name: entry.name.replace(".json", ""),
|
||||
name: entry.name.replaceAll(" ", "-").replace(".json", ""),
|
||||
config: result.data ?? null,
|
||||
isError: !result.success,
|
||||
};
|
||||
|
||||
@@ -98,7 +98,6 @@ const optionMapping: OptionMapping = {
|
||||
allowMicrophone: (oldOptions) => oldOptions.allowMicrophone,
|
||||
allowGeolocation: (oldOptions) => oldOptions.allowGeolocation,
|
||||
allowScrolling: (oldOptions) => oldOptions.allowScrolling,
|
||||
allowTransparency: (oldOptions) => oldOptions.allowTransparency,
|
||||
},
|
||||
video: {
|
||||
feedUrl: (oldOptions) => oldOptions.FeedUrl,
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"zod": "^4.1.5"
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@extractus/feed-extractor": "7.1.6",
|
||||
"@extractus/feed-extractor": "7.1.7",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
@@ -32,13 +32,13 @@
|
||||
"dayjs": "^1.11.18",
|
||||
"octokit": "^5.0.3",
|
||||
"superjson": "2.2.2",
|
||||
"undici": "7.15.0"
|
||||
"undici": "7.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,8 @@
|
||||
"@homarr/api": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@mantine/dates": "^8.2.8",
|
||||
"next": "15.5.2",
|
||||
"@mantine/dates": "^8.3.1",
|
||||
"next": "15.5.3",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1"
|
||||
},
|
||||
@@ -35,7 +35,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -33,12 +33,12 @@
|
||||
"@homarr/settings": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.2.8",
|
||||
"@mantine/hooks": "^8.2.8",
|
||||
"@mantine/spotlight": "^8.2.8",
|
||||
"@mantine/core": "^8.3.1",
|
||||
"@mantine/hooks": "^8.3.1",
|
||||
"@mantine/spotlight": "^8.3.1",
|
||||
"@tabler/icons-react": "^3.34.1",
|
||||
"jotai": "^2.13.1",
|
||||
"next": "15.5.2",
|
||||
"jotai": "^2.14.0",
|
||||
"next": "15.5.3",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"use-deep-compare-effect": "^1.8.1"
|
||||
@@ -47,7 +47,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,8 @@
|
||||
"dayjs": "^1.11.18",
|
||||
"deepmerge": "4.3.1",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "15.5.2",
|
||||
"next-intl": "4.3.5",
|
||||
"next": "15.5.3",
|
||||
"next-intl": "4.3.8",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1"
|
||||
},
|
||||
@@ -41,7 +41,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint": "^9.35.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@
|
||||
"apps": "Aplicacions",
|
||||
"boards": "Tauler",
|
||||
"integrations": "Integracions",
|
||||
"credentialUsers": ""
|
||||
"credentialUsers": "Credencials d'usuari"
|
||||
}
|
||||
},
|
||||
"tokenModal": {
|
||||
@@ -82,7 +82,7 @@
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "Nom del grup",
|
||||
"description": ""
|
||||
"description": "El nom ha de coincidir amb el grup d'Administradors del proveïdor extern"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -110,12 +110,12 @@
|
||||
"page": {
|
||||
"login": {
|
||||
"title": "Entreu al vostre compte",
|
||||
"subtitle": ""
|
||||
"subtitle": "Benvingut de nou! Si us plau, introduïu les credencials"
|
||||
},
|
||||
"invite": {
|
||||
"title": "Uneix-te a Homarr",
|
||||
"subtitle": "",
|
||||
"description": ""
|
||||
"subtitle": "Benvingut a Homarr! Si us plau, crea el teu compte",
|
||||
"description": "Has sigut invitat per {username}"
|
||||
},
|
||||
"init": {
|
||||
"title": "Nova instal·lació de Homarr",
|
||||
@@ -178,7 +178,7 @@
|
||||
},
|
||||
"forgotPassword": {
|
||||
"label": "Heu oblidat la contrasenya?",
|
||||
"description": ""
|
||||
"description": "Un administrador pot utilitzar el següent comandament per reinicialitzar la teva contrasenya"
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
@@ -239,10 +239,10 @@
|
||||
"changePingIconsEnabled": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "Icones de ping alternats correctament"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "Impossible d'alternar les icones de ping"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -307,12 +307,12 @@
|
||||
"name": "Nom",
|
||||
"members": "Membres",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Panell d'inici",
|
||||
"description": "Només els panells accessibles al grup es pot seleccionar"
|
||||
},
|
||||
"mobileBoard": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Panell mòbil",
|
||||
"description": "Només els panells accessibles al grup es pot seleccionar"
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
@@ -384,29 +384,29 @@
|
||||
},
|
||||
"full-all": {
|
||||
"label": "Accés total a les integracions",
|
||||
"description": ""
|
||||
"description": "Permetre que els membres gestionin, utilitzin i interactuïn amb cada integració"
|
||||
}
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"title": "",
|
||||
"title": "Medis",
|
||||
"item": {
|
||||
"upload": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Pujar medis",
|
||||
"description": "Permetre als membres puja medis"
|
||||
},
|
||||
"view-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Veure medis",
|
||||
"description": "Permet que els membres vegin tots els continguts multimèdia"
|
||||
},
|
||||
"full-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Accés complet als medis",
|
||||
"description": "Permet als membres gestionar i esborrar tots els continguts multimèdia"
|
||||
}
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
"title": "",
|
||||
"title": "Altres",
|
||||
"item": {
|
||||
"view-logs": {
|
||||
"label": "Veure registres",
|
||||
@@ -426,95 +426,95 @@
|
||||
"description": "Permet que els membres modifiquin tots els motors de cerca"
|
||||
},
|
||||
"full-all": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
"label": "Accés complet a la cerca",
|
||||
"description": "Permet als membres gestionar i esborrar els motors de cerca"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"memberNotice": {
|
||||
"mixed": "",
|
||||
"external": ""
|
||||
"mixed": "Alguns membres de proveïdors externs no es poden gestionar aquí",
|
||||
"external": "Tots els membres de proveïdors externs no es poden gestionar aquí"
|
||||
},
|
||||
"reservedNotice": {
|
||||
"message": ""
|
||||
"message": "Aquest grup està reservat pel sistema i restringeix algunes accions <checkoutDocs></checkoutDocs>"
|
||||
},
|
||||
"action": {
|
||||
"create": {
|
||||
"label": "",
|
||||
"label": "Nou grup",
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "El grup s'ha creat satisfactòriament"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "El grup no es pot crear"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transfer": {
|
||||
"label": "",
|
||||
"description": "",
|
||||
"confirm": "",
|
||||
"label": "Transferir propietari",
|
||||
"description": "Transferir la propietat del grup a altre usuari.",
|
||||
"confirm": "Segur que vols transferir el propietari de {name} a {username}?",
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "Transferència del {group} a {user} satisfactòria"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "No ha sigut possible transferir la propietat"
|
||||
}
|
||||
}
|
||||
},
|
||||
"addMember": {
|
||||
"label": ""
|
||||
"label": "Afegir usuari"
|
||||
},
|
||||
"removeMember": {
|
||||
"label": "",
|
||||
"confirm": ""
|
||||
"label": "Esborrar usuari",
|
||||
"confirm": "Segur que vols esborrar {user} d'aquest grup?"
|
||||
},
|
||||
"delete": {
|
||||
"label": "",
|
||||
"description": "",
|
||||
"confirm": "",
|
||||
"label": "Esborrar grup",
|
||||
"description": "Un cop esborris el grup, no el podràs recuperar. ",
|
||||
"confirm": "Segur que desitges esborrar el grup {name}?",
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "S'ha esborrat el grup {name}"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "Impossible esborrar el grup {name}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"changePermissions": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Privilegis guardats",
|
||||
"message": "Els privilegis s'han guardat satisfactòriament"
|
||||
},
|
||||
"error": {
|
||||
"title": "",
|
||||
"message": ""
|
||||
"title": "Privilegis no guardats",
|
||||
"message": "Els privilegis no s'han guardat"
|
||||
}
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"message": ""
|
||||
"message": "El grup {name} s'ha guardat satisfactòriament"
|
||||
},
|
||||
"error": {
|
||||
"message": ""
|
||||
"message": "No s'ha pogut guardar el grup {name}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"label": "",
|
||||
"notFound": ""
|
||||
"label": "Seleccionar grup",
|
||||
"notFound": "No s'ha trobat el grup"
|
||||
},
|
||||
"settings": {
|
||||
"board": {
|
||||
"notification": {
|
||||
"success": {
|
||||
"title": "",
|
||||
"title": "Configuració guardada",
|
||||
"message": ""
|
||||
},
|
||||
"error": {
|
||||
@@ -952,6 +952,18 @@
|
||||
"opnsenseApiSecret": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"githubAppId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"githubInstallationId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"privateKey": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3243,9 +3255,6 @@
|
||||
"dnsHole": {
|
||||
"label": ""
|
||||
},
|
||||
"sessionCleanup": {
|
||||
"label": ""
|
||||
},
|
||||
"updateChecker": {
|
||||
"label": ""
|
||||
},
|
||||
|
||||
@@ -952,6 +952,18 @@
|
||||
"opnsenseApiSecret": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"githubAppId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"githubInstallationId": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"privateKey": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3243,9 +3255,6 @@
|
||||
"dnsHole": {
|
||||
"label": "DNS Hole 数据"
|
||||
},
|
||||
"sessionCleanup": {
|
||||
"label": "会话清理"
|
||||
},
|
||||
"updateChecker": {
|
||||
"label": "更新检查"
|
||||
},
|
||||
|
||||
Loaded 100 of 156 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user