Compare commits
331
Commits
No files matched your search
@@ -33,6 +33,15 @@ body:
|
|||||||
options:
|
options:
|
||||||
# The below comment is used to insert a new version with on-release.yml
|
# The below comment is used to insert a new version with on-release.yml
|
||||||
#NEXT_VERSION#
|
#NEXT_VERSION#
|
||||||
|
- 1.59.1
|
||||||
|
- 1.59.0
|
||||||
|
- 1.58.1
|
||||||
|
- 1.58.0
|
||||||
|
- 1.57.1
|
||||||
|
- 1.57.0
|
||||||
|
- 1.56.1
|
||||||
|
- 1.56.0
|
||||||
|
- 1.55.0
|
||||||
- 1.54.0
|
- 1.54.0
|
||||||
- 1.53.0
|
- 1.53.0
|
||||||
- 1.52.0
|
- 1.52.0
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ runs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Build docker image for ${{ inputs.architecture }}
|
- name: Build docker image for ${{ inputs.architecture }}
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
|
|||||||
@@ -11,6 +11,12 @@
|
|||||||
matchPackageNames: ["tsx"],
|
matchPackageNames: ["tsx"],
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
// Reenable once bug with undici v8 is fixed (https://github.com/homarr-labs/homarr/issues/5467)
|
||||||
|
{
|
||||||
|
matchPackageNames: ["undici"],
|
||||||
|
matchNewValue: "8.*",
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
|
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
|
||||||
automerge: true,
|
automerge: true,
|
||||||
@@ -31,6 +37,12 @@
|
|||||||
automerge: false,
|
automerge: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
// Updates lockfile for prebuilt-debian after if package.json is updated, otherwise only root lockfile is updated
|
||||||
|
postUpgradeTasks: {
|
||||||
|
commands: ["cd deployments/prebuilt-debian && pnpm install --ignore-workspace --lockfile-only"],
|
||||||
|
fileFilters: ["deployments/prebuilt-debian/pnpm-lock.yaml"],
|
||||||
|
executionMode: "update",
|
||||||
|
},
|
||||||
updateInternalDeps: true,
|
updateInternalDeps: true,
|
||||||
rangeStrategy: "bump",
|
rangeStrategy: "bump",
|
||||||
automerge: false,
|
automerge: false,
|
||||||
|
|||||||
@@ -9,13 +9,18 @@ jobs:
|
|||||||
approve-automatic-prs:
|
approve-automatic-prs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 2
|
timeout-minutes: 2
|
||||||
if: github.actor_id == 158783068 || github.actor_id == 190541745 || github.actor_id == 210161987 # Id of renovate bot and crowdin bot see https://api.github.com/users/homarr-renovate%5Bbot%5D and https://api.github.com/users/homarr-crowdin%5Bbot%5D and https://api.github.com/users/homarr-update-contributors%5Bbot%5D
|
# 158783068: homarr-renovate[bot]
|
||||||
|
# 190541745: homarr-crowdin[bot]
|
||||||
|
# 210161987: homarr-update-contributors[bot]
|
||||||
|
# 175486441: homarr-releases[bot]
|
||||||
|
# Extracted from https://api.github.com/users/<username>
|
||||||
|
if: github.actor_id == 158783068 || github.actor_id == 190541745 || github.actor_id == 210161987 || github.actor_id == 175486441
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: actions/create-github-app-token@v2
|
uses: actions/create-github-app-token@v3
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
||||||
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
- name: Build docker image
|
- name: Build docker image
|
||||||
id: build-docker-image
|
id: build-docker-image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
context: .
|
context: .
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: actions/create-github-app-token@v2
|
uses: actions/create-github-app-token@v3
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
|
||||||
app-id: ${{ secrets.CROWDIN_APP_ID }}
|
app-id: ${{ secrets.CROWDIN_APP_ID }}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: actions/create-github-app-token@v2
|
uses: actions/create-github-app-token@v3
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||||
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||||
@@ -60,12 +60,12 @@ jobs:
|
|||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v5
|
||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v6
|
||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
with:
|
with:
|
||||||
node-version: 24.13.1
|
node-version: 24.14.1
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: npm i -g pnpm
|
- run: npm i -g pnpm
|
||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
@@ -123,23 +123,23 @@ jobs:
|
|||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: "${{ env.GHCR_REPO }}"
|
images: "${{ env.GHCR_REPO }}"
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Build and push by digest
|
- name: Build and push by digest
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
network: host
|
network: host
|
||||||
@@ -162,23 +162,23 @@ jobs:
|
|||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: "${{ env.GHCR_REPO }}"
|
images: "${{ env.GHCR_REPO }}"
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Build and push by digest
|
- name: Build and push by digest
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
network: host
|
network: host
|
||||||
@@ -231,14 +231,14 @@ jobs:
|
|||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
if: needs.release.outputs.skipped == 'false'
|
if: needs.release.outputs.skipped == 'false'
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: actions/create-github-app-token@v2
|
uses: actions/create-github-app-token@v3
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||||
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||||
permission-contents: write # required to publish the release
|
permission-contents: write # required to publish the release
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ jobs:
|
|||||||
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }} (new behaviour: ${{ steps.semver.outputs.bump }})"
|
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }} (new behaviour: ${{ steps.semver.outputs.bump }})"
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainApprovalToken
|
id: obtainApprovalToken
|
||||||
uses: actions/create-github-app-token@v2
|
uses: actions/create-github-app-token@v3
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
||||||
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
gh pr review --approve --body "Automatically approved by GitHub Action"
|
gh pr review --approve --body "Automatically approved by GitHub Action"
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainMergeToken
|
id: obtainMergeToken
|
||||||
uses: actions/create-github-app-token@v2
|
uses: actions/create-github-app-token@v3
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||||
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: actions/create-github-app-token@v2
|
uses: actions/create-github-app-token@v3
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.HOMARR_DOCS_RELEASE_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.HOMARR_DOCS_RELEASE_APP_PRIVATE_KEY }}
|
||||||
app-id: ${{ vars.HOMARR_DOCS_RELEASE_APP_ID }}
|
app-id: ${{ vars.HOMARR_DOCS_RELEASE_APP_ID }}
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: actions/create-github-app-token@v2
|
uses: actions/create-github-app-token@v3
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||||
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: create-pull-request
|
id: create-pull-request
|
||||||
uses: peter-evans/create-pull-request@v7
|
uses: peter-evans/create-pull-request@v8
|
||||||
with:
|
with:
|
||||||
add-paths: .github/ISSUE_TEMPLATE/bug_report.yml
|
add-paths: .github/ISSUE_TEMPLATE/bug_report.yml
|
||||||
commit-message: "chore: update bug report template"
|
commit-message: "chore: update bug report template"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
skip-stability-days:
|
skip-stability-days:
|
||||||
if: ${{ !startsWith(github.head_ref, 'renovate/') }}
|
if: ${{ !startsWith(github.head_ref, 'renovate/') && github.event.pull_request.head.repo.full_name == github.repository }}
|
||||||
name: Skip Stability Days
|
name: Skip Stability Days
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 1
|
timeout-minutes: 1
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: actions/create-github-app-token@v2
|
uses: actions/create-github-app-token@v3
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
||||||
app-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
app-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v5
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: actions/create-github-app-token@v2
|
uses: actions/create-github-app-token@v3
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
||||||
app-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
app-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
||||||
|
|||||||
@@ -68,3 +68,9 @@ apps/nextjs/public/images/background.png
|
|||||||
|
|
||||||
# next-intl
|
# next-intl
|
||||||
en.d.json.ts
|
en.d.json.ts
|
||||||
|
|
||||||
|
# personal agent config
|
||||||
|
AGENTS.md
|
||||||
|
.opencode
|
||||||
|
.claude/CLAUDE.md
|
||||||
|
.claude/settings.json
|
||||||
+219
@@ -1,3 +1,222 @@
|
|||||||
|
## [1.59.1](https://github.com/homarr-labs/homarr/compare/v1.59.0...v1.59.1) (2026-04-10)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **qbittorrent:** requests fail ([#5473](https://github.com/homarr-labs/homarr/issues/5473)) ([db6bb87](https://github.com/homarr-labs/homarr/commit/db6bb870e3be14c7895a2882755d3aee09e44fa6))
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* **deps:** downgrade undici from v8 to v7 ([#5473](https://github.com/homarr-labs/homarr/issues/5473)) ([dc44fac](https://github.com/homarr-labs/homarr/commit/dc44fac42aab5679427b2921402dfa11e634e1cf))
|
||||||
|
|
||||||
|
## [1.59.0](https://github.com/homarr-labs/homarr/compare/v1.58.1...v1.59.0) (2026-04-10)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Anchor integration and Anchor Note widget ([#5040](https://github.com/homarr-labs/homarr/issues/5040)) ([7879b31](https://github.com/homarr-labs/homarr/commit/7879b3163074b6af9381b8eb70dced55faddc506))
|
||||||
|
* **rss-feed:** add optional poster ([#5452](https://github.com/homarr-labs/homarr/issues/5452)) ([6ec058c](https://github.com/homarr-labs/homarr/commit/6ec058c835040dbf72cd2490d49f94be7da066dc))
|
||||||
|
* **rss-feed:** add periodic live updates ([#5449](https://github.com/homarr-labs/homarr/issues/5449)) ([faffb81](https://github.com/homarr-labs/homarr/commit/faffb818f99900e3abb5fa78cfbb7980692362b5))
|
||||||
|
* speedtest tracker integration ([#5363](https://github.com/homarr-labs/homarr/issues/5363)) ([22d599c](https://github.com/homarr-labs/homarr/commit/22d599c0cf64b39f3d616ceadc6c2ac32551f99d))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** update dependency @tabler/icons-react to ^3.41.1 ([#5400](https://github.com/homarr-labs/homarr/issues/5400)) ([f0ef472](https://github.com/homarr-labs/homarr/commit/f0ef4729bfb7c3672968b77b9b757bc34877c60f))
|
||||||
|
* **deps:** update dependency adm-zip to v0.5.17 ([#5422](https://github.com/homarr-labs/homarr/issues/5422)) ([dbe8d84](https://github.com/homarr-labs/homarr/commit/dbe8d84202b00687f16784cbedf61d7488efbbff))
|
||||||
|
* **deps:** update dependency dotenv to ^17.4.0 ([#5427](https://github.com/homarr-labs/homarr/issues/5427)) ([50bf7cd](https://github.com/homarr-labs/homarr/commit/50bf7cdf194c13bcbf95ef8a6a1bc7eb4c9c3865))
|
||||||
|
* **deps:** update dependency dotenv to ^17.4.1 ([#5462](https://github.com/homarr-labs/homarr/issues/5462)) ([845f115](https://github.com/homarr-labs/homarr/commit/845f11526ea7b3dd6f543fad48a158a4782c52bb))
|
||||||
|
* **deps:** update dependency fast-xml-parser to ^5.5.10 ([#5439](https://github.com/homarr-labs/homarr/issues/5439)) ([a7b195d](https://github.com/homarr-labs/homarr/commit/a7b195d2cb25fffe81266e7f6fa9499d94cae733))
|
||||||
|
* **deps:** update dependency next to v16.2.3 [security] ([#5461](https://github.com/homarr-labs/homarr/issues/5461)) ([7982def](https://github.com/homarr-labs/homarr/commit/7982def853fd69c269946dc2d6377186512baaf9))
|
||||||
|
* **deps:** update dependency next-intl to v4.8.4 ([#5413](https://github.com/homarr-labs/homarr/issues/5413)) ([6047eb4](https://github.com/homarr-labs/homarr/commit/6047eb4477584172f7d8e6e14237c251ac487d81))
|
||||||
|
* **deps:** update dependency next-intl to v4.9.0 ([#5426](https://github.com/homarr-labs/homarr/issues/5426)) ([612038a](https://github.com/homarr-labs/homarr/commit/612038ad070681c582cb54ac4298bc07700765c9))
|
||||||
|
* **deps:** update dependency react-quill-new to ^3.8.3 ([#5463](https://github.com/homarr-labs/homarr/issues/5463)) ([13e13ba](https://github.com/homarr-labs/homarr/commit/13e13ba3526436b044ac83d08c346e8e406ca3d4))
|
||||||
|
* **deps:** update dependency sass to ^1.99.0 ([#5437](https://github.com/homarr-labs/homarr/issues/5437)) ([3f73474](https://github.com/homarr-labs/homarr/commit/3f73474d3125279d94395272365aa67e0a0f69e8))
|
||||||
|
* **deps:** update dependency typescript-eslint to ^8.58.0 ([#5409](https://github.com/homarr-labs/homarr/issues/5409)) ([1e869f6](https://github.com/homarr-labs/homarr/commit/1e869f6a96d7e34af068b5666e01093a4b20fe97))
|
||||||
|
* **deps:** update dependency undici to v8 ([#5432](https://github.com/homarr-labs/homarr/issues/5432)) ([f2f5128](https://github.com/homarr-labs/homarr/commit/f2f5128cbd961b6576e195ac8b695f12996c377e))
|
||||||
|
* **deps:** update dependency undici to v8.0.1 ([#5440](https://github.com/homarr-labs/homarr/issues/5440)) ([a0e0083](https://github.com/homarr-labs/homarr/commit/a0e0083b16ea6ce42a0e3594ad53321210c98141))
|
||||||
|
* **deps:** update dependency undici to v8.0.2 ([#5446](https://github.com/homarr-labs/homarr/issues/5446)) ([75b36bb](https://github.com/homarr-labs/homarr/commit/75b36bb1609a0ea1a97158f4674ebf8f4c5f205e))
|
||||||
|
* **deps:** update nextjs monorepo to v16.2.2 ([#5418](https://github.com/homarr-labs/homarr/issues/5418)) ([8597470](https://github.com/homarr-labs/homarr/commit/859747037b4a0caca9e208e0b4ebce638b6c5d4e))
|
||||||
|
* **deps:** update tanstack-query monorepo to ^5.96.0 ([#5410](https://github.com/homarr-labs/homarr/issues/5410)) ([8b1e3ba](https://github.com/homarr-labs/homarr/commit/8b1e3babc02d8ab6a7db9fd85325a0b92813ce8c))
|
||||||
|
* **deps:** update tanstack-query monorepo to ^5.96.1 ([#5421](https://github.com/homarr-labs/homarr/issues/5421)) ([72ad935](https://github.com/homarr-labs/homarr/commit/72ad935ddf831c008e53ae3a1c5e924cc82710d1))
|
||||||
|
* **deps:** update tanstack-query monorepo to ^5.96.2 ([#5442](https://github.com/homarr-labs/homarr/issues/5442)) ([b619ba1](https://github.com/homarr-labs/homarr/commit/b619ba1d46768cc3ed1d3739e0f379e0059bc2d9))
|
||||||
|
* **deps:** update tiptap monorepo to v3.22.0 ([#5414](https://github.com/homarr-labs/homarr/issues/5414)) ([402d948](https://github.com/homarr-labs/homarr/commit/402d948fe7d36406b7a75a4e4861330c6bcaa78b))
|
||||||
|
* **deps:** update tiptap monorepo to v3.22.1 ([#5434](https://github.com/homarr-labs/homarr/issues/5434)) ([2baab54](https://github.com/homarr-labs/homarr/commit/2baab5402d79caba0e1b3c53f52fdf0cbeb6875d))
|
||||||
|
* **deps:** update tiptap monorepo to v3.22.2 ([#5448](https://github.com/homarr-labs/homarr/issues/5448)) ([8cab4b0](https://github.com/homarr-labs/homarr/commit/8cab4b03333050376ae22a14eaaa1c0f045992d3))
|
||||||
|
* **deps:** update turbo monorepo to ^2.9.1 ([#5408](https://github.com/homarr-labs/homarr/issues/5408)) ([7607140](https://github.com/homarr-labs/homarr/commit/7607140f94f5b19a6b9cbacc7deabd2afc004d24))
|
||||||
|
* **deps:** update turbo monorepo to ^2.9.2 ([#5415](https://github.com/homarr-labs/homarr/issues/5415)) ([d8480c0](https://github.com/homarr-labs/homarr/commit/d8480c05a63525dc655a73675c2a9eec575a9539))
|
||||||
|
* **deps:** update turbo monorepo to ^2.9.3 ([#5419](https://github.com/homarr-labs/homarr/issues/5419)) ([dfa8793](https://github.com/homarr-labs/homarr/commit/dfa87939f200719af9f34e0e409895cf6e5a9c13))
|
||||||
|
* **logs:** missing fields from top level error with metadata in output ([#5456](https://github.com/homarr-labs/homarr/issues/5456)) ([41a03ef](https://github.com/homarr-labs/homarr/commit/41a03ef4e03675c5adf1171be152a767679bc466))
|
||||||
|
* **security:** remove salt from database ([#5412](https://github.com/homarr-labs/homarr/issues/5412)) ([2454cac](https://github.com/homarr-labs/homarr/commit/2454cac7be14207f822bf3e38f9a7fd261645dc5))
|
||||||
|
|
||||||
|
## [1.58.1](https://github.com/homarr-labs/homarr/compare/v1.58.0...v1.58.1) (2026-04-04)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* arabic text in english translations ([a86cdb9](https://github.com/homarr-labs/homarr/commit/a86cdb9fb91a10c8250f849e70c17777c18ada77))
|
||||||
|
|
||||||
|
## [1.58.0](https://github.com/homarr-labs/homarr/compare/v1.57.1...v1.58.0) (2026-04-03)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **integrations:** add search.ch integration with timetable widget ([#5180](https://github.com/homarr-labs/homarr/issues/5180)) ([abb2dfc](https://github.com/homarr-labs/homarr/commit/abb2dfc92ab51b16e7172374cfee8b9c0c752454))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **boards:** unable to click on links at settings page ([#5230](https://github.com/homarr-labs/homarr/issues/5230)) ([6a200c3](https://github.com/homarr-labs/homarr/commit/6a200c39afaa1a9a1057643612065a40ad1cdd82))
|
||||||
|
* **deps:** update dependency @ctrl/deluge to ^7.5.3 ([#5365](https://github.com/homarr-labs/homarr/issues/5365)) ([9c134c5](https://github.com/homarr-labs/homarr/commit/9c134c5ceb742b21b021ef3b49de8cc0d526d460))
|
||||||
|
* **deps:** update dependency @ctrl/deluge to v8 ([#5367](https://github.com/homarr-labs/homarr/issues/5367)) ([e88e3d1](https://github.com/homarr-labs/homarr/commit/e88e3d17c01dab8836bec11d2ac01195f06cdc2e))
|
||||||
|
* **deps:** update dependency @immich/sdk to ^2.6.2 ([#5370](https://github.com/homarr-labs/homarr/issues/5370)) ([4a5bafa](https://github.com/homarr-labs/homarr/commit/4a5bafa79092b209e94a56f72362011f416bf115))
|
||||||
|
* **deps:** update dependency @immich/sdk to ^2.6.3 ([#5380](https://github.com/homarr-labs/homarr/issues/5380)) ([2dd3c90](https://github.com/homarr-labs/homarr/commit/2dd3c906c4215b55e23f4b15ba4b423e23be8376))
|
||||||
|
* **deps:** update dependency @tabler/icons-react to ^3.41.0 ([#5384](https://github.com/homarr-labs/homarr/issues/5384)) ([a9bcc9d](https://github.com/homarr-labs/homarr/commit/a9bcc9d04bdfcf28ea561cd590ca2a4bd775593c))
|
||||||
|
* **deps:** update dependency better-sqlite3 to ^12.8.0 ([#5266](https://github.com/homarr-labs/homarr/issues/5266)) ([46db8b6](https://github.com/homarr-labs/homarr/commit/46db8b6ab32406fb4ff7a7a9938692954778a070))
|
||||||
|
* **deps:** update dependency drizzle-orm to ^0.45.2 ([#5388](https://github.com/homarr-labs/homarr/issues/5388)) ([0a78e3f](https://github.com/homarr-labs/homarr/commit/0a78e3f12aa99509f42d7af647a478c5abaf5c96))
|
||||||
|
* **deps:** update dependency fast-xml-parser to ^5.5.9 ([#5359](https://github.com/homarr-labs/homarr/issues/5359)) ([189806b](https://github.com/homarr-labs/homarr/commit/189806b5a2b8e67f925cc8def6bc8ddefdc5bfc4))
|
||||||
|
* **deps:** update dependency fastify to ^5.8.4 ([#5351](https://github.com/homarr-labs/homarr/issues/5351)) ([728f7e4](https://github.com/homarr-labs/homarr/commit/728f7e489aa499bcd7e97c7ec8571e29e7e2cab3))
|
||||||
|
* **deps:** update dependency isomorphic-dompurify to ^3.7.0 ([#5366](https://github.com/homarr-labs/homarr/issues/5366)) ([c2b54dd](https://github.com/homarr-labs/homarr/commit/c2b54ddfaab93daa4e65c0c38bf5c27e4402a492))
|
||||||
|
* **deps:** update dependency isomorphic-dompurify to ^3.7.1 ([#5369](https://github.com/homarr-labs/homarr/issues/5369)) ([8d1ff40](https://github.com/homarr-labs/homarr/commit/8d1ff4065d669268f0e6a03f2b68c1ba728a756a))
|
||||||
|
* **deps:** update dependency jotai to ^2.19.0 ([#5368](https://github.com/homarr-labs/homarr/issues/5368)) ([14d8717](https://github.com/homarr-labs/homarr/commit/14d87178b5a93bdc6fb74793feed95d014814d0b))
|
||||||
|
* **deps:** update dependency node-ical to ^0.23.1 - abandoned ([#4856](https://github.com/homarr-labs/homarr/issues/4856)) ([b07c545](https://github.com/homarr-labs/homarr/commit/b07c545f17f4dafa62167047baa9eed4b7ee4395))
|
||||||
|
* **deps:** update dependency typescript-eslint to ^8.57.2 ([#5360](https://github.com/homarr-labs/homarr/issues/5360)) ([cefbf0e](https://github.com/homarr-labs/homarr/commit/cefbf0e2715e9adbc72cdb0b83d84c309e3257d6))
|
||||||
|
* **deps:** update dependency undici to v7.24.6 ([#5374](https://github.com/homarr-labs/homarr/issues/5374)) ([4f2cc9a](https://github.com/homarr-labs/homarr/commit/4f2cc9af2a42e19624def3bdea60fcb00334c483))
|
||||||
|
* **deps:** update dependency zod to ^4.3.5 - abandoned ([#4803](https://github.com/homarr-labs/homarr/issues/4803)) ([2b4c83d](https://github.com/homarr-labs/homarr/commit/2b4c83dccac168ffa6f9b6e184ed004659373cf7))
|
||||||
|
* **deps:** update dependency zod to ^4.3.6 ([#5376](https://github.com/homarr-labs/homarr/issues/5376)) ([a9582f0](https://github.com/homarr-labs/homarr/commit/a9582f04edb0e161d1fab6e64b13d0ce463ef341))
|
||||||
|
* **deps:** update tanstack-query monorepo to ^5.95.0 ([#5325](https://github.com/homarr-labs/homarr/issues/5325)) ([b11bf36](https://github.com/homarr-labs/homarr/commit/b11bf367d0567e4c8b9068804442ebd200dab01c))
|
||||||
|
* **deps:** update tanstack-query monorepo to ^5.95.1 ([#5355](https://github.com/homarr-labs/homarr/issues/5355)) ([332b6b4](https://github.com/homarr-labs/homarr/commit/332b6b4f42e7c90570cd4c25a6e6a331a76c660f))
|
||||||
|
* **deps:** update tanstack-query monorepo to ^5.95.2 ([#5357](https://github.com/homarr-labs/homarr/issues/5357)) ([6820991](https://github.com/homarr-labs/homarr/commit/682099189b689ea17e16af41fd280a72ed68016f))
|
||||||
|
* **deps:** update tiptap monorepo to v3.20.5 ([#5371](https://github.com/homarr-labs/homarr/issues/5371)) ([6820f8a](https://github.com/homarr-labs/homarr/commit/6820f8afea622587c1ee18b13d13a041ca0572ee))
|
||||||
|
* **deps:** update tiptap monorepo to v3.21.0 ([#5386](https://github.com/homarr-labs/homarr/issues/5386)) ([b30dbaf](https://github.com/homarr-labs/homarr/commit/b30dbafbbbad468ff37ff11237a64ec29df62345))
|
||||||
|
* **deps:** update trpc monorepo to ^11.15.0 ([#5354](https://github.com/homarr-labs/homarr/issues/5354)) ([2fb2981](https://github.com/homarr-labs/homarr/commit/2fb2981d2fb79ca2ca6a9bc5c08d91c8fa5710e7))
|
||||||
|
* **deps:** update trpc monorepo to ^11.15.1 ([#5381](https://github.com/homarr-labs/homarr/issues/5381)) ([451e68a](https://github.com/homarr-labs/homarr/commit/451e68ab57551e23dd7ae772b7483cc56d57ebbb))
|
||||||
|
* **deps:** update trpc monorepo to ^11.16.0 ([#5392](https://github.com/homarr-labs/homarr/issues/5392)) ([72a93e6](https://github.com/homarr-labs/homarr/commit/72a93e681f3701459176692857220730c279f80b))
|
||||||
|
* **deps:** update turbo monorepo to ^2.8.21 ([#5391](https://github.com/homarr-labs/homarr/issues/5391)) ([fa40b6b](https://github.com/homarr-labs/homarr/commit/fa40b6b647b884a7f10c268a22330e0e68b50d4d))
|
||||||
|
* honor trusted certificates in Immich integration requests ([#5275](https://github.com/homarr-labs/homarr/issues/5275)) ([7679553](https://github.com/homarr-labs/homarr/commit/7679553f16ff2945a3eb6cb668ca3a701d13d5ce)), closes [#5242](https://github.com/homarr-labs/homarr/issues/5242)
|
||||||
|
* node-ical prevents many trpc queries from loading ([#5395](https://github.com/homarr-labs/homarr/issues/5395)) ([c977b84](https://github.com/homarr-labs/homarr/commit/c977b84e2d84d5724219f5ff5f99bc550438e168))
|
||||||
|
|
||||||
|
## [1.57.1](https://github.com/homarr-labs/homarr/compare/v1.57.0...v1.57.1) (2026-03-27)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* dashdot gpu ([#5347](https://github.com/homarr-labs/homarr/issues/5347)) ([192a43a](https://github.com/homarr-labs/homarr/commit/192a43a031eb9431da2c11395eda69a6c7236b9b))
|
||||||
|
* **deps:** update dependency @t3-oss/env-nextjs to ^0.13.11 ([#5345](https://github.com/homarr-labs/homarr/issues/5345)) ([49d9394](https://github.com/homarr-labs/homarr/commit/49d9394cd78b2a1146c8dc00dd982215a2f9a580))
|
||||||
|
* **deps:** update dependency dockerode to ^4.0.10 ([#5327](https://github.com/homarr-labs/homarr/issues/5327)) ([42a3791](https://github.com/homarr-labs/homarr/commit/42a3791e6b5664a0f28d3ee4d29d5257ef3765f6))
|
||||||
|
* **deps:** update dependency fast-xml-parser to ^5.5.8 ([#5324](https://github.com/homarr-labs/homarr/issues/5324)) ([7982938](https://github.com/homarr-labs/homarr/commit/798293813cf82e38d7059069823f927b7cdfb3b8))
|
||||||
|
|
||||||
|
## [1.57.0](https://github.com/homarr-labs/homarr/compare/v1.56.1...v1.57.0) (2026-03-27)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add "allowModals" option for iframe widget ([#5339](https://github.com/homarr-labs/homarr/issues/5339)) ([2fefd9d](https://github.com/homarr-labs/homarr/commit/2fefd9da01c827160fa5fd9b63a38615ea9969e2))
|
||||||
|
* add GPU monitoring support via Glances integration ([#5255](https://github.com/homarr-labs/homarr/issues/5255)) ([003dac4](https://github.com/homarr-labs/homarr/commit/003dac4744f2e23535f1cbb7494187201af4b164)), closes [#5102](https://github.com/homarr-labs/homarr/issues/5102)
|
||||||
|
* add nextcloud notification provider ([#5343](https://github.com/homarr-labs/homarr/issues/5343)) ([f88bf95](https://github.com/homarr-labs/homarr/commit/f88bf9582e1cf45a6b7fd898e12d227869a3849f))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** update dependency @immich/sdk to ^2.6.0 ([#5318](https://github.com/homarr-labs/homarr/issues/5318)) ([20a9b3d](https://github.com/homarr-labs/homarr/commit/20a9b3d556a15e6fdf7d1dbd78b677b5ce219c75))
|
||||||
|
* **deps:** update dependency @immich/sdk to ^2.6.1 ([#5319](https://github.com/homarr-labs/homarr/issues/5319)) ([feaf022](https://github.com/homarr-labs/homarr/commit/feaf022965cf4a27112c7e3bcb9ef3ad0dd5c281))
|
||||||
|
* **deps:** update dependency @next/eslint-plugin-next to v16.1.7 ([#5289](https://github.com/homarr-labs/homarr/issues/5289)) ([237d11d](https://github.com/homarr-labs/homarr/commit/237d11d2c021e28d58b0d627d2802d509d4c1b2b))
|
||||||
|
* **deps:** update dependency @tanstack/react-query to ^5.91.0 ([#5305](https://github.com/homarr-labs/homarr/issues/5305)) ([13b99d0](https://github.com/homarr-labs/homarr/commit/13b99d021b46fe0541ce9b42ba66756fafd26c30))
|
||||||
|
* **deps:** update dependency @tanstack/react-query to ^5.91.2 ([#5314](https://github.com/homarr-labs/homarr/issues/5314)) ([6fa6ba3](https://github.com/homarr-labs/homarr/commit/6fa6ba385e6691fefbb9f045a0ff976bcd85de16))
|
||||||
|
* **deps:** update dependency drizzle-kit to ^0.31.10 ([#5291](https://github.com/homarr-labs/homarr/issues/5291)) ([09227a3](https://github.com/homarr-labs/homarr/commit/09227a3884d291032f1012b59ce06babc3c87103))
|
||||||
|
* **deps:** update dependency fast-xml-parser to ^5.5.6 ([#5297](https://github.com/homarr-labs/homarr/issues/5297)) ([991044e](https://github.com/homarr-labs/homarr/commit/991044ebd78288bea7b441d3c14b159b5e66915b))
|
||||||
|
* **deps:** update dependency fast-xml-parser to ^5.5.7 ([#5315](https://github.com/homarr-labs/homarr/issues/5315)) ([2336f9e](https://github.com/homarr-labs/homarr/commit/2336f9ee5e35f7b610743dee55b91caf8434c1e4))
|
||||||
|
* **deps:** update dependency ioredis to v5.10.1 ([#5316](https://github.com/homarr-labs/homarr/issues/5316)) ([47b2d36](https://github.com/homarr-labs/homarr/commit/47b2d36f7a0933b6a6bbb61a449888be9304e1c5))
|
||||||
|
* **deps:** update dependency isomorphic-dompurify to ^3.4.0 ([#5292](https://github.com/homarr-labs/homarr/issues/5292)) ([6276449](https://github.com/homarr-labs/homarr/commit/62764499e7bbc9995ecf3de7351c9224d6aee4b6))
|
||||||
|
* **deps:** update dependency isomorphic-dompurify to ^3.5.0 ([#5299](https://github.com/homarr-labs/homarr/issues/5299)) ([b6b11c3](https://github.com/homarr-labs/homarr/commit/b6b11c39d2b4a33c5732ae239d7c1aaba6003cc6))
|
||||||
|
* **deps:** update dependency isomorphic-dompurify to ^3.5.1 ([#5300](https://github.com/homarr-labs/homarr/issues/5300)) ([94dfddf](https://github.com/homarr-labs/homarr/commit/94dfddf8bde2966b46bcd2d30e5e4f886ebe4605))
|
||||||
|
* **deps:** update dependency isomorphic-dompurify to ^3.6.0 ([#5334](https://github.com/homarr-labs/homarr/issues/5334)) ([8d64cb8](https://github.com/homarr-labs/homarr/commit/8d64cb8a5e276677b033774152c921aedc1e6c8a))
|
||||||
|
* **deps:** update dependency mysql2 to v3.20.0 ([#5283](https://github.com/homarr-labs/homarr/issues/5283)) ([4fb48c2](https://github.com/homarr-labs/homarr/commit/4fb48c23a2ccb1c047bab0d354933561e81cc3ed))
|
||||||
|
* **deps:** update dependency swagger-ui-react to ^5.32.1 ([#5296](https://github.com/homarr-labs/homarr/issues/5296)) ([63c55ce](https://github.com/homarr-labs/homarr/commit/63c55cec5711c41544c65ab83f24d09e63b10354))
|
||||||
|
* **deps:** update dependency trpc-to-openapi to ^3.2.0 ([#5320](https://github.com/homarr-labs/homarr/issues/5320)) ([22f5d3b](https://github.com/homarr-labs/homarr/commit/22f5d3b95959cf0a835c9750b41106eaafc4432e))
|
||||||
|
* **deps:** update dependency typescript-eslint to ^8.57.1 ([#5286](https://github.com/homarr-labs/homarr/issues/5286)) ([d4e4c96](https://github.com/homarr-labs/homarr/commit/d4e4c9669d648632a055b5ec0515821b50821142))
|
||||||
|
* **deps:** update dependency undici to v7.24.5 ([#5338](https://github.com/homarr-labs/homarr/issues/5338)) ([22bbef9](https://github.com/homarr-labs/homarr/commit/22bbef9d8e03215abddf6c6e26a5df45f252626d))
|
||||||
|
* **deps:** update dependency ws to ^8.20.0 ([#5337](https://github.com/homarr-labs/homarr/issues/5337)) ([d53c922](https://github.com/homarr-labs/homarr/commit/d53c922688606bd7d9494089e1ec43b930ad9923))
|
||||||
|
* **deps:** update mantine monorepo to ^8.3.18 ([#5298](https://github.com/homarr-labs/homarr/issues/5298)) ([6f8e629](https://github.com/homarr-labs/homarr/commit/6f8e62963a4eb47cb3095dff46ee0e759e1cbd3c))
|
||||||
|
* **deps:** update nextjs monorepo to v16.2.0 ([#5309](https://github.com/homarr-labs/homarr/issues/5309)) ([8831613](https://github.com/homarr-labs/homarr/commit/883161357dab4d94ff794f9444040d88074c464c))
|
||||||
|
* **deps:** update nextjs monorepo to v16.2.1 ([#5330](https://github.com/homarr-labs/homarr/issues/5330)) ([e23a397](https://github.com/homarr-labs/homarr/commit/e23a397a80e4409f5da50c737aeb26b295ce79fc))
|
||||||
|
* **deps:** update testcontainers-node monorepo to ^11.13.0 ([#5293](https://github.com/homarr-labs/homarr/issues/5293)) ([c009dd7](https://github.com/homarr-labs/homarr/commit/c009dd7de649bf24ab9bcf5ea25c65ebd18540bb))
|
||||||
|
* **deps:** update tiptap monorepo to v3.20.4 ([#5287](https://github.com/homarr-labs/homarr/issues/5287)) ([369b4d4](https://github.com/homarr-labs/homarr/commit/369b4d486327d0eaa04b70dd692c426f9a39afd7))
|
||||||
|
* **deps:** update trpc monorepo to ^11.14.1 ([#5328](https://github.com/homarr-labs/homarr/issues/5328)) ([d7f0596](https://github.com/homarr-labs/homarr/commit/d7f0596db8e44e2e79bb26095abd142b50675cc3))
|
||||||
|
* **deps:** update turbo monorepo to ^2.8.18 ([#5308](https://github.com/homarr-labs/homarr/issues/5308)) ([24694b9](https://github.com/homarr-labs/homarr/commit/24694b92411aae818b97a3a5c9add97d707a2f38))
|
||||||
|
* **deps:** update turbo monorepo to ^2.8.19 ([#5310](https://github.com/homarr-labs/homarr/issues/5310)) ([7537184](https://github.com/homarr-labs/homarr/commit/7537184a8168a4837d0a83a925a70c938555f199))
|
||||||
|
* **deps:** update turbo monorepo to ^2.8.20 ([#5317](https://github.com/homarr-labs/homarr/issues/5317)) ([5b4e6fc](https://github.com/homarr-labs/homarr/commit/5b4e6fcc11c877815c4730607de84ab70d630fb8))
|
||||||
|
* handle null ports in Docker container table ([#5313](https://github.com/homarr-labs/homarr/issues/5313)) ([1639da5](https://github.com/homarr-labs/homarr/commit/1639da57794d2555426e56030b4b34e2cc201aa9)), closes [#5311](https://github.com/homarr-labs/homarr/issues/5311)
|
||||||
|
* warnings in code ([4f2b821](https://github.com/homarr-labs/homarr/commit/4f2b8217f819b91ec4b05e32aebd6bc5473db155))
|
||||||
|
|
||||||
|
## [1.56.1](https://github.com/homarr-labs/homarr/compare/v1.56.0...v1.56.1) (2026-03-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** update dependency dayjs to ^1.11.20 ([#5253](https://github.com/homarr-labs/homarr/issues/5253)) ([615062c](https://github.com/homarr-labs/homarr/commit/615062c6b141fa1e4bc91d2fab505fa1652608d2))
|
||||||
|
* **deps:** update dependency fast-xml-parser to ^5.5.1 ([#5246](https://github.com/homarr-labs/homarr/issues/5246)) ([91a9b6b](https://github.com/homarr-labs/homarr/commit/91a9b6bec9438e9a812960b829cea343e8478c63))
|
||||||
|
* **deps:** update dependency fast-xml-parser to ^5.5.3 ([#5251](https://github.com/homarr-labs/homarr/issues/5251)) ([d1d8659](https://github.com/homarr-labs/homarr/commit/d1d8659f951cdd22bf4a22b407a4f3027f036ec2))
|
||||||
|
* **deps:** update dependency fast-xml-parser to ^5.5.5 ([#5270](https://github.com/homarr-labs/homarr/issues/5270)) ([2423ecd](https://github.com/homarr-labs/homarr/commit/2423ecdce57353dd50d678444da800c02a246223))
|
||||||
|
* **deps:** update dependency isomorphic-dompurify to ^3.1.0 ([#5239](https://github.com/homarr-labs/homarr/issues/5239)) ([63f91b6](https://github.com/homarr-labs/homarr/commit/63f91b6352e9ba58a0823434d86915371f1c1d30))
|
||||||
|
* **deps:** update dependency isomorphic-dompurify to ^3.3.0 ([#5252](https://github.com/homarr-labs/homarr/issues/5252)) ([e8f6876](https://github.com/homarr-labs/homarr/commit/e8f6876d2477fb0e1936006b81f7324b90b6eb33))
|
||||||
|
* **deps:** update dependency jotai to ^2.18.1 ([#5238](https://github.com/homarr-labs/homarr/issues/5238)) ([729b49f](https://github.com/homarr-labs/homarr/commit/729b49ffa9b1f91ebd7bcd2300febba4d9d0fbb9))
|
||||||
|
* **deps:** update dependency mysql2 to v3.19.1 ([#5240](https://github.com/homarr-labs/homarr/issues/5240)) ([1040eca](https://github.com/homarr-labs/homarr/commit/1040ecac04fd0b10e101495d5230a3fa4898097a))
|
||||||
|
* **deps:** update dependency next to v16.1.7 [security] ([#5263](https://github.com/homarr-labs/homarr/issues/5263)) ([3bbde3c](https://github.com/homarr-labs/homarr/commit/3bbde3cbb8eb3aa528485f17d40817cb982a8687))
|
||||||
|
* **deps:** update dependency sass to ^1.98.0 ([#5247](https://github.com/homarr-labs/homarr/issues/5247)) ([f995cba](https://github.com/homarr-labs/homarr/commit/f995cba157c9954b6f266bd5b2bb8d60a08f51f1))
|
||||||
|
* **deps:** update dependency typescript-eslint to ^8.57.0 ([#5241](https://github.com/homarr-labs/homarr/issues/5241)) ([7841b58](https://github.com/homarr-labs/homarr/commit/7841b58f0fdd4a4494b6f3d61881a1efb4c9eba3))
|
||||||
|
* **deps:** update mantine monorepo to ^8.3.17 ([#5271](https://github.com/homarr-labs/homarr/issues/5271)) ([6fa9087](https://github.com/homarr-labs/homarr/commit/6fa9087a119334dee63e4ca4ed51276b25c4564a))
|
||||||
|
* **deps:** update tiptap monorepo to v3.20.2 ([#5279](https://github.com/homarr-labs/homarr/issues/5279)) ([55cf3c7](https://github.com/homarr-labs/homarr/commit/55cf3c72a3d9cd9f702e802716d549d7d2644105))
|
||||||
|
* **deps:** update trpc monorepo to ^11.13.2 ([#5273](https://github.com/homarr-labs/homarr/issues/5273)) ([7d93b10](https://github.com/homarr-labs/homarr/commit/7d93b1003245ab89fdba7175b0b4f745041cd1fe))
|
||||||
|
* **deps:** update trpc monorepo to ^11.13.3 ([#5276](https://github.com/homarr-labs/homarr/issues/5276)) ([0e0b857](https://github.com/homarr-labs/homarr/commit/0e0b857905a7b5196dbf529520f86467aa94d8ce))
|
||||||
|
* **deps:** update trpc monorepo to ^11.13.4 ([#5278](https://github.com/homarr-labs/homarr/issues/5278)) ([9648d9e](https://github.com/homarr-labs/homarr/commit/9648d9e99f02be8ba16a5238bd47866f7303724d))
|
||||||
|
* **deps:** update turbo monorepo to ^2.8.15 ([#5243](https://github.com/homarr-labs/homarr/issues/5243)) ([1bd924e](https://github.com/homarr-labs/homarr/commit/1bd924ea9917949ac64ab754d97964b12c083b82))
|
||||||
|
* **deps:** update turbo monorepo to ^2.8.16 ([#5250](https://github.com/homarr-labs/homarr/issues/5250)) ([ac80008](https://github.com/homarr-labs/homarr/commit/ac80008ce0796cb6927642c83ff2e667f6e9d24c))
|
||||||
|
* **deps:** update turbo monorepo to ^2.8.17 ([#5265](https://github.com/homarr-labs/homarr/issues/5265)) ([d2f438f](https://github.com/homarr-labs/homarr/commit/d2f438f294df3093b249213ac0a018b770705e3b))
|
||||||
|
|
||||||
|
## [1.56.0](https://github.com/homarr-labs/homarr/compare/v1.55.0...v1.56.0) (2026-03-13)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add immich integration and widget ([#5165](https://github.com/homarr-labs/homarr/issues/5165)) ([cdd90b8](https://github.com/homarr-labs/homarr/commit/cdd90b8aa091e4d05a40af35ea87486b83bf4182))
|
||||||
|
* **boards:** show supported integrations in widget selection ([#5187](https://github.com/homarr-labs/homarr/issues/5187)) ([dabe96b](https://github.com/homarr-labs/homarr/commit/dabe96bd5ea52110812934b1ec9c8dc9ecd77603))
|
||||||
|
* expand cli ([#5159](https://github.com/homarr-labs/homarr/issues/5159)) ([aa36ce3](https://github.com/homarr-labs/homarr/commit/aa36ce3c4784c91024c4e8001c236e28f02396e7))
|
||||||
|
* Tracearr integration ([#5060](https://github.com/homarr-labs/homarr/issues/5060)) ([f686001](https://github.com/homarr-labs/homarr/commit/f68600150ab1985d56f1bed45d8ac158a74ec443))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **boards:** duplication of boards fails with postgresql / mysql ([#5228](https://github.com/homarr-labs/homarr/issues/5228)) ([ba9c3f3](https://github.com/homarr-labs/homarr/commit/ba9c3f3e27659a5c06604d06192ccb8316658937))
|
||||||
|
* **deps:** update dependency @tabler/icons-react to ^3.38.0 ([#5182](https://github.com/homarr-labs/homarr/issues/5182)) ([6aaaf31](https://github.com/homarr-labs/homarr/commit/6aaaf31d8b5495090081f8795844db00a7b0a2ed))
|
||||||
|
* **deps:** update dependency @tabler/icons-react to ^3.39.0 ([#5209](https://github.com/homarr-labs/homarr/issues/5209)) ([22fd8e9](https://github.com/homarr-labs/homarr/commit/22fd8e9b95bcddd9a9a1fa599f1a42f4d502f205))
|
||||||
|
* **deps:** update dependency @tabler/icons-react to ^3.40.0 ([#5220](https://github.com/homarr-labs/homarr/issues/5220)) ([12ad241](https://github.com/homarr-labs/homarr/commit/12ad241406d581c98826ed270d71faf50779c601))
|
||||||
|
* **deps:** update dependency fast-xml-parser to ^5.4.2 ([#5190](https://github.com/homarr-labs/homarr/issues/5190)) ([5159d9f](https://github.com/homarr-labs/homarr/commit/5159d9f0e7263166f26ec770c55dd4e2944dfd72))
|
||||||
|
* **deps:** update dependency fastify to ^5.8.1 ([#5202](https://github.com/homarr-labs/homarr/issues/5202)) ([b02e8cc](https://github.com/homarr-labs/homarr/commit/b02e8cc875df20ff4d007c527bb5682db960fae6))
|
||||||
|
* **deps:** update dependency fastify to ^5.8.2 ([#5221](https://github.com/homarr-labs/homarr/issues/5221)) ([5c4f394](https://github.com/homarr-labs/homarr/commit/5c4f39450148fd91e2cecf70c20c8ec78ecebfd6))
|
||||||
|
* **deps:** update dependency mysql2 to v3.19.0 ([#5206](https://github.com/homarr-labs/homarr/issues/5206)) ([324bfd9](https://github.com/homarr-labs/homarr/commit/324bfd90d1a2c6ca80610c549b84d93f4a695cab))
|
||||||
|
* **deps:** update dependency pg to ^8.20.0 ([#5198](https://github.com/homarr-labs/homarr/issues/5198)) ([1c6e23a](https://github.com/homarr-labs/homarr/commit/1c6e23a4b6509e83667396a392b23655f8b614dd))
|
||||||
|
* **deps:** update mantine monorepo to ^8.3.16 ([#5201](https://github.com/homarr-labs/homarr/issues/5201)) ([c7fa5cd](https://github.com/homarr-labs/homarr/commit/c7fa5cdbb3ffdf90dbc9c9dbef0551e2b0edfb4c))
|
||||||
|
* **deps:** update tiptap monorepo to v3.20.1 ([#5215](https://github.com/homarr-labs/homarr/issues/5215)) ([d941ed5](https://github.com/homarr-labs/homarr/commit/d941ed58fa482f2e41d27b6b5088146a608b236e))
|
||||||
|
* **deps:** update trpc monorepo to ^11.12.0 ([#5213](https://github.com/homarr-labs/homarr/issues/5213)) ([00dd32b](https://github.com/homarr-labs/homarr/commit/00dd32bbe124f02ad7ae5ecb4e298a6d3fb4f0b8))
|
||||||
|
* **deps:** update turbo monorepo to ^2.8.14 ([#5113](https://github.com/homarr-labs/homarr/issues/5113)) ([8ee05e0](https://github.com/homarr-labs/homarr/commit/8ee05e0d491bfec9f768a3a915ad1c8abf433b5b))
|
||||||
|
* **glances:** unable to parse uptime of 1 day ([#5226](https://github.com/homarr-labs/homarr/issues/5226)) ([3091ed6](https://github.com/homarr-labs/homarr/commit/3091ed6358d83249a94827f55a812323c1e80497))
|
||||||
|
* **image-proxy:** hash full URL and headers deterministically ([#5184](https://github.com/homarr-labs/homarr/issues/5184)) ([9a878af](https://github.com/homarr-labs/homarr/commit/9a878af414e12691420edb945ed6f9f6255c7eae))
|
||||||
|
* language changes when searching for some icons ([#5224](https://github.com/homarr-labs/homarr/issues/5224)) ([21225c9](https://github.com/homarr-labs/homarr/commit/21225c979edb9a2c9cc00c4649363c0aab8058f1))
|
||||||
|
* **releases:** allow any icon url for repositories ([#5229](https://github.com/homarr-labs/homarr/issues/5229)) ([d3c2ad1](https://github.com/homarr-labs/homarr/commit/d3c2ad134f7a9972118abcfff5482ca6d6105406))
|
||||||
|
* **security:** harden hashing in image-proxy ([#5188](https://github.com/homarr-labs/homarr/issues/5188)) ([5723da2](https://github.com/homarr-labs/homarr/commit/5723da21aae8b69642a39e4b5a05ad84fa8dd8ef))
|
||||||
|
* **stock-price:** align sparkline color with displayed price change ([#5181](https://github.com/homarr-labs/homarr/issues/5181)) ([bbf0adf](https://github.com/homarr-labs/homarr/commit/bbf0adf31a1f08f3ca09b709efd1daa15cb8498c))
|
||||||
|
* **system-disks:** format absolute denominator with human-readable size ([#5183](https://github.com/homarr-labs/homarr/issues/5183)) ([85c4601](https://github.com/homarr-labs/homarr/commit/85c460190ceedad83c40af81d5efa270bc464b94))
|
||||||
|
* **true-nas:** ignore offline/exported pools ([#5225](https://github.com/homarr-labs/homarr/issues/5225)) ([451602d](https://github.com/homarr-labs/homarr/commit/451602d1e61256696f6bcb0cd76f09748345a940))
|
||||||
|
|
||||||
|
## [1.55.0](https://github.com/homarr-labs/homarr/compare/v1.54.0...v1.55.0) (2026-03-06)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add seerr integration ([#5139](https://github.com/homarr-labs/homarr/issues/5139)) ([0e29a2f](https://github.com/homarr-labs/homarr/commit/0e29a2fce172cbfda083b899c01b92bd8b6210b7))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** update dependency fast-xml-parser to ^5.4.1 ([#5150](https://github.com/homarr-labs/homarr/issues/5150)) ([eb2f85f](https://github.com/homarr-labs/homarr/commit/eb2f85f6f8c9d7d90c1aeec079225678dfef4dd0))
|
||||||
|
* **deps:** update dependency ioredis to v5.10.0 ([#5169](https://github.com/homarr-labs/homarr/issues/5169)) ([1c06e56](https://github.com/homarr-labs/homarr/commit/1c06e562c33bd9f1387cb05a3dece7beedfe43c2))
|
||||||
|
* **deps:** update dependency ldapts to v8.1.7 ([#5168](https://github.com/homarr-labs/homarr/issues/5168)) ([ec45aac](https://github.com/homarr-labs/homarr/commit/ec45aacc63766c51a99ead6130bade442124e70d))
|
||||||
|
* **deps:** update dependency mysql2 to v3.17.5 ([#5140](https://github.com/homarr-labs/homarr/issues/5140)) ([66ad67e](https://github.com/homarr-labs/homarr/commit/66ad67eced587fd49a470395298477dc09cdd225))
|
||||||
|
* **deps:** update dependency mysql2 to v3.18.0 ([#5141](https://github.com/homarr-labs/homarr/issues/5141)) ([52792af](https://github.com/homarr-labs/homarr/commit/52792af186a6eb3519aafdee5a7c0dd03a86a461))
|
||||||
|
* **deps:** update dependency mysql2 to v3.18.1 ([#5152](https://github.com/homarr-labs/homarr/issues/5152)) ([8bf2a43](https://github.com/homarr-labs/homarr/commit/8bf2a43e065671c7c119ef1da5aa09d284db0ede))
|
||||||
|
* **deps:** update dependency mysql2 to v3.18.2 ([#5164](https://github.com/homarr-labs/homarr/issues/5164)) ([4717a57](https://github.com/homarr-labs/homarr/commit/4717a5724d4a6286c0f2bb92cf1e1fe17903a13a))
|
||||||
|
* **deps:** update dependency pg to ^8.19.0 ([#5153](https://github.com/homarr-labs/homarr/issues/5153)) ([0b0fb1c](https://github.com/homarr-labs/homarr/commit/0b0fb1c5798357d7bc847dfe577b26529be8aa5c))
|
||||||
|
* **deps:** update dependency swagger-ui-react to ^5.32.0 ([#5167](https://github.com/homarr-labs/homarr/issues/5167)) ([ff11599](https://github.com/homarr-labs/homarr/commit/ff1159943c967b6fea44894765c2210c3cdd44cc))
|
||||||
|
* **deps:** update dependency typescript-eslint to ^8.56.1 ([#5142](https://github.com/homarr-labs/homarr/issues/5142)) ([ac3b4e7](https://github.com/homarr-labs/homarr/commit/ac3b4e7de0460cab7723dcc1a5a8e14072a37d25))
|
||||||
|
* **deps:** update trpc monorepo to ^11.11.0 ([#5174](https://github.com/homarr-labs/homarr/issues/5174)) ([bc2a532](https://github.com/homarr-labs/homarr/commit/bc2a532a03d8cb76270f52b766f1a7f46d704096))
|
||||||
|
|
||||||
## [1.54.0](https://github.com/homarr-labs/homarr/compare/v1.53.2...v1.54.0) (2026-02-27)
|
## [1.54.0](https://github.com/homarr-labs/homarr/compare/v1.53.2...v1.54.0) (2026-02-27)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24.13.1-alpine AS base
|
FROM node:24.14.1-alpine AS base
|
||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
@@ -52,6 +52,7 @@ COPY --from=builder /app/packages/db/migrations ./db/migrations
|
|||||||
# Automatically leverage output traces to reduce image size
|
# Automatically leverage output traces to reduce image size
|
||||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
COPY --from=builder /app/apps/nextjs/.next/standalone ./
|
COPY --from=builder /app/apps/nextjs/.next/standalone ./
|
||||||
|
COPY --from=builder /app/apps/nextjs/socket-cleanup.cjs ./apps/nextjs/socket-cleanup.cjs
|
||||||
COPY --from=builder /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
|
COPY --from=builder /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
|
||||||
COPY --from=builder /app/apps/nextjs/public ./apps/nextjs/public
|
COPY --from=builder /app/apps/nextjs/public ./apps/nextjs/public
|
||||||
COPY scripts/run.sh ./run.sh
|
COPY scripts/run.sh ./run.sh
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const withNextIntl = createNextIntlPlugin({
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
compress: false, // Workaround for vercel/next.js#89091 - zlib retains ServerResponse after client disconnect, causing CLOSE-WAIT socket leak
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
// react compiler breaks mantine-react-table, so disabled for now
|
// react compiler breaks mantine-react-table, so disabled for now
|
||||||
//reactCompiler: true,
|
//reactCompiler: true,
|
||||||
|
|||||||
+57
-56
@@ -15,10 +15,10 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "catalog:",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "catalog:",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "catalog:",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "catalog:",
|
||||||
"@homarr/analytics": "workspace:^0.1.0",
|
"@homarr/analytics": "workspace:^0.1.0",
|
||||||
"@homarr/api": "workspace:^0.1.0",
|
"@homarr/api": "workspace:^0.1.0",
|
||||||
"@homarr/auth": "workspace:^0.1.0",
|
"@homarr/auth": "workspace:^0.1.0",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"@homarr/docker": "workspace:^0.1.0",
|
"@homarr/docker": "workspace:^0.1.0",
|
||||||
"@homarr/form": "workspace:^0.1.0",
|
"@homarr/form": "workspace:^0.1.0",
|
||||||
"@homarr/forms-collection": "workspace:^0.1.0",
|
"@homarr/forms-collection": "workspace:^0.1.0",
|
||||||
"@homarr/gridstack": "^1.12.0",
|
"@homarr/gridstack": "catalog:",
|
||||||
"@homarr/icons": "workspace:^0.1.0",
|
"@homarr/icons": "workspace:^0.1.0",
|
||||||
"@homarr/image-proxy": "workspace:^0.1.0",
|
"@homarr/image-proxy": "workspace:^0.1.0",
|
||||||
"@homarr/integrations": "workspace:^0.1.0",
|
"@homarr/integrations": "workspace:^0.1.0",
|
||||||
@@ -48,61 +48,62 @@
|
|||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@homarr/widgets": "workspace:^0.1.0",
|
"@homarr/widgets": "workspace:^0.1.0",
|
||||||
"@mantine/colors-generator": "^8.3.15",
|
"@mantine/colors-generator": "catalog:",
|
||||||
"@mantine/core": "^8.3.15",
|
"@mantine/core": "catalog:",
|
||||||
"@mantine/dropzone": "^8.3.15",
|
"@mantine/dropzone": "catalog:",
|
||||||
"@mantine/hooks": "^8.3.15",
|
"@mantine/hooks": "catalog:",
|
||||||
"@mantine/modals": "^8.3.15",
|
"@mantine/modals": "catalog:",
|
||||||
"@mantine/tiptap": "^8.3.15",
|
"@mantine/tiptap": "catalog:",
|
||||||
"@million/lint": "1.0.14",
|
"@million/lint": "catalog:",
|
||||||
"@tabler/icons-react": "^3.37.1",
|
"@tabler/icons-react": "catalog:",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "catalog:",
|
||||||
"@tanstack/react-query-devtools": "^5.91.3",
|
"@tanstack/react-query-devtools": "catalog:",
|
||||||
"@tanstack/react-query-next-experimental": "^5.91.0",
|
"@tanstack/react-query-next-experimental": "catalog:",
|
||||||
"@trpc/client": "^11.10.0",
|
"@trpc/client": "catalog:",
|
||||||
"@trpc/next": "^11.10.0",
|
"@trpc/next": "catalog:",
|
||||||
"@trpc/react-query": "^11.10.0",
|
"@trpc/react-query": "catalog:",
|
||||||
"@trpc/server": "^11.10.0",
|
"@trpc/server": "catalog:",
|
||||||
"@xterm/addon-canvas": "^0.7.0",
|
"@xterm/addon-canvas": "catalog:",
|
||||||
"@xterm/addon-fit": "0.11.0",
|
"@xterm/addon-fit": "catalog:",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "catalog:",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "catalog:",
|
||||||
"chroma-js": "^3.2.0",
|
"chroma-js": "catalog:",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "catalog:",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "catalog:",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "catalog:",
|
||||||
"flag-icons": "^7.5.0",
|
"flag-icons": "catalog:",
|
||||||
"glob": "^13.0.6",
|
"glob": "catalog:",
|
||||||
"isomorphic-dompurify": "^3.0.0",
|
"isomorphic-dompurify": "catalog:",
|
||||||
"jotai": "^2.18.0",
|
"jotai": "catalog:",
|
||||||
"mantine-react-table": "2.0.0-beta.9",
|
"mantine-react-table": "catalog:",
|
||||||
"next": "16.1.6",
|
"next": "catalog:",
|
||||||
"postcss-preset-mantine": "^1.18.0",
|
"postcss-preset-mantine": "catalog:",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "catalog:",
|
||||||
"react": "19.2.4",
|
"react": "catalog:",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "catalog:",
|
||||||
"react-error-boundary": "^6.1.1",
|
"react-error-boundary": "catalog:",
|
||||||
"react-simple-code-editor": "^0.14.1",
|
"react-simple-code-editor": "catalog:",
|
||||||
"sass": "^1.97.3",
|
"sass": "catalog:",
|
||||||
"superjson": "2.2.6",
|
"superjson": "catalog:",
|
||||||
"swagger-ui-react": "^5.31.2",
|
"swagger-ui-react": "catalog:",
|
||||||
"use-deep-compare-effect": "^1.8.1",
|
"use-deep-compare-effect": "catalog:",
|
||||||
"zod": "^4.2.1"
|
"yaml": "catalog:",
|
||||||
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/chroma-js": "3.1.2",
|
"@types/chroma-js": "catalog:",
|
||||||
"@types/node": "^24.10.13",
|
"@types/node": "catalog:",
|
||||||
"@types/prismjs": "^1.26.6",
|
"@types/prismjs": "catalog:",
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "catalog:",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "catalog:",
|
||||||
"@types/swagger-ui-react": "^5.18.0",
|
"@types/swagger-ui-react": "catalog:",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "catalog:",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "catalog:",
|
||||||
"node-loader": "^2.1.0",
|
"node-loader": "catalog:",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "catalog:",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Periodic cleanup of zombie CLOSE-WAIT sockets in Next.js standalone mode.
|
||||||
|
// Workaround for vercel/next.js#89091 — AfterContext retains ServerResponse
|
||||||
|
// after client disconnect, leaving sockets in CLOSE-WAIT indefinitely.
|
||||||
|
//
|
||||||
|
// Usage: node --require ./socket-cleanup.cjs server.js
|
||||||
|
|
||||||
|
const INTERVAL_MS = 60_000; // Check every 60 seconds
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
let destroyed = 0;
|
||||||
|
for (const handle of process._getActiveHandles()) {
|
||||||
|
if (
|
||||||
|
handle.constructor?.name === "Socket" &&
|
||||||
|
handle.localPort === 3000 &&
|
||||||
|
handle.readyState === "writeOnly" &&
|
||||||
|
!handle._httpMessage &&
|
||||||
|
handle.bytesWritten === 0
|
||||||
|
) {
|
||||||
|
handle.destroy();
|
||||||
|
destroyed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (destroyed > 0) {
|
||||||
|
console.log(`[socket-cleanup] Destroyed ${destroyed} zombie socket(s)`);
|
||||||
|
}
|
||||||
|
}, INTERVAL_MS).unref(); // unref() so this timer doesn't prevent process exit
|
||||||
@@ -7,7 +7,7 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|||||||
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
|
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
|
||||||
import {
|
import {
|
||||||
createWSClient,
|
createWSClient,
|
||||||
httpBatchStreamLink,
|
httpBatchLink,
|
||||||
httpLink,
|
httpLink,
|
||||||
isNonJsonSerializable,
|
isNonJsonSerializable,
|
||||||
loggerLink,
|
loggerLink,
|
||||||
@@ -90,7 +90,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
|
|||||||
url: getTrpcUrl(),
|
url: getTrpcUrl(),
|
||||||
headers: createHeadersCallbackForSource("nextjs-react (form-data)"),
|
headers: createHeadersCallbackForSource("nextjs-react (form-data)"),
|
||||||
}),
|
}),
|
||||||
false: httpBatchStreamLink({
|
false: httpBatchLink({
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
url: getTrpcUrl(),
|
url: getTrpcUrl(),
|
||||||
maxURLLength: 2083, // Suggested by tRPC: https://trpc.io/docs/client/links/httpBatchLink#setting-a-maximum-url-length
|
maxURLLength: 2083, // Suggested by tRPC: https://trpc.io/docs/client/links/httpBatchLink#setting-a-maximum-url-length
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import type { useForm } from "@homarr/form";
|
|||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
import { sanitizeRedirectionUrl } from "@homarr/validation/redirection-url";
|
||||||
import { userSignInSchema } from "@homarr/validation/user";
|
import { userSignInSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
type Provider = "credentials" | "ldap" | "oidc";
|
type Provider = "credentials" | "ldap" | "oidc";
|
||||||
@@ -70,7 +71,7 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
|
|||||||
|
|
||||||
// Redirect to the callback URL if the response is defined and comes from a credentials provider (ldap or credentials). oidc is redirected automatically.
|
// Redirect to the callback URL if the response is defined and comes from a credentials provider (ldap or credentials). oidc is redirected automatically.
|
||||||
await revalidatePathActionAsync("/");
|
await revalidatePathActionAsync("/");
|
||||||
router.push(callbackUrl);
|
router.push(sanitizeRedirectionUrl(callbackUrl));
|
||||||
},
|
},
|
||||||
[t, router, callbackUrl],
|
[t, router, callbackUrl],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Card, Center, Stack, Text, Title } from "@mantine/core";
|
|||||||
import { env } from "@homarr/auth/env";
|
import { env } from "@homarr/auth/env";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { getScopedI18n } from "@homarr/translation/server";
|
import { getScopedI18n } from "@homarr/translation/server";
|
||||||
|
import { sanitizeRedirectionUrl } from "@homarr/validation/redirection-url";
|
||||||
|
|
||||||
import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
|
import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
|
||||||
import { LoginForm } from "./_login-form";
|
import { LoginForm } from "./_login-form";
|
||||||
@@ -19,7 +20,7 @@ export default async function Login(props: LoginProps) {
|
|||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
redirect(searchParams.callbackUrl ?? "/");
|
redirect(sanitizeRedirectionUrl(searchParams.callbackUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = await getScopedI18n("user.page.login");
|
const t = await getScopedI18n("user.page.login");
|
||||||
|
|||||||
@@ -264,6 +264,5 @@ const usePreventLeaveWithDirty = (isDirty: boolean) => {
|
|||||||
window.removeEventListener("popstate", handlePopState);
|
window.removeEventListener("popstate", handlePopState);
|
||||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [isDirty]);
|
}, [isDirty]);
|
||||||
};
|
};
|
||||||
@@ -27,7 +27,6 @@ export const BoardReadyProvider = ({ children }: PropsWithChildren) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setReadySections((previous) => previous.filter((id) => board.sections.some((section) => section.id === id)));
|
setReadySections((previous) => previous.filter((id) => board.sections.some((section) => section.id === id)));
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [board.sections.length, setReadySections]);
|
}, [board.sections.length, setReadySections]);
|
||||||
|
|
||||||
const markAsReady = useCallback((id: string) => {
|
const markAsReady = useCallback((id: string) => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { Button, Grid, Group, Loader, Stack, TextInput } from "@mantine/core";
|
import { Button, Grid, Group, Loader, Stack, TextInput } from "@mantine/core";
|
||||||
import { useDebouncedValue, useDocumentTitle, useFavicon } from "@mantine/hooks";
|
import { useDebouncedValue, useDocumentTitle } from "@mantine/hooks";
|
||||||
|
|
||||||
import { useUpdateBoard } from "@homarr/boards/updater";
|
import { useUpdateBoard } from "@homarr/boards/updater";
|
||||||
import { useZodForm } from "@homarr/form";
|
import { useZodForm } from "@homarr/form";
|
||||||
@@ -10,6 +10,7 @@ import { IconPicker } from "@homarr/forms-collection";
|
|||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
|
import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
|
||||||
|
|
||||||
|
import { homarrLogoPath } from "~/components/layout/logo/homarr-logo";
|
||||||
import { createMetaTitle } from "~/metadata";
|
import { createMetaTitle } from "~/metadata";
|
||||||
import type { Board } from "../../_types";
|
import type { Board } from "../../_types";
|
||||||
import { useSavePartialSettingsMutation } from "./_shared";
|
import { useSavePartialSettingsMutation } from "./_shared";
|
||||||
@@ -53,7 +54,6 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useLogoPreview(form.values.logoImageUrl);
|
useLogoPreview(form.values.logoImageUrl);
|
||||||
useFaviconPreview(form.values.faviconImageUrl);
|
|
||||||
const metaTitleStatus = useMetaTitlePreview(form.values.metaTitle);
|
const metaTitleStatus = useMetaTitlePreview(form.values.metaTitle);
|
||||||
|
|
||||||
// Cleanup for not applied changes of the page title and logo image URL
|
// Cleanup for not applied changes of the page title and logo image URL
|
||||||
@@ -75,6 +75,7 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
|||||||
pageTitle: values.pageTitle,
|
pageTitle: values.pageTitle,
|
||||||
logoImageUrl: values.logoImageUrl,
|
logoImageUrl: values.logoImageUrl,
|
||||||
};
|
};
|
||||||
|
updateFavicon(values.faviconImageUrl ?? homarrLogoPath);
|
||||||
savePartialSettings({
|
savePartialSettings({
|
||||||
id: board.id,
|
id: board.id,
|
||||||
...values,
|
...values,
|
||||||
@@ -146,7 +147,13 @@ const useMetaTitlePreview = (title: string | null) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const useFaviconPreview = (url: string | null) => {
|
// Previously we used the useFavicon hook from Mantine
|
||||||
const [faviconDebounced] = useDebouncedValue(url ?? "", 500);
|
// However if the user tried to click on a link to a different page
|
||||||
useFavicon(faviconDebounced);
|
// it caused an error "Cannot read properties of null (reading 'removeChild')"
|
||||||
|
// Probably because the head element was null (https://github.com/mantinedev/mantine/blob/b90d9b81031f45f5d15e75f25138ed6477f65bce/packages/%40mantine/hooks/src/use-favicon/use-favicon.ts#L21)
|
||||||
|
// See https://github.com/homarr-labs/homarr/issues/4905
|
||||||
|
const updateFavicon = (url: string) => {
|
||||||
|
const link: HTMLLinkElement | null = document.querySelector('link[rel="icon"]');
|
||||||
|
if (!link) return;
|
||||||
|
link.href = url;
|
||||||
};
|
};
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
.contributorCard {
|
.contributorCard {
|
||||||
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dependencyCard:hover {
|
||||||
|
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
||||||
|
}
|
||||||
@@ -12,8 +12,7 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
Kbd,
|
Kbd,
|
||||||
List,
|
SimpleGrid,
|
||||||
ListItem,
|
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
TableTbody,
|
TableTbody,
|
||||||
@@ -22,9 +21,11 @@ import {
|
|||||||
TableThead,
|
TableThead,
|
||||||
TableTr,
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
|
UnstyledButton,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconKeyboard, IconLanguage, IconLibrary, IconUsers } from "@tabler/icons-react";
|
import { IconKeyboard, IconLanguage, IconLibrary, IconPackage, IconUsers } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { capitalize, objectEntries } from "@homarr/common";
|
import { capitalize, objectEntries } from "@homarr/common";
|
||||||
import { hotkeys } from "@homarr/definitions";
|
import { hotkeys } from "@homarr/definitions";
|
||||||
@@ -33,7 +34,8 @@ import { getScopedI18n } from "@homarr/translation/server";
|
|||||||
import { homarrLogoPath } from "~/components/layout/logo/homarr-logo";
|
import { homarrLogoPath } from "~/components/layout/logo/homarr-logo";
|
||||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||||
import { createMetaTitle } from "~/metadata";
|
import { createMetaTitle } from "~/metadata";
|
||||||
import { getPackageAttributesAsync } from "~/versions/package-reader";
|
import type { PackageJsonDependencies } from "~/versions/package-reader";
|
||||||
|
import { getPackageVersion } from "~/versions/package-reader";
|
||||||
import type githubContributorsJson from "../../../../../../../static-data/contributors.json";
|
import type githubContributorsJson from "../../../../../../../static-data/contributors.json";
|
||||||
import type crowdinContributorsJson from "../../../../../../../static-data/translators.json";
|
import type crowdinContributorsJson from "../../../../../../../static-data/translators.json";
|
||||||
import classes from "./about.module.css";
|
import classes from "./about.module.css";
|
||||||
@@ -57,7 +59,10 @@ const getHostAsync = async () => {
|
|||||||
export default async function AboutPage() {
|
export default async function AboutPage() {
|
||||||
const baseServerUrl = `http://${await getHostAsync()}`;
|
const baseServerUrl = `http://${await getHostAsync()}`;
|
||||||
const t = await getScopedI18n("management.page.about");
|
const t = await getScopedI18n("management.page.about");
|
||||||
const attributes = await getPackageAttributesAsync();
|
const version = getPackageVersion();
|
||||||
|
const dependencies = (await fetch(`${baseServerUrl}/api/about/dependencies`).then((res) =>
|
||||||
|
res.json(),
|
||||||
|
)) as PackageJsonDependencies;
|
||||||
const githubContributors = (await fetch(`${baseServerUrl}/api/about/contributors/github`).then((res) =>
|
const githubContributors = (await fetch(`${baseServerUrl}/api/about/contributors/github`).then((res) =>
|
||||||
res.json(),
|
res.json(),
|
||||||
)) as typeof githubContributorsJson;
|
)) as typeof githubContributorsJson;
|
||||||
@@ -76,7 +81,7 @@ export default async function AboutPage() {
|
|||||||
<Title order={1} tt="uppercase">
|
<Title order={1} tt="uppercase">
|
||||||
Homarr
|
Homarr
|
||||||
</Title>
|
</Title>
|
||||||
<Title order={2}>{t("version", { version: attributes.version })}</Title>
|
<Title order={2}>{t("version", { version })}</Title>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Center>
|
</Center>
|
||||||
@@ -137,25 +142,42 @@ export default async function AboutPage() {
|
|||||||
<Text>{t("accordion.libraries.title")}</Text>
|
<Text>{t("accordion.libraries.title")}</Text>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{t("accordion.libraries.subtitle", {
|
{t("accordion.libraries.subtitle", {
|
||||||
count: String(Object.keys(attributes.dependencies).length),
|
count: String(Object.keys(dependencies).length),
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</AccordionControl>
|
</AccordionControl>
|
||||||
<AccordionPanel>
|
<AccordionPanel>
|
||||||
<List>
|
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3, lg: 4, xl: 5 }} spacing="md">
|
||||||
{Object.entries(attributes.dependencies)
|
{Object.entries(dependencies)
|
||||||
|
.filter(([, value]) => !value.includes("workspace:"))
|
||||||
.sort(([key1], [key2]) => key1.localeCompare(key2))
|
.sort(([key1], [key2]) => key1.localeCompare(key2))
|
||||||
.map(([key, value]) => (
|
.map(([name, version]) => (
|
||||||
<ListItem key={key}>
|
<UnstyledButton
|
||||||
{value.includes("workspace:") ? (
|
key={name}
|
||||||
<Text>{key}</Text>
|
component="a"
|
||||||
) : (
|
href={`https://www.npmjs.com/package/${name}`}
|
||||||
<a href={`https://www.npmjs.com/package/${key}`}>{key}</a>
|
target="_blank"
|
||||||
)}
|
>
|
||||||
</ListItem>
|
<Card withBorder radius="md" p="xs" className={classes.dependencyCard}>
|
||||||
|
<Group gap="sm" wrap="nowrap">
|
||||||
|
<ThemeIcon variant="light" size="lg" radius="md">
|
||||||
|
<IconPackage size="1.5rem" stroke={1.5} />
|
||||||
|
</ThemeIcon>
|
||||||
|
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Text size="sm" fw="bold" lineClamp={1} title={name}>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
v{version.replace("^", "").replace("~", "")}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
</UnstyledButton>
|
||||||
))}
|
))}
|
||||||
</List>
|
</SimpleGrid>
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem value="hotkeys">
|
<AccordionItem value="hotkeys">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { ApiKeysManagement } from "./components/api-keys";
|
|||||||
|
|
||||||
export async function generateMetadata() {
|
export async function generateMetadata() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session?.user || !session.user.permissions.includes("admin")) {
|
if (!session?.user.permissions.includes("admin")) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export async function generateMetadata() {
|
|||||||
|
|
||||||
export default async function ApiPage() {
|
export default async function ApiPage() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session?.user || !session.user.permissions.includes("admin")) {
|
if (!session?.user.permissions.includes("admin")) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
const document = openApiDocument(extractBaseUrlFromHeaders(await headers()));
|
const document = openApiDocument(extractBaseUrlFromHeaders(await headers()));
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ const createColumns = (
|
|||||||
accessorKey: "ports",
|
accessorKey: "ports",
|
||||||
header: t("docker.field.ports.label"),
|
header: t("docker.field.ports.label"),
|
||||||
Cell({ cell }) {
|
Cell({ cell }) {
|
||||||
|
if (!cell.row.original.ports) return null;
|
||||||
if (!cell.row.original.ports.length) return null;
|
if (!cell.row.original.ports.length) return null;
|
||||||
return (
|
return (
|
||||||
<OverflowBadge overflowCount={1} data={cell.row.original.ports.map((port) => port.PrivatePort.toString())} />
|
<OverflowBadge overflowCount={1} data={cell.row.original.ports.map((port) => port.PrivatePort.toString())} />
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { LogContextProvider } from "./log-context";
|
|||||||
|
|
||||||
export async function generateMetadata() {
|
export async function generateMetadata() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session?.user || !session.user.permissions.includes("admin")) {
|
if (!session?.user.permissions.includes("admin")) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const t = await getScopedI18n("management");
|
const t = await getScopedI18n("management");
|
||||||
@@ -30,7 +30,7 @@ export async function generateMetadata() {
|
|||||||
|
|
||||||
export default async function LogsManagementPage() {
|
export default async function LogsManagementPage() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session?.user || !session.user.permissions.includes("other-view-logs")) {
|
if (!session?.user.permissions.includes("other-view-logs")) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { getDependenciesAsync } from "../../../../versions/package-reader";
|
||||||
|
|
||||||
|
export const GET = async () => {
|
||||||
|
const dependencies = await getDependenciesAsync();
|
||||||
|
return NextResponse.json(dependencies);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
@@ -75,7 +75,6 @@ const executeHealthCheckSafelyAsync = async (
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
||||||
logger.error(new ErrorWithMetadata("Healthcheck failed", { name }, { cause: error }));
|
logger.error(new ErrorWithMetadata("Healthcheck failed", { name }, { cause: error }));
|
||||||
return {
|
return {
|
||||||
status: "unhealthy",
|
status: "unhealthy",
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
|
|||||||
when={
|
when={
|
||||||
widgetSupportsIntegrations &&
|
widgetSupportsIntegrations &&
|
||||||
item.integrationIds.length === 0 &&
|
item.integrationIds.length === 0 &&
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
(!("integrationsRequired" in definition) || definition.integrationsRequired !== false)
|
(!("integrationsRequired" in definition) || definition.integrationsRequired !== false)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core";
|
import { Avatar, Button, Card, Center, Grid, Group, Input, Stack, Text, Tooltip } from "@mantine/core";
|
||||||
import { IconSearch } from "@tabler/icons-react";
|
import { IconSearch } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { objectEntries } from "@homarr/common";
|
import { objectEntries } from "@homarr/common";
|
||||||
import type { WidgetKind } from "@homarr/definitions";
|
import { getIconUrl, getIntegrationName } from "@homarr/definitions";
|
||||||
|
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal } from "@homarr/modals";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import type { TablerIcon } from "@homarr/ui";
|
import type { TablerIcon } from "@homarr/ui";
|
||||||
@@ -21,6 +22,10 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
|
|||||||
objectEntries(widgetImports)
|
objectEntries(widgetImports)
|
||||||
.map(([kind, value]) => ({
|
.map(([kind, value]) => ({
|
||||||
kind,
|
kind,
|
||||||
|
supportedIntegrations:
|
||||||
|
"supportedIntegrations" in value.definition
|
||||||
|
? value.definition.supportedIntegrations.filter((integration) => integration !== "mock")
|
||||||
|
: [],
|
||||||
icon: value.definition.icon,
|
icon: value.definition.icon,
|
||||||
name: t(`widget.${kind}.name`),
|
name: t(`widget.${kind}.name`),
|
||||||
description: t(`widget.${kind}.description`),
|
description: t(`widget.${kind}.description`),
|
||||||
@@ -74,6 +79,7 @@ const WidgetItem = ({
|
|||||||
}: {
|
}: {
|
||||||
item: {
|
item: {
|
||||||
kind: WidgetKind;
|
kind: WidgetKind;
|
||||||
|
supportedIntegrations: IntegrationKind[];
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
icon: TablerIcon;
|
icon: TablerIcon;
|
||||||
@@ -97,7 +103,8 @@ const WidgetItem = ({
|
|||||||
{item.description}
|
{item.description}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Button onClick={onSelect} variant="light" size="xs" mt="auto" radius="md" fullWidth>
|
<SupportedIntegrations mt="auto" integrations={item.supportedIntegrations} />
|
||||||
|
<Button onClick={onSelect} variant="light" size="xs" radius="md" fullWidth>
|
||||||
{t(`item.create.addToBoard`)}
|
{t(`item.create.addToBoard`)}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -105,3 +112,48 @@ const WidgetItem = ({
|
|||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface SupportedIntegrationsProps {
|
||||||
|
integrations: IntegrationKind[];
|
||||||
|
mt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SupportedIntegrations = ({ integrations, mt }: SupportedIntegrationsProps) => {
|
||||||
|
if (integrations.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When there are 8 or more integrations, we show 6 and a "+X" avatar. Otherwise, we show all integrations.
|
||||||
|
const countToShow = integrations.length >= 8 ? 6 : 7;
|
||||||
|
const moreCount = integrations.length - countToShow;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Center mt={mt}>
|
||||||
|
<Tooltip.Group openDelay={300} closeDelay={100}>
|
||||||
|
<Group gap={2}>
|
||||||
|
{integrations.slice(0, countToShow).map((integration) => (
|
||||||
|
<Tooltip key={integration} label={getIntegrationName(integration)} withArrow>
|
||||||
|
<Avatar src={getIconUrl(integration)} size="xs" radius="xl" />
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
{moreCount > 0 && (
|
||||||
|
<Tooltip
|
||||||
|
withArrow
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
{integrations.slice(countToShow).map((integration) => (
|
||||||
|
<div key={integration}>{getIntegrationName(integration)}</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Avatar radius="xl" size="xs">
|
||||||
|
+{moreCount}
|
||||||
|
</Avatar>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Tooltip.Group>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -206,7 +206,6 @@ export const useGridstack = (section: Omit<Section, "items">, itemIds: string[])
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only run this effect when the section items change
|
// Only run this effect when the section items change
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [itemIds.length, columnCount]);
|
}, [itemIds.length, columnCount]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import fsPromises from "fs/promises";
|
import fsPromises from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
import { glob } from "glob";
|
import { glob } from "glob";
|
||||||
|
import { parse as parseYaml } from "yaml";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
import packageJson from "../../../../package.json";
|
import packageJson from "../../../../package.json";
|
||||||
|
|
||||||
const getPackageVersion = () => packageJson.version;
|
export const getPackageVersion = () => packageJson.version;
|
||||||
const getDependenciesAsync = async (): Promise<PackageJsonDependencies> => {
|
export const getDependenciesAsync = async (): Promise<PackageJsonDependencies> => {
|
||||||
const pathNames = await glob("**/package.json", {
|
const pathNames = await glob("**/package.json", {
|
||||||
ignore: "node_modules/**",
|
ignore: "**/node_modules/**",
|
||||||
cwd: "../../",
|
cwd: "../../",
|
||||||
absolute: true,
|
absolute: true,
|
||||||
});
|
});
|
||||||
@@ -15,21 +19,37 @@ const getDependenciesAsync = async (): Promise<PackageJsonDependencies> => {
|
|||||||
.map((packageContent) => (JSON.parse(packageContent) as PackageJson).dependencies)
|
.map((packageContent) => (JSON.parse(packageContent) as PackageJson).dependencies)
|
||||||
.filter((dependencies) => dependencies !== undefined);
|
.filter((dependencies) => dependencies !== undefined);
|
||||||
|
|
||||||
|
const catalog = await parseDependencyCatalogsAsync();
|
||||||
|
|
||||||
let dependencies = {};
|
let dependencies = {};
|
||||||
for (const dependenciesOfPackage of packageDependencies) {
|
for (const dependenciesOfPackage of packageDependencies) {
|
||||||
dependencies = { ...dependencies, ...dependenciesOfPackage };
|
const resolvedDependencies = Object.entries(dependenciesOfPackage).map(([name, version]) => {
|
||||||
|
const catalogVersion = catalog.get(name);
|
||||||
|
return [name, catalogVersion ?? version] as const;
|
||||||
|
});
|
||||||
|
dependencies = { ...dependencies, ...Object.fromEntries(resolvedDependencies) };
|
||||||
}
|
}
|
||||||
return dependencies;
|
return dependencies;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPackageAttributesAsync = async () => {
|
type DependencyCatalog = Map<string, string>;
|
||||||
return {
|
const parseDependencyCatalogsAsync = async (): Promise<DependencyCatalog> => {
|
||||||
version: getPackageVersion(),
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
dependencies: await getDependenciesAsync(),
|
const workspaceConfigContent = await fsPromises.readFile(
|
||||||
};
|
path.join(currentDir, "..", "..", "..", "..", "pnpm-workspace.yaml"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
const workspaceConfig: unknown = parseYaml(workspaceConfigContent);
|
||||||
|
const parseResult = workspaceConfigCatalogSchema.parse(workspaceConfig);
|
||||||
|
|
||||||
|
return new Map(Object.entries(parseResult.catalog ?? {}));
|
||||||
};
|
};
|
||||||
|
|
||||||
type PackageJsonDependencies = Record<string, string>;
|
const workspaceConfigCatalogSchema = z.object({
|
||||||
|
catalog: z.record(z.string(), z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type PackageJsonDependencies = Record<string, string>;
|
||||||
interface PackageJson {
|
interface PackageJson {
|
||||||
dependencies: PackageJsonDependencies | undefined;
|
dependencies: PackageJsonDependencies | undefined;
|
||||||
}
|
}
|
||||||
+12
-12
@@ -37,22 +37,22 @@
|
|||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@homarr/widgets": "workspace:^0.1.0",
|
"@homarr/widgets": "workspace:^0.1.0",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "catalog:",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "catalog:",
|
||||||
"fastify": "^5.7.4",
|
"fastify": "catalog:",
|
||||||
"superjson": "2.2.6",
|
"superjson": "catalog:",
|
||||||
"undici": "7.18.2"
|
"undici": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node": "^24.10.13",
|
"@types/node": "catalog:",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "catalog:",
|
||||||
"esbuild": "^0.27.3",
|
"esbuild": "catalog:",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "catalog:",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "catalog:",
|
||||||
"tsx": "4.20.4",
|
"tsx": "catalog:",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,18 +25,18 @@
|
|||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "catalog:",
|
||||||
"tsx": "4.20.4",
|
"tsx": "catalog:",
|
||||||
"ws": "^8.19.0"
|
"ws": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "catalog:",
|
||||||
"esbuild": "^0.27.3",
|
"esbuild": "catalog:",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "catalog:",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "catalog:",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24.13.1-trixie AS base
|
FROM node:24.14.1-trixie AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json .
|
COPY package.json .
|
||||||
COPY pnpm-lock.yaml .
|
COPY pnpm-lock.yaml .
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
"name": "homarr-prebuilt-debian",
|
"name": "homarr-prebuilt-debian",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^12.6.2"
|
"better-sqlite3": "^12.8.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.30.1",
|
"packageManager": "pnpm@10.33.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.13.1",
|
"node": ">=24.14.1",
|
||||||
"pnpm": ">=10.30.1"
|
"pnpm": ">=10.33.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|||||||
+6
-5
@@ -9,16 +9,16 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
better-sqlite3:
|
better-sqlite3:
|
||||||
specifier: ^12.6.2
|
specifier: ^12.8.0
|
||||||
version: 12.6.2
|
version: 12.8.0
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
base64-js@1.5.1:
|
base64-js@1.5.1:
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
|
||||||
better-sqlite3@12.6.2:
|
better-sqlite3@12.8.0:
|
||||||
resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==}
|
resolution: {integrity: sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==}
|
||||||
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
|
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
|
||||||
|
|
||||||
bindings@1.5.0:
|
bindings@1.5.0:
|
||||||
@@ -93,6 +93,7 @@ packages:
|
|||||||
prebuild-install@7.1.3:
|
prebuild-install@7.1.3:
|
||||||
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
|
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
pump@3.0.3:
|
pump@3.0.3:
|
||||||
@@ -147,7 +148,7 @@ snapshots:
|
|||||||
|
|
||||||
base64-js@1.5.1: {}
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
better-sqlite3@12.6.2:
|
better-sqlite3@12.8.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
bindings: 1.5.0
|
bindings: 1.5.0
|
||||||
prebuild-install: 7.1.3
|
prebuild-install: 7.1.3
|
||||||
|
|||||||
+62
-20
@@ -64,6 +64,13 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
|
<a href="https://homarr.dev/docs/integrations/anchor" target="_blank" rel="noreferrer noopener">
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/anchor.svg" alt="Anchor" width="90" height="90" />
|
||||||
|
<br/>
|
||||||
|
<p align="center">Anchor</p>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/aria2" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/aria2" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/PapirusDevelopmentTeam/papirus_icons@latest/src/system_downloads_3.svg" alt="Aria2" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/PapirusDevelopmentTeam/papirus_icons@latest/src/system_downloads_3.svg" alt="Aria2" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -97,15 +104,15 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">Deluge</p>
|
<p align="center">Deluge</p>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
<td align="center">
|
<tr><td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/docker-hub" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/docker-hub" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/docker.svg" alt="Docker Hub" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/docker.svg" alt="Docker Hub" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">Docker<br/>Hub</p>
|
<p align="center">Docker<br/>Hub</p>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr><td align="center">
|
<td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/emby" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/emby" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/emby.svg" alt="Emby" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/emby.svg" alt="Emby" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -146,15 +153,22 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">Home<br/>Assistant</p>
|
<p align="center">Home<br/>Assistant</p>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
<td align="center">
|
<tr><td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/ical" target="_blank" rel="noreferrer noopener">
|
<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" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/ical.svg" alt="iCal" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">iCal</p>
|
<p align="center">iCal</p>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr><td align="center">
|
<td align="center">
|
||||||
|
<a href="https://homarr.dev/docs/integrations/immich" target="_blank" rel="noreferrer noopener">
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/immich.svg" alt="Immich" width="90" height="90" />
|
||||||
|
<br/>
|
||||||
|
<p align="center">Immich</p>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/jellyfin" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/jellyfin" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg" alt="Jellyfin" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg" alt="Jellyfin" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -188,8 +202,8 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">Nextcloud</p>
|
<p align="center">Nextcloud</p>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
<td align="center">
|
<tr><td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/npm" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/npm" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/npm.svg" alt="NPM" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/npm.svg" alt="NPM" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -202,8 +216,8 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">ntfy</p>
|
<p align="center">ntfy</p>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr><td align="center">
|
<td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/nzbget" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/nzbget" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nzbget.svg" alt="NZBGet" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nzbget.svg" alt="NZBGet" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -237,8 +251,8 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">Pi-hole</p>
|
<p align="center">Pi-hole</p>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
<td align="center">
|
<tr><td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/plex" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/plex" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/plex.svg" alt="Plex" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/plex.svg" alt="Plex" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -251,8 +265,8 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">Prowlarr</p>
|
<p align="center">Prowlarr</p>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr><td align="center">
|
<td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/proxmox" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/proxmox" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/proxmox.svg" alt="Proxmox" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/proxmox.svg" alt="Proxmox" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -286,8 +300,8 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">Readarr</p>
|
<p align="center">Readarr</p>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
<td align="center">
|
<tr><td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/sabnzbd" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/sabnzbd" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sabnzbd.svg" alt="SABnzbd" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sabnzbd.svg" alt="SABnzbd" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -295,13 +309,27 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
|
<a href="https://homarr.dev/docs/integrations/search-ch" target="_blank" rel="noreferrer noopener">
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/search-ch.png" alt="Search.ch" width="90" height="90" />
|
||||||
|
<br/>
|
||||||
|
<p align="center">Search.ch</p>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://homarr.dev/docs/integrations/seerr" target="_blank" rel="noreferrer noopener">
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/seerr.svg" alt="Seerr" width="90" height="90" />
|
||||||
|
<br/>
|
||||||
|
<p align="center">Seerr</p>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/slskd" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/slskd" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/slskd.svg" alt="Slskd" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/slskd.svg" alt="Slskd" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">Slskd</p>
|
<p align="center">Slskd</p>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr><td align="center">
|
<td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/sonarr" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/sonarr" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sonarr.svg" alt="Sonarr" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sonarr.svg" alt="Sonarr" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -309,11 +337,25 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
|
<a href="https://homarr.dev/docs/integrations/speedtest-tracker" target="_blank" rel="noreferrer noopener">
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/speedtest-tracker.png" alt="Speedtest Tracker" width="90" height="90" />
|
||||||
|
<br/>
|
||||||
|
<p align="center">Speedtest<br/>Tracker</p>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/tdarr" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/tdarr" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/tdarr.png" alt="Tdarr" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/tdarr.png" alt="Tdarr" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
<p align="center">Tdarr</p>
|
<p align="center">Tdarr</p>
|
||||||
</a>
|
</a>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td align="center">
|
||||||
|
<a href="https://homarr.dev/docs/integrations/tracearr" target="_blank" rel="noreferrer noopener">
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/tracearr.svg" alt="Tracearr" width="90" height="90" />
|
||||||
|
<br/>
|
||||||
|
<p align="center">Tracearr</p>
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/transmission" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/transmission" target="_blank" rel="noreferrer noopener">
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ describe("Health checks", () => {
|
|||||||
// Assert
|
// Assert
|
||||||
expect(readyResponse.status).toBe(200);
|
expect(readyResponse.status).toBe(200);
|
||||||
expect(liveResponse.status).toBe(200);
|
expect(liveResponse.status).toBe(200);
|
||||||
}, 20_000);
|
}, 60_000);
|
||||||
|
|
||||||
test("ready and live should return 200 OK with external redis", async () => {
|
test("ready and live should return 200 OK with external redis", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -42,5 +42,5 @@ describe("Health checks", () => {
|
|||||||
liveResponse.status,
|
liveResponse.status,
|
||||||
`Expected live to return OK statusCode=${liveResponse.status} content=${await liveResponse.text()}`,
|
`Expected live to return OK statusCode=${liveResponse.status} content=${await liveResponse.text()}`,
|
||||||
).toBe(200);
|
).toBe(200);
|
||||||
}, 20_000);
|
}, 60_000);
|
||||||
});
|
});
|
||||||
+1
-1
@@ -12,5 +12,5 @@ describe("Home", () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(homeResponse.status).toBe(200);
|
expect(homeResponse.status).toBe(200);
|
||||||
}, 20_000);
|
}, 60_000);
|
||||||
});
|
});
|
||||||
@@ -18,6 +18,11 @@ export const createHomarrContainer = (
|
|||||||
.withExposedPorts(7575)
|
.withExposedPorts(7575)
|
||||||
.withEnvironment({
|
.withEnvironment({
|
||||||
...options.environment,
|
...options.environment,
|
||||||
|
// We disable external connections due to the following reasons:
|
||||||
|
// - No icons have to be downloaded (11k)
|
||||||
|
// - No analytics data is sent
|
||||||
|
// - It is just faster and makes the tests more reliable (better performance)
|
||||||
|
NO_EXTERNAL_CONNECTION: true.toString(),
|
||||||
SECRET_ENCRYPTION_KEY: "0".repeat(64),
|
SECRET_ENCRYPTION_KEY: "0".repeat(64),
|
||||||
})
|
})
|
||||||
.withBindMounts(
|
.withBindMounts(
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ events {
|
|||||||
}
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_vary on;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 7575;
|
listen 7575;
|
||||||
listen [::]:7575;
|
listen [::]:7575;
|
||||||
|
|||||||
+40
-40
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "1.54.0",
|
"version": "1.59.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env CI=true turbo build",
|
"build": "cross-env CI=true turbo build",
|
||||||
@@ -36,33 +36,33 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@semantic-release/changelog": "^6.0.3",
|
"@semantic-release/changelog": "catalog:",
|
||||||
"@semantic-release/commit-analyzer": "^13.0.1",
|
"@semantic-release/commit-analyzer": "catalog:",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/git": "catalog:",
|
||||||
"@semantic-release/github": "^12.0.6",
|
"@semantic-release/github": "catalog:",
|
||||||
"@semantic-release/npm": "^13.1.4",
|
"@semantic-release/npm": "catalog:",
|
||||||
"@semantic-release/release-notes-generator": "^14.1.0",
|
"@semantic-release/release-notes-generator": "catalog:",
|
||||||
"@testcontainers/redis": "^11.12.0",
|
"@testcontainers/redis": "catalog:",
|
||||||
"@turbo/gen": "^2.8.9",
|
"@turbo/gen": "catalog:",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "catalog:",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "catalog:",
|
||||||
"@vitest/ui": "^4.0.18",
|
"@vitest/ui": "catalog:",
|
||||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
"conventional-changelog-conventionalcommits": "catalog:",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "catalog:",
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "catalog:",
|
||||||
"json5": "^2.2.3",
|
"json5": "catalog:",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "catalog:",
|
||||||
"semantic-release": "^25.0.3",
|
"semantic-release": "catalog:",
|
||||||
"testcontainers": "^11.12.0",
|
"testcontainers": "catalog:",
|
||||||
"turbo": "^2.8.9",
|
"turbo": "catalog:",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "catalog:",
|
||||||
"vite-tsconfig-paths": "^6.1.1",
|
"vite-tsconfig-paths": "catalog:",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "catalog:"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.30.1",
|
"packageManager": "pnpm@10.33.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.13.1",
|
"node": ">=24.14.1",
|
||||||
"pnpm": ">=10.30.1"
|
"pnpm": ">=10.33.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
@@ -77,25 +77,25 @@
|
|||||||
"tree-sitter-json"
|
"tree-sitter-json"
|
||||||
],
|
],
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@babel/helpers@<7.26.10": ">=7.28.6",
|
"@babel/helpers@<7.26.10": ">=7.29.2",
|
||||||
"@babel/runtime@<7.26.10": ">=7.28.6",
|
"@babel/runtime@<7.26.10": ">=7.29.2",
|
||||||
"axios@>=1.0.0 <1.8.2": ">=1.13.5",
|
"axios@>=1.0.0 <1.8.2": ">=1.15.0",
|
||||||
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.3",
|
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.5",
|
||||||
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
|
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.5",
|
||||||
"esbuild@<=0.24.2": ">=0.27.3",
|
"esbuild@<=0.24.2": ">=0.28.0",
|
||||||
"form-data@>=4.0.0 <4.0.4": ">=4.0.5",
|
"form-data@>=4.0.0 <4.0.4": ">=4.0.5",
|
||||||
"hono@<4.6.5": ">=4.11.9",
|
"hono@<4.6.5": ">=4.12.12",
|
||||||
"linkifyjs@<4.3.2": ">=4.3.2",
|
"linkifyjs@<4.3.2": ">=4.3.2",
|
||||||
"nanoid@>=4.0.0 <5.0.9": ">=5.1.6",
|
"nanoid@>=4.0.0 <5.0.9": ">=5.1.7",
|
||||||
"prismjs@<1.30.0": ">=1.30.0",
|
"prismjs@<1.30.0": ">=1.30.0",
|
||||||
"proxmox-api>undici": "7.19.2",
|
"proxmox-api>undici": "7.24.6",
|
||||||
"react-is": "^19.2.4",
|
"react-is": "^19.2.5",
|
||||||
"rollup@>=4.0.0 <4.22.4": ">=4.59.0",
|
"rollup@>=4.0.0 <4.22.4": ">=4.60.1",
|
||||||
"sha.js@<=2.4.11": ">=2.4.12",
|
"sha.js@<=2.4.11": ">=2.4.12",
|
||||||
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
|
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.2",
|
||||||
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
|
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.2",
|
||||||
"tmp@<=0.2.3": ">=0.2.5",
|
"tmp@<=0.2.3": ">=0.2.5",
|
||||||
"vite@>=5.0.0 <=5.4.18": ">=7.3.1"
|
"vite@>=5.0.0 <=5.4.18": ">=8.0.8"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@types/node-unifi": "patches/@types__node-unifi.patch",
|
"@types/node-unifi": "patches/@types__node-unifi.patch",
|
||||||
|
|||||||
@@ -28,14 +28,14 @@
|
|||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/docker": "workspace:^0.1.0",
|
"@homarr/docker": "workspace:^0.1.0",
|
||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@umami/node": "^0.4.0",
|
"@umami/node": "catalog:",
|
||||||
"superjson": "2.2.6"
|
"superjson": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "catalog:",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+16
-16
@@ -40,26 +40,26 @@
|
|||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@kubernetes/client-node": "^1.4.0",
|
"@kubernetes/client-node": "catalog:",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "catalog:",
|
||||||
"@trpc/client": "^11.10.0",
|
"@trpc/client": "catalog:",
|
||||||
"@trpc/react-query": "^11.10.0",
|
"@trpc/react-query": "catalog:",
|
||||||
"@trpc/server": "^11.10.0",
|
"@trpc/server": "catalog:",
|
||||||
"@trpc/tanstack-react-query": "^11.10.0",
|
"@trpc/tanstack-react-query": "catalog:",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "catalog:",
|
||||||
"next": "16.1.6",
|
"next": "catalog:",
|
||||||
"react": "19.2.4",
|
"react": "catalog:",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "catalog:",
|
||||||
"superjson": "2.2.6",
|
"superjson": "catalog:",
|
||||||
"trpc-to-openapi": "^3.1.0",
|
"trpc-to-openapi": "catalog:",
|
||||||
"zod": "^4.2.1"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "catalog:",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "catalog:",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
import { hashPasswordAsync } from "@homarr/auth";
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
import { generateSecureRandomToken } from "@homarr/common/server";
|
import { generateSecureRandomToken } from "@homarr/common/server";
|
||||||
import { db, eq } from "@homarr/db";
|
import { db, eq } from "@homarr/db";
|
||||||
@@ -14,7 +14,6 @@ export const apiKeysRouter = createTRPCRouter({
|
|||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
apiKey: false,
|
apiKey: false,
|
||||||
salt: false,
|
|
||||||
},
|
},
|
||||||
with: {
|
with: {
|
||||||
user: {
|
user: {
|
||||||
@@ -29,14 +28,12 @@ export const apiKeysRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
create: permissionRequiredProcedure.requiresPermission("admin").mutation(async ({ ctx }) => {
|
create: permissionRequiredProcedure.requiresPermission("admin").mutation(async ({ ctx }) => {
|
||||||
const salt = await createSaltAsync();
|
|
||||||
const randomToken = generateSecureRandomToken(64);
|
const randomToken = generateSecureRandomToken(64);
|
||||||
const hashedRandomToken = await hashPasswordAsync(randomToken, salt);
|
const hashedRandomToken = await hashPasswordAsync(randomToken);
|
||||||
const id = createId();
|
const id = createId();
|
||||||
await db.insert(apiKeys).values({
|
await db.insert(apiKeys).values({
|
||||||
id,
|
id,
|
||||||
apiKey: hashedRandomToken,
|
apiKey: hashedRandomToken,
|
||||||
salt,
|
|
||||||
userId: ctx.session.user.id,
|
userId: ctx.session.user.id,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -442,7 +442,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
await handleTransactionsAsync(ctx.db, {
|
await handleTransactionsAsync(ctx.db, {
|
||||||
async handleAsync(db, schema) {
|
async handleAsync(db, schema) {
|
||||||
await db.transaction(async (transaction) => {
|
await db.transaction(async (transaction) => {
|
||||||
transaction.insert(schema.boards).values({
|
await transaction.insert(schema.boards).values({
|
||||||
...boardProps,
|
...boardProps,
|
||||||
id: newBoardId,
|
id: newBoardId,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ interface DockerContainer {
|
|||||||
id: string;
|
id: string;
|
||||||
state: ContainerState;
|
state: ContainerState;
|
||||||
image: string;
|
image: string;
|
||||||
ports: Port[];
|
ports: Port[] | undefined;
|
||||||
iconUrl: string | null;
|
iconUrl: string | null;
|
||||||
cpuUsage: number;
|
cpuUsage: number;
|
||||||
memoryUsage: number;
|
memoryUsage: number;
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ async function setupUrlRestrictionAsync(dbUrls: string[]) {
|
|||||||
hideDescription: false,
|
hideDescription: false,
|
||||||
textLinesClamp: 3,
|
textLinesClamp: 3,
|
||||||
maximumAmountPosts: 5,
|
maximumAmountPosts: 5,
|
||||||
|
showPosterImage: true,
|
||||||
} satisfies WidgetComponentProps<"rssFeed">["options"]),
|
} satisfies WidgetComponentProps<"rssFeed">["options"]),
|
||||||
}) satisfies InferInsertModel<typeof items>,
|
}) satisfies InferInsertModel<typeof items>,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import type { MySqlRawQueryResult } from "drizzle-orm/mysql2";
|
||||||
|
import type { QueryResult } from "pg";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
import { comparePasswordsAsync, hashPasswordAsync } from "@homarr/auth";
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { and, eq, like } from "@homarr/db";
|
import { and, eq, handleTransactionsAsync, like } from "@homarr/db";
|
||||||
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
||||||
import { boards, groupMembers, groupPermissions, groups, invites, users } from "@homarr/db/schema";
|
import { boards, groupMembers, groupPermissions, groups, invites, users } from "@homarr/db/schema";
|
||||||
import { selectUserSchema } from "@homarr/db/validationSchemas";
|
import { selectUserSchema } from "@homarr/db/validationSchemas";
|
||||||
import { credentialsAdminGroup } from "@homarr/definitions";
|
|
||||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||||
|
import { credentialsAdminGroup } from "@homarr/definitions";
|
||||||
import { byIdSchema } from "@homarr/validation/common";
|
import { byIdSchema } from "@homarr/validation/common";
|
||||||
import type { userBaseCreateSchema } from "@homarr/validation/user";
|
import type { userBaseCreateSchema } from "@homarr/validation/user";
|
||||||
import {
|
import {
|
||||||
@@ -90,15 +92,58 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, "credentials", input.username);
|
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, "credentials", input.username);
|
||||||
|
|
||||||
await createUserAsync(ctx.db, input);
|
const hashedPassword = await hashPasswordAsync(input.password);
|
||||||
|
|
||||||
// Delete invite as it's used
|
const userId = createId();
|
||||||
await ctx.db.delete(invites).where(inviteWhere);
|
const user = {
|
||||||
|
id: userId,
|
||||||
|
name: input.username,
|
||||||
|
password: hashedPassword,
|
||||||
|
};
|
||||||
|
|
||||||
|
await handleTransactionsAsync(ctx.db, {
|
||||||
|
async handleAsync(db, schema) {
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
await trx.insert(schema.users).values(user);
|
||||||
|
|
||||||
|
// Delete invite as it's used
|
||||||
|
const queryResult = (await trx.delete(schema.invites).where(inviteWhere)) as
|
||||||
|
| QueryResult
|
||||||
|
| MySqlRawQueryResult;
|
||||||
|
let count = 0;
|
||||||
|
if (Array.isArray(queryResult)) {
|
||||||
|
count = queryResult[0].affectedRows;
|
||||||
|
} else {
|
||||||
|
count = queryResult.rowCount ?? 0;
|
||||||
|
}
|
||||||
|
if (count === 0) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Invalid invite",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSync(db) {
|
||||||
|
db.transaction((trx) => {
|
||||||
|
trx.insert(users).values(user).run();
|
||||||
|
// Delete invite as it's used
|
||||||
|
const result = trx.delete(invites).where(inviteWhere).run();
|
||||||
|
|
||||||
|
if (result.changes === 0) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Invalid invite",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
create: permissionRequiredProcedure
|
create: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.meta({ openapi: { method: "POST", path: "/api/users", tags: ["users"], protect: true } })
|
.meta({ openapi: { method: "POST", path: "/api/users", tags: ["users"], protect: true } })
|
||||||
.input(userCreateSchema)
|
.input(convertIntersectionToZodObject(userCreateSchema))
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
throwIfCredentialsDisabled();
|
throwIfCredentialsDisabled();
|
||||||
@@ -329,7 +374,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
await ctx.db.delete(users).where(eq(users.id, input.userId));
|
await ctx.db.delete(users).where(eq(users.id, input.userId));
|
||||||
}),
|
}),
|
||||||
changePassword: protectedProcedure
|
changePassword: protectedProcedure
|
||||||
.input(userChangePasswordApiSchema)
|
.input(convertIntersectionToZodObject(userChangePasswordApiSchema))
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.meta({ openapi: { method: "PATCH", path: "/api/users/{userId}/changePassword", tags: ["users"], protect: true } })
|
.meta({ openapi: { method: "PATCH", path: "/api/users/{userId}/changePassword", tags: ["users"], protect: true } })
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
@@ -346,7 +391,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
password: true,
|
password: true,
|
||||||
salt: true,
|
|
||||||
provider: true,
|
provider: true,
|
||||||
},
|
},
|
||||||
where: eq(users.id, input.userId),
|
where: eq(users.id, input.userId),
|
||||||
@@ -376,8 +420,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isPreviousPasswordRequired) {
|
if (isPreviousPasswordRequired) {
|
||||||
const previousPasswordHash = await hashPasswordAsync(input.previousPassword, dbUser.salt ?? "");
|
const isValid = await comparePasswordsAsync(input.previousPassword, dbUser.password ?? "");
|
||||||
const isValid = previousPasswordHash === dbUser.password;
|
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -387,8 +430,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const salt = await createSaltAsync();
|
const hashedPassword = await hashPasswordAsync(input.password);
|
||||||
const hashedPassword = await hashPasswordAsync(input.password, salt);
|
|
||||||
await ctx.db
|
await ctx.db
|
||||||
.update(users)
|
.update(users)
|
||||||
.set({
|
.set({
|
||||||
@@ -536,8 +578,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const createUserAsync = async (db: Database, input: Omit<z.infer<typeof userBaseCreateSchema>, "groupIds">) => {
|
const createUserAsync = async (db: Database, input: Omit<z.infer<typeof userBaseCreateSchema>, "groupIds">) => {
|
||||||
const salt = await createSaltAsync();
|
const hashedPassword = await hashPasswordAsync(input.password);
|
||||||
const hashedPassword = await hashPasswordAsync(input.password, salt);
|
|
||||||
|
|
||||||
const userId = createId();
|
const userId = createId();
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
@@ -545,7 +586,6 @@ const createUserAsync = async (db: Database, input: Omit<z.infer<typeof userBase
|
|||||||
name: input.username,
|
name: input.username,
|
||||||
email: input.email,
|
email: input.email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
salt,
|
|
||||||
});
|
});
|
||||||
return userId;
|
return userId;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
import { ResponseError } from "@homarr/common/server";
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { anchorNotesListInputSchema, anchorNoteUpdateInputSchema, createIntegrationAsync } from "@homarr/integrations";
|
||||||
|
import { anchorNoteRequestHandler, anchorNotesListRequestHandler } from "@homarr/request-handler/anchor-notes";
|
||||||
|
|
||||||
|
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
const noteIdInput = z.object({
|
||||||
|
noteId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "anchorNotesRouter" });
|
||||||
|
|
||||||
|
const isJsonDeltaString = (value: string) => {
|
||||||
|
try {
|
||||||
|
const parsed: unknown = JSON.parse(value);
|
||||||
|
return typeof parsed === "object" && parsed !== null && Array.isArray((parsed as { ops?: unknown }).ops);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeAnchorContent = (content: string | undefined) => {
|
||||||
|
if (content === undefined) return undefined;
|
||||||
|
|
||||||
|
const trimmed = content.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return JSON.stringify({ ops: [{ insert: "\n" }] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isJsonDeltaString(content)) return content;
|
||||||
|
|
||||||
|
const normalizedText = content.endsWith("\n") ? content : `${content}\n`;
|
||||||
|
return JSON.stringify({ ops: [{ insert: normalizedText }] });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const anchorNotesRouter = createTRPCRouter({
|
||||||
|
listNotes: publicProcedure
|
||||||
|
.concat(createOneIntegrationMiddleware("query", "anchor"))
|
||||||
|
.input(anchorNotesListInputSchema)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const handler = anchorNotesListRequestHandler.handler(ctx.integration, { ...input, limit: input.limit ?? 50 });
|
||||||
|
|
||||||
|
const { data } = await handler.getCachedOrUpdatedDataAsync({
|
||||||
|
forceUpdate: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
getNote: publicProcedure
|
||||||
|
.concat(createOneIntegrationMiddleware("query", "anchor"))
|
||||||
|
.input(noteIdInput)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const handler = anchorNoteRequestHandler.handler(ctx.integration, { noteId: input.noteId });
|
||||||
|
|
||||||
|
const { data } = await handler.getCachedOrUpdatedDataAsync({
|
||||||
|
forceUpdate: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
updateNote: protectedProcedure
|
||||||
|
.concat(createOneIntegrationMiddleware("interact", "anchor"))
|
||||||
|
.input(anchorNoteUpdateInputSchema)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const integrationInstance = await createIntegrationAsync(ctx.integration);
|
||||||
|
const normalizedContent = normalizeAnchorContent(input.content);
|
||||||
|
const isContentNormalized =
|
||||||
|
input.content !== undefined && normalizedContent !== undefined && input.content !== normalizedContent;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedNote = await integrationInstance.updateNoteAsync({
|
||||||
|
noteId: input.noteId,
|
||||||
|
...(input.title !== undefined ? { title: input.title } : {}),
|
||||||
|
...(normalizedContent !== undefined ? { content: normalizedContent } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const noteHandler = anchorNoteRequestHandler.handler(ctx.integration, { noteId: input.noteId });
|
||||||
|
await noteHandler.invalidateAsync();
|
||||||
|
|
||||||
|
return updatedNote;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ResponseError && error.statusCode === 400) {
|
||||||
|
logger.warn("Anchor update note failed validation", {
|
||||||
|
noteId: input.noteId,
|
||||||
|
integrationId: ctx.integration.id,
|
||||||
|
contentLength: input.content?.length ?? 0,
|
||||||
|
contentIsJsonDelta: input.content ? isJsonDeltaString(input.content) : false,
|
||||||
|
contentNormalized: isContentNormalized,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof ResponseError && error.statusCode === 403) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "You do not have permission to edit this note",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof ResponseError && error.statusCode === 404) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Note not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
|
import {
|
||||||
|
immichAlbumRequestHandler,
|
||||||
|
immichAlbumsRequestHandler,
|
||||||
|
immichStatsRequestHandler,
|
||||||
|
} from "@homarr/request-handler/immich";
|
||||||
|
|
||||||
|
import type { IntegrationAction } from "../../middlewares/integration";
|
||||||
|
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
const createImmichIntegrationMiddleware = (action: IntegrationAction) =>
|
||||||
|
createOneIntegrationMiddleware(action, ...getIntegrationKindsByCategory("photoService"));
|
||||||
|
|
||||||
|
export const immichRouter = createTRPCRouter({
|
||||||
|
getServerStats: publicProcedure.concat(createImmichIntegrationMiddleware("query")).query(async ({ ctx }) => {
|
||||||
|
const innerHandler = immichStatsRequestHandler.handler(ctx.integration, {});
|
||||||
|
const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
|
return data.data;
|
||||||
|
}),
|
||||||
|
|
||||||
|
getAlbum: publicProcedure
|
||||||
|
.concat(createImmichIntegrationMiddleware("query"))
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
albumId: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const innerHandler = immichAlbumRequestHandler.handler(ctx.integration, { albumId: input.albumId });
|
||||||
|
const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
|
return data.data;
|
||||||
|
}),
|
||||||
|
|
||||||
|
getAlbums: publicProcedure.concat(createImmichIntegrationMiddleware("query")).query(async ({ ctx }) => {
|
||||||
|
const innerHandler = immichAlbumsRequestHandler.handler(ctx.integration, {});
|
||||||
|
const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
|
return data.data;
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -3,7 +3,9 @@ import { lazy } from "@trpc/server";
|
|||||||
import { createTRPCRouter } from "../../trpc";
|
import { createTRPCRouter } from "../../trpc";
|
||||||
|
|
||||||
export const widgetRouter = createTRPCRouter({
|
export const widgetRouter = createTRPCRouter({
|
||||||
|
anchorNotes: lazy(() => import("./anchor-notes").then((mod) => mod.anchorNotesRouter)),
|
||||||
coolify: lazy(() => import("./coolify").then((mod) => mod.coolifyRouter)),
|
coolify: lazy(() => import("./coolify").then((mod) => mod.coolifyRouter)),
|
||||||
|
immich: lazy(() => import("./immich").then((mod) => mod.immichRouter)),
|
||||||
notebook: lazy(() => import("./notebook").then((mod) => mod.notebookRouter)),
|
notebook: lazy(() => import("./notebook").then((mod) => mod.notebookRouter)),
|
||||||
weather: lazy(() => import("./weather").then((mod) => mod.weatherRouter)),
|
weather: lazy(() => import("./weather").then((mod) => mod.weatherRouter)),
|
||||||
app: lazy(() => import("./app").then((mod) => mod.appRouter)),
|
app: lazy(() => import("./app").then((mod) => mod.appRouter)),
|
||||||
@@ -25,4 +27,7 @@ export const widgetRouter = createTRPCRouter({
|
|||||||
networkController: lazy(() => import("./network-controller").then((mod) => mod.networkControllerRouter)),
|
networkController: lazy(() => import("./network-controller").then((mod) => mod.networkControllerRouter)),
|
||||||
firewall: lazy(() => import("./firewall").then((mod) => mod.firewallRouter)),
|
firewall: lazy(() => import("./firewall").then((mod) => mod.firewallRouter)),
|
||||||
notifications: lazy(() => import("./notifications").then((mod) => mod.notificationsRouter)),
|
notifications: lazy(() => import("./notifications").then((mod) => mod.notificationsRouter)),
|
||||||
|
timetable: lazy(() => import("./timetable").then((mod) => mod.timetableRouter)),
|
||||||
|
tracearr: lazy(() => import("./tracearr").then((mod) => mod.tracearrRouter)),
|
||||||
|
speedtestTracker: lazy(() => import("./speedtest-tracker").then((mod) => mod.speedtestTrackerRouter)),
|
||||||
});
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { observable } from "@trpc/server/observable";
|
||||||
import SuperJSON from "superjson";
|
import SuperJSON from "superjson";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
@@ -5,45 +6,59 @@ import type { Session } from "@homarr/auth";
|
|||||||
import type { Database } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
import { eq } from "@homarr/db";
|
import { eq } from "@homarr/db";
|
||||||
import { items, users } from "@homarr/db/schema";
|
import { items, users } from "@homarr/db/schema";
|
||||||
|
import type { ExtendedFeedEntry } from "@homarr/request-handler/rss-feeds";
|
||||||
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
|
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../../../../widgets/src";
|
import type { WidgetComponentProps } from "../../../../widgets/src";
|
||||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
const feedsInput = z.object({
|
||||||
|
urls: z.array(z.string()).max(100),
|
||||||
|
maximumAmountPosts: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
export const rssFeedRouter = createTRPCRouter({
|
export const rssFeedRouter = createTRPCRouter({
|
||||||
getFeeds: publicProcedure
|
getFeeds: publicProcedure.input(feedsInput).query(async ({ ctx, input }) => {
|
||||||
.input(
|
const urls = (await canAccessAllFeedsAsync(ctx.db, ctx.session))
|
||||||
z.object({
|
? input.urls
|
||||||
urls: z.array(z.string()).max(100),
|
: await restrictUrlsAsync(ctx.db, input.urls);
|
||||||
maximumAmountPosts: z.number(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const urls = (await canAccessAllFeedsAsync(ctx.db, ctx.session))
|
|
||||||
? input.urls
|
|
||||||
: await restrictUrlsAsync(ctx.db, input.urls);
|
|
||||||
|
|
||||||
const rssFeeds = await Promise.all(
|
const rssFeeds = await Promise.all(
|
||||||
urls.map(async (url) => {
|
urls.map(async (url) => {
|
||||||
const innerHandler = rssFeedsRequestHandler.handler({
|
const innerHandler = rssFeedsRequestHandler.handler({
|
||||||
url,
|
url,
|
||||||
count: input.maximumAmountPosts,
|
count: input.maximumAmountPosts,
|
||||||
});
|
|
||||||
return await innerHandler.getCachedOrUpdatedDataAsync({
|
|
||||||
forceUpdate: false,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return rssFeeds
|
|
||||||
.flatMap((rssFeed) => rssFeed.data.entries)
|
|
||||||
.slice(0, input.maximumAmountPosts)
|
|
||||||
.sort((entryA, entryB) => {
|
|
||||||
return entryA.published && entryB.published
|
|
||||||
? new Date(entryB.published).getTime() - new Date(entryA.published).getTime()
|
|
||||||
: 0;
|
|
||||||
});
|
});
|
||||||
}),
|
return await innerHandler.getCachedOrUpdatedDataAsync({
|
||||||
|
forceUpdate: false,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return rssFeeds
|
||||||
|
.flatMap((rssFeed) => rssFeed.data.entries)
|
||||||
|
.slice(0, input.maximumAmountPosts)
|
||||||
|
.sort((entryA, entryB) => {
|
||||||
|
return entryA.published && entryB.published
|
||||||
|
? new Date(entryB.published).getTime() - new Date(entryA.published).getTime()
|
||||||
|
: 0;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
subscribeFeeds: publicProcedure.input(feedsInput).subscription(({ input }) => {
|
||||||
|
// It is not necessary to check access to all feeds here, as the client will only subscribe to feeds which already exist
|
||||||
|
return observable<{ url: string; entries: ExtendedFeedEntry[] }>((emit) => {
|
||||||
|
const unsubscribes = input.urls.map((url) => {
|
||||||
|
const innerHandler = rssFeedsRequestHandler.handler({ url, count: input.maximumAmountPosts });
|
||||||
|
return innerHandler.subscribe((data) => {
|
||||||
|
emit.next({ url, entries: data.entries });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function canAccessAllFeedsAsync(db: Database, session: Session | null) {
|
export async function canAccessAllFeedsAsync(db: Database, session: Session | null) {
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { observable } from "@trpc/server/observable";
|
||||||
|
|
||||||
|
import type { SpeedtestTrackerDashboardData } from "@homarr/integrations/types";
|
||||||
|
import { speedtestTrackerRequestHandler } from "@homarr/request-handler/speedtest-tracker";
|
||||||
|
|
||||||
|
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
export const speedtestTrackerRouter = createTRPCRouter({
|
||||||
|
getDashboard: publicProcedure
|
||||||
|
.concat(createManyIntegrationMiddleware("query", "speedtestTracker"))
|
||||||
|
.query(async ({ ctx }) => {
|
||||||
|
const results = await Promise.all(
|
||||||
|
ctx.integrations.map(async (integration) => {
|
||||||
|
const innerHandler = speedtestTrackerRequestHandler.handler(integration, {});
|
||||||
|
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
|
|
||||||
|
return {
|
||||||
|
integrationId: integration.id,
|
||||||
|
integrationName: integration.name,
|
||||||
|
integrationUrl: integration.url,
|
||||||
|
dashboard: data,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}),
|
||||||
|
|
||||||
|
subscribeToDashboard: publicProcedure
|
||||||
|
.concat(createManyIntegrationMiddleware("query", "speedtestTracker"))
|
||||||
|
.subscription(({ ctx }) => {
|
||||||
|
return observable<{
|
||||||
|
integrationId: string;
|
||||||
|
dashboard: SpeedtestTrackerDashboardData;
|
||||||
|
timestamp: Date;
|
||||||
|
}>((emit) => {
|
||||||
|
const unsubscribes = ctx.integrations.map((integration) => {
|
||||||
|
const innerHandler = speedtestTrackerRequestHandler.handler(integration, {});
|
||||||
|
return innerHandler.subscribe((dashboard) => {
|
||||||
|
emit.next({
|
||||||
|
integrationId: integration.id,
|
||||||
|
dashboard,
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
|
import { timetableOptionsSchema } from "@homarr/integrations/types";
|
||||||
|
import {
|
||||||
|
timetableGetTimetableRequestHandler,
|
||||||
|
timetableSearchStationsRequestHandler,
|
||||||
|
} from "@homarr/request-handler/timetable";
|
||||||
|
|
||||||
|
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
export const timetableRouter = createTRPCRouter({
|
||||||
|
getTimetable: publicProcedure
|
||||||
|
.concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("timetable")))
|
||||||
|
.input(timetableOptionsSchema)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const innerHandler = timetableGetTimetableRequestHandler.handler(ctx.integration, {
|
||||||
|
stationId: input.stationId,
|
||||||
|
limit: input.limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
searchStations: publicProcedure
|
||||||
|
.concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("timetable")))
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
query: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const innerHandler = timetableSearchStationsRequestHandler.handler(ctx.integration, {
|
||||||
|
query: input.query,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { observable } from "@trpc/server/observable";
|
||||||
|
|
||||||
|
import type { TracearrDashboardData } from "@homarr/integrations/types";
|
||||||
|
import { tracearrRequestHandler } from "@homarr/request-handler/tracearr";
|
||||||
|
|
||||||
|
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
|
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
export const tracearrRouter = createTRPCRouter({
|
||||||
|
getDashboard: publicProcedure.concat(createManyIntegrationMiddleware("query", "tracearr")).query(async ({ ctx }) => {
|
||||||
|
const results = await Promise.all(
|
||||||
|
ctx.integrations.map(async (integration) => {
|
||||||
|
const innerHandler = tracearrRequestHandler.handler(integration, {});
|
||||||
|
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
|
|
||||||
|
return {
|
||||||
|
integrationId: integration.id,
|
||||||
|
integrationName: integration.name,
|
||||||
|
integrationUrl: integration.url,
|
||||||
|
dashboard: data,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}),
|
||||||
|
subscribeToDashboard: publicProcedure
|
||||||
|
.concat(createManyIntegrationMiddleware("query", "tracearr"))
|
||||||
|
.subscription(({ ctx }) => {
|
||||||
|
return observable<{ integrationId: string; dashboard: TracearrDashboardData; timestamp: Date }>((emit) => {
|
||||||
|
const unsubscribes = ctx.integrations.map((integration) => {
|
||||||
|
const innerHandler = tracearrRequestHandler.handler(integration, {});
|
||||||
|
return innerHandler.subscribe((dashboard) => {
|
||||||
|
emit.next({
|
||||||
|
integrationId: integration.id,
|
||||||
|
dashboard,
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ import type { Database } from "@homarr/db";
|
|||||||
import { eq } from "@homarr/db";
|
import { eq } from "@homarr/db";
|
||||||
import { apiKeys } from "@homarr/db/schema";
|
import { apiKeys } from "@homarr/db/schema";
|
||||||
|
|
||||||
import { hashPasswordAsync } from "../security";
|
import { comparePasswordsAsync } from "../security";
|
||||||
import { createSessionAsync } from "../server";
|
import { createSessionAsync } from "../server";
|
||||||
|
|
||||||
const logger = createLogger({ module: "apiKeyAuth" });
|
const logger = createLogger({ module: "apiKeyAuth" });
|
||||||
@@ -41,7 +41,6 @@ export const getSessionFromApiKeyAsync = async (
|
|||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
apiKey: true,
|
apiKey: true,
|
||||||
salt: true,
|
|
||||||
},
|
},
|
||||||
with: {
|
with: {
|
||||||
user: {
|
user: {
|
||||||
@@ -60,9 +59,9 @@ export const getSessionFromApiKeyAsync = async (
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashedApiKey = await hashPasswordAsync(apiKey, apiKeyFromDb.salt);
|
const isValid = await comparePasswordsAsync(apiKey, apiKeyFromDb.apiKey);
|
||||||
|
|
||||||
if (apiKeyFromDb.apiKey !== hashedApiKey) {
|
if (!isValid) {
|
||||||
logger.warn("Failed to authenticate with api-key", { ipAddress, userAgent, reason: "API_KEY_MISMATCH" });
|
logger.warn("Failed to authenticate with api-key", { ipAddress, userAgent, reason: "API_KEY_MISMATCH" });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createId } from "@homarr/common";
|
|||||||
import { apiKeys, users } from "@homarr/db/schema";
|
import { apiKeys, users } from "@homarr/db/schema";
|
||||||
import { createDb } from "@homarr/db/test";
|
import { createDb } from "@homarr/db/test";
|
||||||
|
|
||||||
import { createSaltAsync, hashPasswordAsync } from "../../security";
|
import { hashPasswordAsync } from "../../security";
|
||||||
import { getSessionFromApiKeyAsync } from "../get-api-key-session";
|
import { getSessionFromApiKeyAsync } from "../get-api-key-session";
|
||||||
|
|
||||||
// Mock the logger to avoid console output during tests
|
// Mock the logger to avoid console output during tests
|
||||||
@@ -118,11 +118,9 @@ const setupAsync = async (options?: SetupOptions) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (options?.token) {
|
if (options?.token) {
|
||||||
const salt = await createSaltAsync();
|
|
||||||
await db.insert(apiKeys).values({
|
await db.insert(apiKeys).values({
|
||||||
id: defaultApiKeyId,
|
id: defaultApiKeyId,
|
||||||
apiKey: await hashPasswordAsync(options.token, salt),
|
apiKey: await hashPasswordAsync(options.token),
|
||||||
salt,
|
|
||||||
userId: defaultUserId,
|
userId: defaultUserId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-15
@@ -24,30 +24,30 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "^0.41.1",
|
"@auth/core": "catalog:",
|
||||||
"@auth/drizzle-adapter": "^1.11.1",
|
"@auth/drizzle-adapter": "catalog:",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "catalog:",
|
||||||
"cookies": "^0.9.1",
|
"cookies": "catalog:",
|
||||||
"ldapts": "8.1.6",
|
"ldapts": "catalog:",
|
||||||
"next": "16.1.6",
|
"next": "catalog:",
|
||||||
"next-auth": "5.0.0-beta.30",
|
"next-auth": "catalog:",
|
||||||
"react": "19.2.4",
|
"react": "catalog:",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "catalog:",
|
||||||
"zod": "^4.2.1"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/bcrypt": "6.0.0",
|
"@types/bcrypt": "catalog:",
|
||||||
"@types/cookies": "0.9.2",
|
"@types/cookies": "catalog:",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "catalog:",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "catalog:",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import bcrypt from "bcrypt";
|
|
||||||
import type { z } from "zod/v4";
|
import type { z } from "zod/v4";
|
||||||
|
|
||||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
@@ -7,6 +6,8 @@ import { and, eq } from "@homarr/db";
|
|||||||
import { users } from "@homarr/db/schema";
|
import { users } from "@homarr/db/schema";
|
||||||
import type { userSignInSchema } from "@homarr/validation/user";
|
import type { userSignInSchema } from "@homarr/validation/user";
|
||||||
|
|
||||||
|
import { comparePasswordsAsync } from "../../../security";
|
||||||
|
|
||||||
const logger = createLogger({ module: "basicAuthorization" });
|
const logger = createLogger({ module: "basicAuthorization" });
|
||||||
|
|
||||||
export const authorizeWithBasicCredentialsAsync = async (
|
export const authorizeWithBasicCredentialsAsync = async (
|
||||||
@@ -23,7 +24,7 @@ export const authorizeWithBasicCredentialsAsync = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info("User is trying to log in. Checking password...", { userName: user.name });
|
logger.info("User is trying to log in. Checking password...", { userName: user.name });
|
||||||
const isValidPassword = await bcrypt.compare(credentials.password, user.password);
|
const isValidPassword = await comparePasswordsAsync(credentials.password, user.password);
|
||||||
|
|
||||||
if (!isValidPassword) {
|
if (!isValidPassword) {
|
||||||
logger.warn("Password for user was incorrect", { userName: user.name });
|
logger.warn("Password for user was incorrect", { userName: user.name });
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createId } from "@homarr/common";
|
|||||||
import { users } from "@homarr/db/schema";
|
import { users } from "@homarr/db/schema";
|
||||||
import { createDb } from "@homarr/db/test";
|
import { createDb } from "@homarr/db/test";
|
||||||
|
|
||||||
import { createSaltAsync, hashPasswordAsync } from "../../security";
|
import { hashPasswordAsync } from "../../security";
|
||||||
import { authorizeWithBasicCredentialsAsync } from "../credentials/authorization/basic-authorization";
|
import { authorizeWithBasicCredentialsAsync } from "../credentials/authorization/basic-authorization";
|
||||||
|
|
||||||
const defaultUserId = createId();
|
const defaultUserId = createId();
|
||||||
@@ -13,12 +13,10 @@ describe("authorizeWithBasicCredentials", () => {
|
|||||||
test("should authorize user with correct credentials", async () => {
|
test("should authorize user with correct credentials", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
const salt = await createSaltAsync();
|
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
id: defaultUserId,
|
id: defaultUserId,
|
||||||
name: "test",
|
name: "test",
|
||||||
salt,
|
password: await hashPasswordAsync("test"),
|
||||||
password: await hashPasswordAsync("test", salt),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -34,12 +32,10 @@ describe("authorizeWithBasicCredentials", () => {
|
|||||||
test("should not authorize user with incorrect credentials", async () => {
|
test("should not authorize user with incorrect credentials", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
const salt = await createSaltAsync();
|
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
id: defaultUserId,
|
id: defaultUserId,
|
||||||
name: "test",
|
name: "test",
|
||||||
salt,
|
password: await hashPasswordAsync("test"),
|
||||||
password: await hashPasswordAsync("test", salt),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -55,12 +51,10 @@ describe("authorizeWithBasicCredentials", () => {
|
|||||||
test("should not authorize user with incorrect username", async () => {
|
test("should not authorize user with incorrect username", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const db = createDb();
|
const db = createDb();
|
||||||
const salt = await createSaltAsync();
|
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
id: defaultUserId,
|
id: defaultUserId,
|
||||||
name: "test",
|
name: "test",
|
||||||
salt,
|
password: await hashPasswordAsync("test"),
|
||||||
password: await hashPasswordAsync("test", salt),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
|
|
||||||
export const createSaltAsync = async () => {
|
export const hashPasswordAsync = async (password: string) => {
|
||||||
return bcrypt.genSalt(10);
|
return bcrypt.hash(password, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hashPasswordAsync = async (password: string, salt: string) => {
|
/**
|
||||||
return bcrypt.hash(password, salt);
|
* Compares a plaintext password with a hashed password and returns whether they match.
|
||||||
|
* @param password plaintext password
|
||||||
|
* @param hash hashed password
|
||||||
|
* @returns wheter it is the same
|
||||||
|
*/
|
||||||
|
export const comparePasswordsAsync = async (password: string, hash: string) => {
|
||||||
|
return await bcrypt.compare(password, hash);
|
||||||
};
|
};
|
||||||
@@ -1,25 +1,11 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { createSaltAsync, hashPasswordAsync } from "../security";
|
import { comparePasswordsAsync, hashPasswordAsync } from "../security";
|
||||||
|
|
||||||
describe("createSalt should return a salt", () => {
|
|
||||||
it("should return a salt", async () => {
|
|
||||||
const result = await createSaltAsync();
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.length).toBeGreaterThan(25);
|
|
||||||
});
|
|
||||||
it("should return a different salt each time", async () => {
|
|
||||||
const result1 = await createSaltAsync();
|
|
||||||
const result2 = await createSaltAsync();
|
|
||||||
expect(result1).not.toEqual(result2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("hashPassword should return a hash", () => {
|
describe("hashPassword should return a hash", () => {
|
||||||
it("should return a hash", async () => {
|
it("should return a hash", async () => {
|
||||||
const password = "password";
|
const password = "password";
|
||||||
const salt = await createSaltAsync();
|
const result = await hashPasswordAsync(password);
|
||||||
const result = await hashPasswordAsync(password, salt);
|
|
||||||
expect(result).toBeDefined();
|
expect(result).toBeDefined();
|
||||||
expect(result.length).toBeGreaterThan(55);
|
expect(result.length).toBeGreaterThan(55);
|
||||||
expect(result).not.toEqual(password);
|
expect(result).not.toEqual(password);
|
||||||
@@ -27,21 +13,44 @@ describe("hashPassword should return a hash", () => {
|
|||||||
it("should return a different hash each time", async () => {
|
it("should return a different hash each time", async () => {
|
||||||
const password = "password";
|
const password = "password";
|
||||||
const password2 = "another password";
|
const password2 = "another password";
|
||||||
const salt = await createSaltAsync();
|
|
||||||
|
|
||||||
const result1 = await hashPasswordAsync(password, salt);
|
const result1 = await hashPasswordAsync(password);
|
||||||
const result2 = await hashPasswordAsync(password2, salt);
|
const result2 = await hashPasswordAsync(password2);
|
||||||
|
|
||||||
expect(result1).not.toEqual(result2);
|
expect(result1).not.toEqual(result2);
|
||||||
});
|
});
|
||||||
it("should return a different hash for the same password with different salts", async () => {
|
it("should return a different hash for the same password with different salts", async () => {
|
||||||
const password = "password";
|
const password = "password";
|
||||||
const salt1 = await createSaltAsync();
|
|
||||||
const salt2 = await createSaltAsync();
|
|
||||||
|
|
||||||
const result1 = await hashPasswordAsync(password, salt1);
|
const result1 = await hashPasswordAsync(password);
|
||||||
const result2 = await hashPasswordAsync(password, salt2);
|
const result2 = await hashPasswordAsync(password);
|
||||||
|
|
||||||
expect(result1).not.toEqual(result2);
|
expect(result1).not.toEqual(result2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("comparePasswordsAsync should compare passwords correctly", () => {
|
||||||
|
it("should return true for matching passwords", async () => {
|
||||||
|
// Arrange
|
||||||
|
const password = "password";
|
||||||
|
const hash = await hashPasswordAsync(password);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await comparePasswordsAsync(password, hash);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
it("should return false for non-matching passwords", async () => {
|
||||||
|
// Arrange
|
||||||
|
const password = "password";
|
||||||
|
const wrongPassword = "wrongpassword";
|
||||||
|
const hash = await hashPasswordAsync(password);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await comparePasswordsAsync(wrongPassword, hash);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -25,14 +25,14 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/api": "workspace:^0.1.0",
|
"@homarr/api": "workspace:^0.1.0",
|
||||||
"react": "19.2.4",
|
"react": "catalog:",
|
||||||
"react-dom": "19.2.4"
|
"react-dom": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "catalog:",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,23 +19,25 @@
|
|||||||
"clean": "rm -rf .turbo node_modules",
|
"clean": "rm -rf .turbo node_modules",
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"typecheck": "tsc --noEmit"
|
"run": "pnpm with-env node cli.cjs",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"with-env": "dotenv -e .env --"
|
||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@drizzle-team/brocli": "^0.11.0",
|
"@drizzle-team/brocli": "catalog:",
|
||||||
"@homarr/auth": "workspace:^0.1.0",
|
"@homarr/auth": "workspace:^0.1.0",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"dotenv": "^17.3.1"
|
"dotenv": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"esbuild": "^0.27.3",
|
"esbuild": "catalog:",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "catalog:",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { command, string } from "@drizzle-team/brocli";
|
||||||
|
|
||||||
|
import { db, eq } from "@homarr/db";
|
||||||
|
import { integrations } from "@homarr/db/schema";
|
||||||
|
|
||||||
|
export const integrationsDelete = command({
|
||||||
|
name: "delete",
|
||||||
|
desc: "Delete an integration by id or name",
|
||||||
|
options: {
|
||||||
|
id: string("id").alias("i").desc("ID of the integration"),
|
||||||
|
name: string("name").alias("n").desc("Name of the integration"),
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
handler: async (options) => {
|
||||||
|
if (!options.id && !options.name) {
|
||||||
|
console.error("Either --id or --name must be provided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry;
|
||||||
|
if (options.id) {
|
||||||
|
entry = await db.query.integrations.findFirst({
|
||||||
|
where: eq(integrations.id, options.id),
|
||||||
|
});
|
||||||
|
} else if (options.name) {
|
||||||
|
entry = await db.query.integrations.findFirst({
|
||||||
|
where: eq(integrations.name, options.name),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
console.error("Integration not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.delete(integrations).where(eq(integrations.id, entry.id));
|
||||||
|
console.log(`Integration ${entry.name} (${entry.id}) deleted`);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { command } from "@drizzle-team/brocli";
|
||||||
|
|
||||||
|
import { db } from "@homarr/db";
|
||||||
|
|
||||||
|
export const integrationsList = command({
|
||||||
|
name: "list",
|
||||||
|
desc: "List all integrations (id and name)",
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
handler: async () => {
|
||||||
|
const all = await db.query.integrations.findMany();
|
||||||
|
if (all.length === 0) {
|
||||||
|
console.log("No integrations found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("ID\t\t\t\tName");
|
||||||
|
for (const i of all) {
|
||||||
|
console.log(`${i.id}\t${i.name}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { command } from "@drizzle-team/brocli";
|
||||||
|
|
||||||
|
import { integrationsDelete } from "./integrations-delete";
|
||||||
|
import { integrationsList } from "./integrations-list";
|
||||||
|
|
||||||
|
export const integrationsRoot = command({
|
||||||
|
name: "integrations",
|
||||||
|
desc: "Group of commands to manage integrations",
|
||||||
|
subcommands: [integrationsList, integrationsDelete],
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { command, string } from "@drizzle-team/brocli";
|
import { command, string } from "@drizzle-team/brocli";
|
||||||
|
|
||||||
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
import { hashPasswordAsync } from "@homarr/auth";
|
||||||
import { createId } from "@homarr/common";
|
import { createId } from "@homarr/common";
|
||||||
import { generateSecureRandomToken } from "@homarr/common/server";
|
import { generateSecureRandomToken } from "@homarr/common/server";
|
||||||
import { and, count, db, eq } from "@homarr/db";
|
import { and, count, db, eq } from "@homarr/db";
|
||||||
@@ -67,9 +67,8 @@ export const recreateAdmin = command({
|
|||||||
permission: "admin",
|
permission: "admin",
|
||||||
});
|
});
|
||||||
|
|
||||||
const salt = await createSaltAsync();
|
|
||||||
const password = generateSecureRandomToken(24);
|
const password = generateSecureRandomToken(24);
|
||||||
const hashedPassword = await hashPasswordAsync(password, salt);
|
const hashedPassword = await hashPasswordAsync(password);
|
||||||
|
|
||||||
const userId = createId();
|
const userId = createId();
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
@@ -77,7 +76,6 @@ export const recreateAdmin = command({
|
|||||||
name: result.data,
|
name: result.data,
|
||||||
provider: "credentials",
|
provider: "credentials",
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
salt,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.insert(groupMembers).values({
|
await db.insert(groupMembers).values({
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const resetPassword = command({
|
|||||||
where: and(eq(users.name, options.username), eq(users.provider, "credentials")),
|
where: and(eq(users.name, options.username), eq(users.provider, "credentials")),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user?.salt) {
|
if (!user?.password) {
|
||||||
console.error(`User ${options.username} not found`);
|
console.error(`User ${options.username} not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ export const resetPassword = command({
|
|||||||
await db
|
await db
|
||||||
.update(users)
|
.update(users)
|
||||||
.set({
|
.set({
|
||||||
password: await hashPasswordAsync(newPassword, user.salt),
|
password: await hashPasswordAsync(newPassword),
|
||||||
})
|
})
|
||||||
.where(eq(users.id, user.id));
|
.where(eq(users.id, user.id));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { command, string } from "@drizzle-team/brocli";
|
||||||
|
|
||||||
|
import { db, eq } from "@homarr/db";
|
||||||
|
import { users } from "@homarr/db/schema";
|
||||||
|
|
||||||
|
export const usersDelete = command({
|
||||||
|
name: "delete",
|
||||||
|
desc: "Delete a user by id or username",
|
||||||
|
options: {
|
||||||
|
id: string("id").alias("i").desc("ID of the user"),
|
||||||
|
username: string("username").alias("u").desc("Name of the user"),
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
handler: async (options) => {
|
||||||
|
if (!options.id && !options.username) {
|
||||||
|
console.error("Either --id or --username must be provided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let user;
|
||||||
|
if (options.id) {
|
||||||
|
user = await db.query.users.findFirst({
|
||||||
|
where: eq(users.id, options.id),
|
||||||
|
});
|
||||||
|
} else if (options.username) {
|
||||||
|
user = await db.query.users.findFirst({
|
||||||
|
where: eq(users.name, options.username),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.error("User not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.delete(users).where(eq(users.id, user.id));
|
||||||
|
console.log(`User ${user.name ?? user.id} (${user.id}) deleted`);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { command } from "@drizzle-team/brocli";
|
||||||
|
|
||||||
|
import { db } from "@homarr/db";
|
||||||
|
|
||||||
|
export const usersList = command({
|
||||||
|
name: "list",
|
||||||
|
desc: "List all users (id, username, email)",
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
handler: async () => {
|
||||||
|
const allUsers = await db.query.users.findMany();
|
||||||
|
|
||||||
|
if (allUsers.length === 0) {
|
||||||
|
console.log("No users found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("ID\t\t\t\tUsername\tEmail");
|
||||||
|
for (const user of allUsers) {
|
||||||
|
console.log(`${user.id}\t${user.name ?? ""}\t${user.email ?? ""}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { command, string } from "@drizzle-team/brocli";
|
||||||
|
|
||||||
|
import { hashPasswordAsync } from "@homarr/auth";
|
||||||
|
import { db, eq } from "@homarr/db";
|
||||||
|
import { sessions, users } from "@homarr/db/schema";
|
||||||
|
|
||||||
|
export const usersUpdatePassword = command({
|
||||||
|
name: "update-password",
|
||||||
|
desc: "Update the password for a user (credentials provider only)",
|
||||||
|
options: {
|
||||||
|
id: string("id").alias("i").desc("ID of the user"),
|
||||||
|
username: string("username").alias("u").desc("Name of the user"),
|
||||||
|
password: string("password").alias("p").required().desc("New password"),
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
handler: async (options) => {
|
||||||
|
if (!process.env.AUTH_PROVIDERS?.toLowerCase().includes("credentials")) {
|
||||||
|
console.error("Credentials provider is not enabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.id && !options.username) {
|
||||||
|
console.error("Either --id or --username must be provided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await db.query.users.findFirst({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
where: options.id ? eq(users.id, options.id) : eq(users.name, options.username!),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user?.password) {
|
||||||
|
console.error("User not found or has no credentials record");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(users)
|
||||||
|
.set({
|
||||||
|
password: await hashPasswordAsync(options.password),
|
||||||
|
})
|
||||||
|
.where(eq(users.id, user.id));
|
||||||
|
|
||||||
|
// invalidate existing sessions
|
||||||
|
await db.delete(sessions).where(eq(sessions.userId, user.id));
|
||||||
|
console.log(`All sessions for user ${user.name ?? user.id} have been deleted`);
|
||||||
|
|
||||||
|
console.log("Password updated successfully");
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { command } from "@drizzle-team/brocli";
|
||||||
|
|
||||||
|
import { usersDelete } from "./users-delete";
|
||||||
|
import { usersList } from "./users-list";
|
||||||
|
import { usersUpdatePassword } from "./users-update-password";
|
||||||
|
|
||||||
|
export const usersRoot = command({
|
||||||
|
name: "users",
|
||||||
|
desc: "Group of commands to manage users",
|
||||||
|
subcommands: [usersList, usersDelete, usersUpdatePassword],
|
||||||
|
});
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { run } from "@drizzle-team/brocli";
|
import { run } from "@drizzle-team/brocli";
|
||||||
|
|
||||||
import { fixUsernames } from "./commands/fix-usernames";
|
import { fixUsernames } from "./commands/fix-usernames";
|
||||||
|
import { integrationsRoot } from "./commands/integrations";
|
||||||
import { recreateAdmin } from "./commands/recreate-admin";
|
import { recreateAdmin } from "./commands/recreate-admin";
|
||||||
import { resetPassword } from "./commands/reset-password";
|
import { resetPassword } from "./commands/reset-password";
|
||||||
|
import { usersRoot } from "./commands/users";
|
||||||
|
|
||||||
const commands = [resetPassword, fixUsernames, recreateAdmin];
|
const commands = [usersRoot, integrationsRoot, resetPassword, fixUsernames, recreateAdmin];
|
||||||
|
|
||||||
void run(commands, {
|
void run(commands, {
|
||||||
name: "homarr-cli",
|
name: "homarr-cli",
|
||||||
|
|||||||
@@ -27,22 +27,22 @@
|
|||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@paralleldrive/cuid2": "^3.3.0",
|
"@paralleldrive/cuid2": "catalog:",
|
||||||
"cookie": "1.1.1",
|
"cookie": "catalog:",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "catalog:",
|
||||||
"next": "16.1.6",
|
"next": "catalog:",
|
||||||
"octokit": "^5.0.5",
|
"octokit": "catalog:",
|
||||||
"react": "19.2.4",
|
"react": "catalog:",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "catalog:",
|
||||||
"undici": "7.18.2",
|
"undici": "catalog:",
|
||||||
"zod": "^4.2.1",
|
"zod": "catalog:",
|
||||||
"zod-validation-error": "^5.0.0"
|
"zod-validation-error": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "catalog:",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,17 @@ dayjs.extend(isBetween);
|
|||||||
|
|
||||||
const validUnits = ["h", "d", "w", "M", "y"] as UnitTypeShort[];
|
const validUnits = ["h", "d", "w", "M", "y"] as UnitTypeShort[];
|
||||||
|
|
||||||
|
export function formatDuration(milliseconds: number) {
|
||||||
|
const totalSeconds = Math.floor(milliseconds / 1000);
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
export const isDateWithin = (date: Date, relativeDate: string): boolean => {
|
export const isDateWithin = (date: Date, relativeDate: string): boolean => {
|
||||||
if (relativeDate.length < 2) {
|
if (relativeDate.length < 2) {
|
||||||
throw new Error("Relative date must be at least 2 characters long");
|
throw new Error("Relative date must be at least 2 characters long");
|
||||||
|
|||||||
+14
-14
@@ -37,24 +37,24 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@t3-oss/env-nextjs": "^0.13.10",
|
"@t3-oss/env-nextjs": "catalog:",
|
||||||
"better-sqlite3": "^12.6.2",
|
"better-sqlite3": "catalog:",
|
||||||
"dns-caching": "^0.2.9",
|
"dns-caching": "catalog:",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "catalog:",
|
||||||
"ioredis": "5.9.3",
|
"ioredis": "catalog:",
|
||||||
"mysql2": "3.17.4",
|
"mysql2": "catalog:",
|
||||||
"pg": "^8.18.0",
|
"pg": "catalog:",
|
||||||
"superjson": "2.2.6",
|
"superjson": "catalog:",
|
||||||
"winston": "3.19.0",
|
"winston": "catalog:",
|
||||||
"zod": "^4.2.1"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/better-sqlite3": "7.6.13",
|
"@types/better-sqlite3": "catalog:",
|
||||||
"@types/pg": "^8.16.0",
|
"@types/pg": "catalog:",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "catalog:",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
|
|
||||||
export const formatMetadata = (metadata: Record<string, unknown> | Error, ignoreKeys?: string[]) => {
|
export const formatMetadata = (metadata: Record<string, unknown> | Error, ignoreKeys?: string[]) => {
|
||||||
const metadataObject = metadata instanceof ErrorWithMetadata ? metadata.metadata : metadata;
|
const metadataObject =
|
||||||
|
metadata instanceof ErrorWithMetadata || ("metadata" in metadata && typeof metadata.metadata === "object")
|
||||||
|
? (metadata.metadata as Record<string, unknown>)
|
||||||
|
: metadata;
|
||||||
|
|
||||||
const filteredMetadata = Object.keys(metadataObject)
|
const filteredMetadata = Object.keys(metadataObject)
|
||||||
.filter((key) => !ignoreKeys?.includes(key))
|
.filter((key) => !ignoreKeys?.includes(key))
|
||||||
|
|||||||
@@ -28,21 +28,21 @@
|
|||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "catalog:",
|
||||||
"@trpc/client": "^11.10.0",
|
"@trpc/client": "catalog:",
|
||||||
"@trpc/server": "^11.10.0",
|
"@trpc/server": "catalog:",
|
||||||
"@trpc/tanstack-react-query": "^11.10.0",
|
"@trpc/tanstack-react-query": "catalog:",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "catalog:",
|
||||||
"react": "19.2.4",
|
"react": "catalog:",
|
||||||
"zod": "^4.2.1"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "catalog:",
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "catalog:",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "catalog:",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "catalog:",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,14 +27,14 @@
|
|||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/core": "workspace:^",
|
"@homarr/core": "workspace:^",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"node-cron": "^4.2.1"
|
"node-cron": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "catalog:",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "catalog:",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,13 +38,13 @@
|
|||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"semver-parser": "^4.1.8"
|
"semver-parser": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "catalog:",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,8 @@ import { mediaServerJob } from "./jobs/integrations/media-server";
|
|||||||
import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding";
|
import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding";
|
||||||
import { networkControllerJob } from "./jobs/integrations/network-controller";
|
import { networkControllerJob } from "./jobs/integrations/network-controller";
|
||||||
import { refreshNotificationsJob } from "./jobs/integrations/notifications";
|
import { refreshNotificationsJob } from "./jobs/integrations/notifications";
|
||||||
|
import { timetableJob } from "./jobs/integrations/timetable";
|
||||||
|
import { tracearrJob } from "./jobs/integrations/tracearr";
|
||||||
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
|
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
|
||||||
import { pingJob } from "./jobs/ping";
|
import { pingJob } from "./jobs/ping";
|
||||||
import { rssFeedsJob } from "./jobs/rss-feeds";
|
import { rssFeedsJob } from "./jobs/rss-feeds";
|
||||||
@@ -50,6 +52,8 @@ export const jobGroup = createCronJobGroup({
|
|||||||
firewallInterfaces: firewallInterfacesJob,
|
firewallInterfaces: firewallInterfacesJob,
|
||||||
refreshNotifications: refreshNotificationsJob,
|
refreshNotifications: refreshNotificationsJob,
|
||||||
weather: weatherJob,
|
weather: weatherJob,
|
||||||
|
timetable: timetableJob,
|
||||||
|
tracearr: tracearrJob,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
|
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions";
|
||||||
|
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
|
||||||
|
import { timetableGetTimetableRequestHandler } from "@homarr/request-handler/timetable";
|
||||||
|
|
||||||
|
import { createCronJob } from "../../lib";
|
||||||
|
|
||||||
|
export const timetableJob = createCronJob("timetable", EVERY_5_MINUTES).withCallback(
|
||||||
|
createRequestIntegrationJobHandler(timetableGetTimetableRequestHandler.handler, {
|
||||||
|
widgetKinds: ["timetable"],
|
||||||
|
getInput: {
|
||||||
|
timetable: (options) =>
|
||||||
|
options.station
|
||||||
|
? {
|
||||||
|
stationId: options.station.value,
|
||||||
|
limit: 10,
|
||||||
|
}
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions";
|
||||||
|
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
|
||||||
|
import { tracearrRequestHandler } from "@homarr/request-handler/tracearr";
|
||||||
|
|
||||||
|
import { createCronJob } from "../../lib";
|
||||||
|
|
||||||
|
export const tracearrJob = createCronJob("tracearr", EVERY_5_SECONDS).withCallback(
|
||||||
|
createRequestIntegrationJobHandler(tracearrRequestHandler.handler, {
|
||||||
|
widgetKinds: ["tracearr"],
|
||||||
|
getInput: {
|
||||||
|
tracearr: () => ({}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -5,9 +5,9 @@ import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
|||||||
import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions";
|
import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions";
|
||||||
import { db, eq } from "@homarr/db";
|
import { db, eq } from "@homarr/db";
|
||||||
import { items } from "@homarr/db/schema";
|
import { items } from "@homarr/db/schema";
|
||||||
// This import is done that way to avoid circular dependencies.
|
|
||||||
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
|
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";
|
||||||
|
|
||||||
|
// This import is done that way to avoid circular dependencies.
|
||||||
import type { WidgetComponentProps } from "../../../widgets";
|
import type { WidgetComponentProps } from "../../../widgets";
|
||||||
import { createCronJob } from "../lib";
|
import { createCronJob } from "../lib";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `apiKey` DROP COLUMN `salt`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `user` DROP COLUMN `salt`;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -260,6 +260,13 @@
|
|||||||
"when": 1760968518445,
|
"when": 1760968518445,
|
||||||
"tag": "0036_add_app_reference_to_integration",
|
"tag": "0036_add_app_reference_to_integration",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 37,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1775399941430,
|
||||||
|
"tag": "0037_remove_salt",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "apiKey" DROP COLUMN "salt";--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" DROP COLUMN "salt";
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -22,6 +22,13 @@
|
|||||||
"when": 1760968530084,
|
"when": 1760968530084,
|
||||||
"tag": "0002_add_app_reference_to_integration",
|
"tag": "0002_add_app_reference_to_integration",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1775399963041,
|
||||||
|
"tag": "0003_remove_salt",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `apiKey` DROP COLUMN `salt`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `user` DROP COLUMN `salt`;
|
||||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 256 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user