Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f36f436f70 |
No files matched your search
@@ -12,6 +12,10 @@ AUTH_SECRET="supersecret"
|
|||||||
# or starting the project without any (which will show a randomly generated one).
|
# or starting the project without any (which will show a randomly generated one).
|
||||||
SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
|
||||||
|
# Cron job API key is generated every time the container starts as it is required for communication between nextjs-api and tasks-api
|
||||||
|
# export CRON_JOB_API_KEY=$(openssl rand -base64 32)
|
||||||
|
# CRON_JOB_API_KEY="your-generated-api-key"
|
||||||
|
|
||||||
LOG_LEVEL='info'
|
LOG_LEVEL='info'
|
||||||
|
|
||||||
# This is how you can use the sqlite driver:
|
# This is how you can use the sqlite driver:
|
||||||
|
|||||||
@@ -33,14 +33,6 @@ 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.3
|
|
||||||
- 1.59.2
|
|
||||||
- 1.59.1
|
|
||||||
- 1.59.0
|
|
||||||
- 1.58.1
|
|
||||||
- 1.58.0
|
|
||||||
- 1.57.1
|
|
||||||
- 1.57.0
|
|
||||||
- 1.56.1
|
- 1.56.1
|
||||||
- 1.56.0
|
- 1.56.0
|
||||||
- 1.55.0
|
- 1.55.0
|
||||||
|
|||||||
@@ -25,16 +25,16 @@ body:
|
|||||||
label: Public API available?
|
label: Public API available?
|
||||||
description: Is there a public API available, that we can consume in Homarr?
|
description: Is there a public API available, that we can consume in Homarr?
|
||||||
options:
|
options:
|
||||||
- "Yes, available on a website"
|
- Yes, available on a website
|
||||||
- "Yes, available in the application itself"
|
- Yes, available in the application itself
|
||||||
- "No"
|
- No
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
attributes:
|
attributes:
|
||||||
label: Are you willing to contribute this yourself?
|
label: Are you willing to contribute this yourself?
|
||||||
options:
|
options:
|
||||||
- "Yes"
|
- Yes
|
||||||
- "No"
|
- No
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -24,7 +24,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Are you willing to contribute this yourself?
|
label: Are you willing to contribute this yourself?
|
||||||
options:
|
options:
|
||||||
- "Yes"
|
- Yes
|
||||||
- "No"
|
- No
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -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@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build docker image for ${{ inputs.architecture }}
|
- name: Build docker image for ${{ inputs.architecture }}
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
|
|||||||
@@ -11,12 +11,6 @@
|
|||||||
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,
|
||||||
@@ -37,12 +31,6 @@
|
|||||||
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,21 +9,16 @@ jobs:
|
|||||||
approve-automatic-prs:
|
approve-automatic-prs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 2
|
timeout-minutes: 2
|
||||||
# 158783068: homarr-renovate[bot]
|
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
|
||||||
# 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@v3
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
||||||
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
||||||
permission-pull-requests: write # required to approve pull request
|
permission-pull-requests: write # required to approve pull request
|
||||||
|
|
||||||
- name: Install GitHub CLI
|
- name: Install GitHub CLI
|
||||||
|
|||||||
@@ -11,10 +11,14 @@ on:
|
|||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: "${{ github.workflow }}-${{ github.ref }}"
|
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
|
||||||
|
|
||||||
|
# You can leverage Vercel Remote Caching with Turbo to speed up your builds
|
||||||
|
# @link https://turborepo.org/docs/core-concepts/remote-caching#remote-caching-on-vercel-builds
|
||||||
env:
|
env:
|
||||||
FORCE_COLOR: 3
|
FORCE_COLOR: 3
|
||||||
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
@@ -55,7 +59,7 @@ jobs:
|
|||||||
uses: ./tooling/github/setup
|
uses: ./tooling/github/setup
|
||||||
|
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: pnpm typecheck
|
run: turbo typecheck
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -85,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@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
context: .
|
context: .
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: actions/create-github-app-token@v3
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
|
||||||
client-id: ${{ secrets.CROWDIN_APP_ID }}
|
app-id: ${{ secrets.CROWDIN_APP_ID }}
|
||||||
permission-contents: write # required to commit to crowdin branch
|
permission-contents: write # required to commit to crowdin branch
|
||||||
permission-pull-requests: write # required to create pull request
|
permission-pull-requests: write # required to create pull request
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
name: "[Deployment] Beta Release"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
send-notifications:
|
|
||||||
type: boolean
|
|
||||||
required: false
|
|
||||||
default: true
|
|
||||||
description: Send notifications
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
create-and-merge-pr:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 2
|
|
||||||
steps:
|
|
||||||
- name: Discord notification
|
|
||||||
if: ${{ github.event.inputs.send-notifications }}
|
|
||||||
env:
|
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
|
||||||
uses: Ilshidur/action-discord@0.4.0
|
|
||||||
with:
|
|
||||||
args: "Beta release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Get Next Version
|
|
||||||
id: semver
|
|
||||||
uses: ietf-tools/semver-action@v1
|
|
||||||
with:
|
|
||||||
token: ${{ github.token }}
|
|
||||||
branch: next
|
|
||||||
- name: Create pull request
|
|
||||||
id: create-pull-request
|
|
||||||
run: 'gh pr create --title "chore(release): release ${{ steps.semver.outputs.next }}-beta" --body "Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}-beta``" --base beta --head next --label automerge'
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
- name: Discord notification
|
|
||||||
if: ${{ github.event.inputs.send-notifications }}
|
|
||||||
env:
|
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
|
||||||
uses: Ilshidur/action-discord@0.4.0
|
|
||||||
with:
|
|
||||||
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }}-beta (new behaviour: ${{ steps.semver.outputs.bump }})"
|
|
||||||
- name: Obtain token
|
|
||||||
id: obtainApprovalToken
|
|
||||||
uses: actions/create-github-app-token@v3
|
|
||||||
with:
|
|
||||||
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
|
||||||
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
|
||||||
permission-pull-requests: write
|
|
||||||
- name: Approve PR
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
|
|
||||||
run: |
|
|
||||||
gh pr review --approve --body "Automatically approved by GitHub Action"
|
|
||||||
- name: Obtain token
|
|
||||||
id: obtainMergeToken
|
|
||||||
uses: actions/create-github-app-token@v3
|
|
||||||
with:
|
|
||||||
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
|
||||||
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
|
||||||
permission-contents: write # write to beta branch (due to merge)
|
|
||||||
permission-pull-requests: write # merge pull request
|
|
||||||
- id: automerge
|
|
||||||
if: ${{ steps.semver.outputs.bump != 'major' }}
|
|
||||||
name: automerge
|
|
||||||
uses: "pascalgn/automerge-action@v0.16.4"
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ steps.obtainMergeToken.outputs.token }}
|
|
||||||
MERGE_METHOD: merge # we prefer merge commits for merging to beta
|
|
||||||
MERGE_COMMIT_MESSAGE: "chore(release): release ${{ steps.semver.outputs.next }}-beta"
|
|
||||||
MERGE_DELETE_BRANCH: false # never set to true!
|
|
||||||
PULL_REQUEST: "${{ steps.create-pull-request.outputs.pr_number }}"
|
|
||||||
MERGE_RETRIES: 20 # 20 retries * MERGE_RETRY_SLEEP until step fails
|
|
||||||
MERGE_RETRY_SLEEP: 10000 # 10 seconds * MERGE_RETRIES until step fails
|
|
||||||
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
|
|
||||||
|
|
||||||
- name: Merged Discord notification
|
|
||||||
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && github.event.inputs.send-notifications }}
|
|
||||||
env:
|
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
|
||||||
uses: Ilshidur/action-discord@0.4.0
|
|
||||||
with:
|
|
||||||
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}-beta"
|
|
||||||
- name: Major Bump Discord notification
|
|
||||||
if: ${{ steps.semver.outputs.bump == 'major' && github.event.inputs.send-notifications }}
|
|
||||||
env:
|
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
|
||||||
uses: Ilshidur/action-discord@0.4.0
|
|
||||||
with:
|
|
||||||
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}-beta"
|
|
||||||
- name: Discord Fail Notification
|
|
||||||
if: failure() && github.event.inputs.send-notifications
|
|
||||||
env:
|
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
|
||||||
uses: Ilshidur/action-discord@0.4.0
|
|
||||||
with:
|
|
||||||
args: "The beta release workflow [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) has failed"
|
|
||||||
@@ -5,7 +5,6 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- beta
|
- beta
|
||||||
- next
|
|
||||||
- dev
|
- dev
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
@@ -35,7 +34,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
env:
|
env:
|
||||||
SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' || github.ref_name == 'next' }}
|
SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' }}
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }}
|
version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }}
|
||||||
git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }}
|
git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }}
|
||||||
@@ -51,23 +50,23 @@ 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@v3
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||||
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||||
permission-contents: write # required to commit package.json & changelog changes, merge them to dev / next and publish the release
|
permission-contents: write # required to commit package.json & changelog changes, merge them to dev and publish the release
|
||||||
|
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v4
|
||||||
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.15.0
|
node-version: 24.14.0
|
||||||
package-manager-cache: false
|
cache: "pnpm"
|
||||||
- run: npm i -g pnpm
|
- run: npm i -g pnpm
|
||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -96,21 +95,20 @@ jobs:
|
|||||||
id: read-git-ref
|
id: read-git-ref
|
||||||
run: |
|
run: |
|
||||||
echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||||
- name: Update dev / next branch
|
- name: Update dev branch
|
||||||
if: env.SKIP_RELEASE == 'false'
|
if: env.SKIP_RELEASE == 'false'
|
||||||
continue-on-error: true # Prevent pipeline from failing when merge fails
|
continue-on-error: true # Prevent pipeline from failing when merge fails
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||||
UPSTREAM_BRANCH: ${{ github.ref_name == 'main' && 'dev' || 'next' }}
|
|
||||||
run: |
|
run: |
|
||||||
git config user.name "Releases Homarr"
|
git config user.name "Releases Homarr"
|
||||||
git config user.email "175486441+homarr-releases[bot]@users.noreply.github.com"
|
git config user.email "175486441+homarr-releases[bot]@users.noreply.github.com"
|
||||||
git remote set-url origin https://x-access-token:${{ steps.obtainToken.outputs.token }}@github.com/${{ github.repository }}.git
|
git remote set-url origin https://x-access-token:${{ steps.obtainToken.outputs.token }}@github.com/${{ github.repository }}.git
|
||||||
git fetch origin ${{ env.UPSTREAM_BRANCH }}
|
git fetch origin dev
|
||||||
git checkout ${{ env.UPSTREAM_BRANCH }}
|
git checkout dev
|
||||||
git pull origin ${{ env.UPSTREAM_BRANCH }}
|
git pull origin dev
|
||||||
git rebase ${{ github.ref_name }}
|
git rebase ${{ github.ref_name }}
|
||||||
git push origin ${{ env.UPSTREAM_BRANCH }}
|
git push origin dev
|
||||||
build-amd64:
|
build-amd64:
|
||||||
name: Build docker image for amd64
|
name: Build docker image for amd64
|
||||||
needs: release
|
needs: release
|
||||||
@@ -125,7 +123,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: "${{ env.GHCR_REPO }}"
|
images: "${{ env.GHCR_REPO }}"
|
||||||
|
|
||||||
@@ -137,11 +135,11 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build and push by digest
|
- name: Build and push by digest
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
network: host
|
network: host
|
||||||
@@ -164,7 +162,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: "${{ env.GHCR_REPO }}"
|
images: "${{ env.GHCR_REPO }}"
|
||||||
|
|
||||||
@@ -176,11 +174,11 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build and push by digest
|
- name: Build and push by digest
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
network: host
|
network: host
|
||||||
@@ -233,10 +231,10 @@ 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@v3
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||||
client-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
|
||||||
@@ -276,6 +274,6 @@ jobs:
|
|||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
uses: Ilshidur/action-discord@0.4.0
|
uses: Ilshidur/action-discord@master
|
||||||
with:
|
with:
|
||||||
args: "Successfully deployed images for branch **${{ github.ref_name }}**. Tagged as **${{env.NEXT_VERSION}}**."
|
args: "Successfully deployed images for branch **${{ github.ref_name }}**. Tagged as **${{env.NEXT_VERSION}}**."
|
||||||
@@ -21,10 +21,10 @@ jobs:
|
|||||||
timeout-minutes: 2
|
timeout-minutes: 2
|
||||||
steps:
|
steps:
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
if: ${{ github.event.inputs.send-notifications }}
|
if: ${{ github.events.inputs.send-notifications }}
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
uses: Ilshidur/action-discord@0.4.0
|
uses: Ilshidur/action-discord@master
|
||||||
with:
|
with:
|
||||||
args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
|
args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -38,23 +38,22 @@ jobs:
|
|||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
branch: dev
|
branch: dev
|
||||||
- name: Create pull request
|
- name: Create pull request
|
||||||
id: create-pull-request
|
|
||||||
run: 'gh pr create --title "chore(release): automatic release ${{ steps.semver.outputs.next }}" --body "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}``" --base main --head dev --label automerge'
|
run: 'gh pr create --title "chore(release): automatic release ${{ steps.semver.outputs.next }}" --body "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}``" --base main --head dev --label automerge'
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
if: ${{ github.event.inputs.send-notifications }}
|
if: ${{ github.events.inputs.send-notifications }}
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
uses: Ilshidur/action-discord@0.4.0
|
uses: Ilshidur/action-discord@master
|
||||||
with:
|
with:
|
||||||
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@v3
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
|
||||||
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
|
||||||
permission-pull-requests: write
|
permission-pull-requests: write
|
||||||
- name: Approve PR
|
- name: Approve PR
|
||||||
env:
|
env:
|
||||||
@@ -63,10 +62,10 @@ 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@v3
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||||
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||||
permission-contents: write # write to main branch (due to merge)
|
permission-contents: write # write to main branch (due to merge)
|
||||||
permission-pull-requests: write # merge pull request
|
permission-pull-requests: write # merge pull request
|
||||||
- id: automerge
|
- id: automerge
|
||||||
@@ -75,7 +74,7 @@ jobs:
|
|||||||
uses: "pascalgn/automerge-action@v0.16.4"
|
uses: "pascalgn/automerge-action@v0.16.4"
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.obtainMergeToken.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.obtainMergeToken.outputs.token }}
|
||||||
MERGE_METHOD: merge # we prefer merge commits for merging to main
|
MERGE_METHOD: merge # we prefer merge commits for merging to master
|
||||||
MERGE_COMMIT_MESSAGE: "chore(release): automatic release ${{ steps.semver.outputs.next }}"
|
MERGE_COMMIT_MESSAGE: "chore(release): automatic release ${{ steps.semver.outputs.next }}"
|
||||||
MERGE_DELETE_BRANCH: false # never set to true!
|
MERGE_DELETE_BRANCH: false # never set to true!
|
||||||
PULL_REQUEST: "${{ steps.create-pull-request.outputs.pr_number }}"
|
PULL_REQUEST: "${{ steps.create-pull-request.outputs.pr_number }}"
|
||||||
@@ -84,23 +83,23 @@ jobs:
|
|||||||
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
|
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
|
||||||
|
|
||||||
- name: Merged Discord notification
|
- name: Merged Discord notification
|
||||||
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && github.event.inputs.send-notifications }}
|
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && github.events.inputs.send-notifications }}
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
uses: Ilshidur/action-discord@0.4.0
|
uses: Ilshidur/action-discord@master
|
||||||
with:
|
with:
|
||||||
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
|
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
|
||||||
- name: Major Bump Discord notification
|
- name: Major Bump Discord notification
|
||||||
if: ${{ steps.semver.outputs.bump == 'major' && github.event.inputs.send-notifications }}
|
if: ${{ steps.semver.outputs.bump == 'major' && github.events.inputs.send-notifications }}
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
uses: Ilshidur/action-discord@0.4.0
|
uses: Ilshidur/action-discord@master
|
||||||
with:
|
with:
|
||||||
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
|
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
|
||||||
- name: Discord Fail Notification
|
- name: Discord Fail Notification
|
||||||
if: failure() && github.event.inputs.send-notifications
|
if: failure() && github.events.inputs.send-notifications
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
uses: Ilshidur/action-discord@0.4.0
|
uses: Ilshidur/action-discord@master
|
||||||
with:
|
with:
|
||||||
args: "The automatic release workflow [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) has failed"
|
args: "The automatic release workflow [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) has failed"
|
||||||
@@ -12,10 +12,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: actions/create-github-app-token@v3
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.HOMARR_DOCS_RELEASE_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.HOMARR_DOCS_RELEASE_APP_PRIVATE_KEY }}
|
||||||
client-id: ${{ vars.HOMARR_DOCS_RELEASE_APP_ID }}
|
app-id: ${{ vars.HOMARR_DOCS_RELEASE_APP_ID }}
|
||||||
owner: homarr-labs
|
owner: homarr-labs
|
||||||
repositories: |
|
repositories: |
|
||||||
documentation
|
documentation
|
||||||
@@ -45,10 +45,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: actions/create-github-app-token@v3
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
|
||||||
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||||
permission-contents: write # required to commit to branch
|
permission-contents: write # required to commit to branch
|
||||||
permission-pull-requests: write # required to create pr & enable automerge
|
permission-pull-requests: write # required to create pr & enable automerge
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
skip-stability-days:
|
skip-stability-days:
|
||||||
if: ${{ !startsWith(github.head_ref, 'renovate/') && github.event.pull_request.head.repo.full_name == github.repository }}
|
if: ${{ !startsWith(github.head_ref, 'renovate/') }}
|
||||||
name: Skip Stability Days
|
name: Skip Stability Days
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 1
|
timeout-minutes: 1
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: actions/create-github-app-token@v3
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
||||||
client-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
app-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
||||||
permission-contents: write # required to commit to branch
|
permission-contents: write # required to commit to branch
|
||||||
permission-pull-requests: write # required to create pr & enable automerge
|
permission-pull-requests: write # required to create pr & enable automerge
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v4
|
||||||
- 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,10 +21,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Obtain token
|
- name: Obtain token
|
||||||
id: obtainToken
|
id: obtainToken
|
||||||
uses: actions/create-github-app-token@v3
|
uses: actions/create-github-app-token@v2
|
||||||
with:
|
with:
|
||||||
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
|
||||||
client-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
app-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
|
||||||
permission-contents: write # required to commit to branch
|
permission-contents: write # required to commit to branch
|
||||||
permission-pull-requests: write # required to create pr & enable automerge
|
permission-pull-requests: write # required to create pr & enable automerge
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
+1
-5
@@ -67,8 +67,4 @@ e2e/shared/tmp
|
|||||||
apps/nextjs/public/images/background.png
|
apps/nextjs/public/images/background.png
|
||||||
|
|
||||||
# next-intl
|
# next-intl
|
||||||
en.d.json.ts
|
en.d.json.ts
|
||||||
|
|
||||||
# personal agent config
|
|
||||||
AGENTS.md
|
|
||||||
.opencode
|
|
||||||
-223
@@ -1,226 +1,3 @@
|
|||||||
## [1.60.1-beta.1](https://github.com/homarr-labs/homarr/compare/v1.60.0...v1.60.1-beta.1) (2026-05-01)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* **memory:** combine 3 node processes into one ([#5600](https://github.com/homarr-labs/homarr/issues/5600)) ([a1aa550](https://github.com/homarr-labs/homarr/commit/a1aa5505a365688b456090a882770e561479833e))
|
|
||||||
|
|
||||||
## [1.60.1-beta.1](https://github.com/homarr-labs/homarr/compare/v1.60.0...v1.60.1-beta.1) (2026-05-01)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* **memory:** combine 3 node processes into one ([#5600](https://github.com/homarr-labs/homarr/issues/5600)) ([a1aa550](https://github.com/homarr-labs/homarr/commit/a1aa5505a365688b456090a882770e561479833e))
|
|
||||||
|
|
||||||
## [1.60.1-beta.1](https://github.com/homarr-labs/homarr/compare/v1.60.0...v1.60.1-beta.1) (2026-05-01)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* **memory:** combine 3 node processes into one ([#5600](https://github.com/homarr-labs/homarr/issues/5600)) ([a1aa550](https://github.com/homarr-labs/homarr/commit/a1aa5505a365688b456090a882770e561479833e))
|
|
||||||
|
|
||||||
## [1.60.0](https://github.com/homarr-labs/homarr/compare/v1.59.3...v1.60.0) (2026-05-01)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **about:** add opencollective contributors ([#5481](https://github.com/homarr-labs/homarr/issues/5481)) ([3d202d4](https://github.com/homarr-labs/homarr/commit/3d202d409febf33d0218b589c979744c84f87407))
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* migrate from node-cron to croner ([#5407](https://github.com/homarr-labs/homarr/issues/5407)) ([e8edc20](https://github.com/homarr-labs/homarr/commit/e8edc203b6c862141668b043ef4232c22b8fae24))
|
|
||||||
* **openapi:** openapi document empty ([#5579](https://github.com/homarr-labs/homarr/issues/5579)) ([ce8e330](https://github.com/homarr-labs/homarr/commit/ce8e330c82bc0be9f299f3071a1e80daafc5f789))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* **icons:** updater takes 10 seconds to run with high cpu usage ([#5608](https://github.com/homarr-labs/homarr/issues/5608)) ([ed6680b](https://github.com/homarr-labs/homarr/commit/ed6680ba6a42864185818eb8d90b2303846c1761))
|
|
||||||
|
|
||||||
## [1.60.0](https://github.com/homarr-labs/homarr/compare/v1.59.3...v1.60.0) (2026-05-01)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **about:** add opencollective contributors ([#5481](https://github.com/homarr-labs/homarr/issues/5481)) ([3d202d4](https://github.com/homarr-labs/homarr/commit/3d202d409febf33d0218b589c979744c84f87407))
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* migrate from node-cron to croner ([#5407](https://github.com/homarr-labs/homarr/issues/5407)) ([e8edc20](https://github.com/homarr-labs/homarr/commit/e8edc203b6c862141668b043ef4232c22b8fae24))
|
|
||||||
* **openapi:** openapi document empty ([#5579](https://github.com/homarr-labs/homarr/issues/5579)) ([ce8e330](https://github.com/homarr-labs/homarr/commit/ce8e330c82bc0be9f299f3071a1e80daafc5f789))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* **icons:** updater takes 10 seconds to run with high cpu usage ([#5608](https://github.com/homarr-labs/homarr/issues/5608)) ([ed6680b](https://github.com/homarr-labs/homarr/commit/ed6680ba6a42864185818eb8d90b2303846c1761))
|
|
||||||
|
|
||||||
## [1.59.3](https://github.com/homarr-labs/homarr/compare/v1.59.2...v1.59.3) (2026-04-24)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **deps:** update dependency better-sqlite3 to ^12.9.0 ([#5531](https://github.com/homarr-labs/homarr/issues/5531)) ([7852f1d](https://github.com/homarr-labs/homarr/commit/7852f1d987d40707ae13494f9c934a563581f97f))
|
|
||||||
|
|
||||||
## [1.59.3](https://github.com/homarr-labs/homarr/compare/v1.59.2...v1.59.3) (2026-04-24)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **deps:** update dependency better-sqlite3 to ^12.9.0 ([#5531](https://github.com/homarr-labs/homarr/issues/5531)) ([7852f1d](https://github.com/homarr-labs/homarr/commit/7852f1d987d40707ae13494f9c934a563581f97f))
|
|
||||||
|
|
||||||
## [1.59.2](https://github.com/homarr-labs/homarr/compare/v1.59.1...v1.59.2) (2026-04-17)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **about:** add missing dependencies ([#5480](https://github.com/homarr-labs/homarr/issues/5480)) ([c48588c](https://github.com/homarr-labs/homarr/commit/c48588c6a748920b05f8afcd79fad17dc65f2930))
|
|
||||||
* **deps:** update dependency @drizzle-team/brocli to ^0.12.0 ([#5489](https://github.com/homarr-labs/homarr/issues/5489)) ([74611b7](https://github.com/homarr-labs/homarr/commit/74611b75a736df43a77ebc780d2e0eee5c40e3f3))
|
|
||||||
* **deps:** update dependency @immich/sdk to ^2.7.0 ([#5490](https://github.com/homarr-labs/homarr/issues/5490)) ([9cdf2aa](https://github.com/homarr-labs/homarr/commit/9cdf2aa1f2b1477522879095b603b100c1dd3ac0))
|
|
||||||
* **deps:** update dependency @immich/sdk to ^2.7.2 ([#5494](https://github.com/homarr-labs/homarr/issues/5494)) ([0927f45](https://github.com/homarr-labs/homarr/commit/0927f45b2573753152fd5e9e318ba62b88abca35))
|
|
||||||
* **deps:** update dependency fast-xml-parser to ^5.5.11 ([#5500](https://github.com/homarr-labs/homarr/issues/5500)) ([652ed1a](https://github.com/homarr-labs/homarr/commit/652ed1a5ed0beeb2dbd44976890f24dc12b90cf6))
|
|
||||||
* **deps:** update dependency isomorphic-dompurify to ^3.8.0 ([#5503](https://github.com/homarr-labs/homarr/issues/5503)) ([0731b7c](https://github.com/homarr-labs/homarr/commit/0731b7c5ef272bcf69c566cdec723f2696309e67))
|
|
||||||
* **deps:** update dependency jotai to ^2.19.1 ([#5487](https://github.com/homarr-labs/homarr/issues/5487)) ([efd8a55](https://github.com/homarr-labs/homarr/commit/efd8a55b6ea113bc42bc90d9ccb0db6c1d2b68ef))
|
|
||||||
* **deps:** update dependency next-intl to v4.9.1 [security] ([#5470](https://github.com/homarr-labs/homarr/issues/5470)) ([f2437c4](https://github.com/homarr-labs/homarr/commit/f2437c473106689e9fa93e7dcbea84b00800ea4a))
|
|
||||||
* **deps:** update dependency swagger-ui-react to ^5.32.2 ([#5488](https://github.com/homarr-labs/homarr/issues/5488)) ([cf131c5](https://github.com/homarr-labs/homarr/commit/cf131c5c51f6c21f942ab89d02353f93dc05774c))
|
|
||||||
* **deps:** update dependency typescript-eslint to ^8.58.1 ([#5496](https://github.com/homarr-labs/homarr/issues/5496)) ([f88984c](https://github.com/homarr-labs/homarr/commit/f88984cda844beb1099a08c9cdeb6691ad13b5a8))
|
|
||||||
* **deps:** update react monorepo to v19.2.5 ([#5502](https://github.com/homarr-labs/homarr/issues/5502)) ([c8a61a9](https://github.com/homarr-labs/homarr/commit/c8a61a9df3087de28c76f573623725eec246dd32))
|
|
||||||
* **deps:** update testcontainers-node monorepo to ^11.14.0 ([#5498](https://github.com/homarr-labs/homarr/issues/5498)) ([1057b47](https://github.com/homarr-labs/homarr/commit/1057b478c37d02e9de7bcd155f0b3fb764149abe))
|
|
||||||
* **deps:** update tiptap monorepo to v3.22.3 ([#5499](https://github.com/homarr-labs/homarr/issues/5499)) ([95abd2c](https://github.com/homarr-labs/homarr/commit/95abd2c9d3a563c7eaebe12f3b8015e3299e27e8))
|
|
||||||
* **deps:** update turbo monorepo to ^2.9.4 ([#5476](https://github.com/homarr-labs/homarr/issues/5476)) ([813f2fa](https://github.com/homarr-labs/homarr/commit/813f2fa5efb999d3364882578fdf456537dba3a6))
|
|
||||||
* **deps:** update turbo monorepo to ^2.9.5 ([#5491](https://github.com/homarr-labs/homarr/issues/5491)) ([ed24ce6](https://github.com/homarr-labs/homarr/commit/ed24ce6381466fa530ce225363231303be4ed92e))
|
|
||||||
* preserve all-day event dates in Nextcloud calendar integration ([#5523](https://github.com/homarr-labs/homarr/issues/5523)) ([2e78979](https://github.com/homarr-labs/homarr/commit/2e789795ed11c816550b6a3ce88fe92a360ede9c))
|
|
||||||
|
|
||||||
## [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)
|
## [1.56.1](https://github.com/homarr-labs/homarr/compare/v1.56.0...v1.56.1) (2026-03-20)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
+3
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24.15.0-alpine AS base
|
FROM node:24.14.0-alpine AS base
|
||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat
|
||||||
@@ -43,6 +43,8 @@ RUN mkdir -p /var/cache/nginx && \
|
|||||||
COPY --from=builder /app/apps/nextjs/next.config.ts .
|
COPY --from=builder /app/apps/nextjs/next.config.ts .
|
||||||
COPY --from=builder /app/apps/nextjs/package.json .
|
COPY --from=builder /app/apps/nextjs/package.json .
|
||||||
|
|
||||||
|
COPY --from=builder /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
|
||||||
|
COPY --from=builder /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
|
||||||
COPY --from=builder /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
|
COPY --from=builder /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
|
||||||
|
|
||||||
COPY --from=builder /app/packages/db/migrations ./db/migrations
|
COPY --from=builder /app/packages/db/migrations ./db/migrations
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ const nextConfig: NextConfig = {
|
|||||||
experimental: {
|
experimental: {
|
||||||
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
|
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
|
||||||
turbopackFileSystemCacheForDev: true,
|
turbopackFileSystemCacheForDev: true,
|
||||||
preloadEntriesOnStart: false,
|
|
||||||
webpackMemoryOptimizations: true,
|
|
||||||
},
|
},
|
||||||
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
|
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
|
||||||
images: {
|
images: {
|
||||||
|
|||||||
+56
-59
@@ -15,10 +15,10 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "catalog:",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "catalog:",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "catalog:",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "catalog:",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@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": "catalog:",
|
"@homarr/gridstack": "^1.12.0",
|
||||||
"@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",
|
||||||
@@ -44,68 +44,65 @@
|
|||||||
"@homarr/server-settings": "workspace:^0.1.0",
|
"@homarr/server-settings": "workspace:^0.1.0",
|
||||||
"@homarr/settings": "workspace:^0.1.0",
|
"@homarr/settings": "workspace:^0.1.0",
|
||||||
"@homarr/spotlight": "workspace:^0.1.0",
|
"@homarr/spotlight": "workspace:^0.1.0",
|
||||||
"@homarr/tasks": "workspace:^0.1.0",
|
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@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/websocket": "workspace:^0.1.0",
|
|
||||||
"@homarr/widgets": "workspace:^0.1.0",
|
"@homarr/widgets": "workspace:^0.1.0",
|
||||||
"@mantine/colors-generator": "catalog:",
|
"@mantine/colors-generator": "^8.3.18",
|
||||||
"@mantine/core": "catalog:",
|
"@mantine/core": "^8.3.18",
|
||||||
"@mantine/dropzone": "catalog:",
|
"@mantine/dropzone": "^8.3.18",
|
||||||
"@mantine/hooks": "catalog:",
|
"@mantine/hooks": "^8.3.18",
|
||||||
"@mantine/modals": "catalog:",
|
"@mantine/modals": "^8.3.18",
|
||||||
"@mantine/tiptap": "catalog:",
|
"@mantine/tiptap": "^8.3.18",
|
||||||
"@million/lint": "catalog:",
|
"@million/lint": "1.0.14",
|
||||||
"@tabler/icons-react": "catalog:",
|
"@tabler/icons-react": "^3.40.0",
|
||||||
"@tanstack/react-query": "catalog:",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"@tanstack/react-query-devtools": "catalog:",
|
"@tanstack/react-query-devtools": "^5.91.3",
|
||||||
"@tanstack/react-query-next-experimental": "catalog:",
|
"@tanstack/react-query-next-experimental": "^5.91.0",
|
||||||
"@trpc/client": "catalog:",
|
"@trpc/client": "^11.13.4",
|
||||||
"@trpc/next": "catalog:",
|
"@trpc/next": "^11.13.4",
|
||||||
"@trpc/react-query": "catalog:",
|
"@trpc/react-query": "^11.13.4",
|
||||||
"@trpc/server": "catalog:",
|
"@trpc/server": "^11.13.4",
|
||||||
"@xterm/addon-canvas": "catalog:",
|
"@xterm/addon-canvas": "^0.7.0",
|
||||||
"@xterm/addon-fit": "catalog:",
|
"@xterm/addon-fit": "0.11.0",
|
||||||
"@xterm/xterm": "catalog:",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"babel-plugin-react-compiler": "catalog:",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"chroma-js": "catalog:",
|
"chroma-js": "^3.2.0",
|
||||||
"clsx": "catalog:",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "catalog:",
|
"dayjs": "^1.11.20",
|
||||||
"dotenv": "catalog:",
|
"dotenv": "^17.3.1",
|
||||||
"flag-icons": "catalog:",
|
"flag-icons": "^7.5.0",
|
||||||
"glob": "catalog:",
|
"glob": "^13.0.6",
|
||||||
"isomorphic-dompurify": "catalog:",
|
"isomorphic-dompurify": "^3.5.1",
|
||||||
"jotai": "catalog:",
|
"jotai": "^2.18.1",
|
||||||
"mantine-react-table": "catalog:",
|
"mantine-react-table": "2.0.0-beta.9",
|
||||||
"next": "catalog:",
|
"next": "16.1.7",
|
||||||
"postcss-preset-mantine": "catalog:",
|
"postcss-preset-mantine": "^1.18.0",
|
||||||
"prismjs": "catalog:",
|
"prismjs": "^1.30.0",
|
||||||
"react": "catalog:",
|
"react": "19.2.4",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "19.2.4",
|
||||||
"react-error-boundary": "catalog:",
|
"react-error-boundary": "^6.1.1",
|
||||||
"react-simple-code-editor": "catalog:",
|
"react-simple-code-editor": "^0.14.1",
|
||||||
"sass": "catalog:",
|
"sass": "^1.98.0",
|
||||||
"superjson": "catalog:",
|
"superjson": "2.2.6",
|
||||||
"swagger-ui-react": "catalog:",
|
"swagger-ui-react": "^5.32.1",
|
||||||
"use-deep-compare-effect": "catalog:",
|
"use-deep-compare-effect": "^1.8.1",
|
||||||
"yaml": "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/chroma-js": "catalog:",
|
"@types/chroma-js": "3.1.2",
|
||||||
"@types/node": "catalog:",
|
"@types/node": "^24.12.0",
|
||||||
"@types/prismjs": "catalog:",
|
"@types/prismjs": "^1.26.6",
|
||||||
"@types/react": "catalog:",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "catalog:",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/swagger-ui-react": "catalog:",
|
"@types/swagger-ui-react": "^5.18.0",
|
||||||
"concurrently": "catalog:",
|
"concurrently": "^9.2.1",
|
||||||
"eslint": "catalog:",
|
"eslint": "^9.39.4",
|
||||||
"node-loader": "catalog:",
|
"node-loader": "^2.1.0",
|
||||||
"prettier": "catalog:",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "catalog:"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,6 @@ 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";
|
||||||
@@ -71,7 +70,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(sanitizeRedirectionUrl(callbackUrl));
|
router.push(callbackUrl);
|
||||||
},
|
},
|
||||||
[t, router, callbackUrl],
|
[t, router, callbackUrl],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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";
|
||||||
@@ -20,7 +19,7 @@ export default async function Login(props: LoginProps) {
|
|||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
redirect(sanitizeRedirectionUrl(searchParams.callbackUrl));
|
redirect(searchParams.callbackUrl ?? "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = await getScopedI18n("user.page.login");
|
const t = await getScopedI18n("user.page.login");
|
||||||
|
|||||||
@@ -264,5 +264,6 @@ 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,6 +27,7 @@ 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 } from "@mantine/hooks";
|
import { useDebouncedValue, useDocumentTitle, useFavicon } 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,7 +10,6 @@ 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";
|
||||||
@@ -54,6 +53,7 @@ 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,7 +75,6 @@ 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,
|
||||||
@@ -147,13 +146,7 @@ const useMetaTitlePreview = (title: string | null) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Previously we used the useFavicon hook from Mantine
|
const useFaviconPreview = (url: string | null) => {
|
||||||
// However if the user tried to click on a link to a different page
|
const [faviconDebounced] = useDebouncedValue(url ?? "", 500);
|
||||||
// it caused an error "Cannot read properties of null (reading 'removeChild')"
|
useFavicon(faviconDebounced);
|
||||||
// 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,7 +1,3 @@
|
|||||||
.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,7 +12,8 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
Kbd,
|
Kbd,
|
||||||
SimpleGrid,
|
List,
|
||||||
|
ListItem,
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
TableTbody,
|
TableTbody,
|
||||||
@@ -21,18 +22,9 @@ import {
|
|||||||
TableThead,
|
TableThead,
|
||||||
TableTr,
|
TableTr,
|
||||||
Text,
|
Text,
|
||||||
ThemeIcon,
|
|
||||||
Title,
|
Title,
|
||||||
UnstyledButton,
|
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import { IconKeyboard, IconLanguage, IconLibrary, IconUsers } from "@tabler/icons-react";
|
||||||
IconKeyboard,
|
|
||||||
IconLanguage,
|
|
||||||
IconLibrary,
|
|
||||||
IconMoneybagHeart,
|
|
||||||
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";
|
||||||
@@ -41,10 +33,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 type { PackageJsonDependencies } from "~/versions/package-reader";
|
import { getPackageAttributesAsync } 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 openCollectiveContributorsJson from "../../../../../../../static-data/opencollective-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";
|
||||||
|
|
||||||
@@ -67,10 +57,7 @@ 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 version = getPackageVersion();
|
const attributes = await getPackageAttributesAsync();
|
||||||
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;
|
||||||
@@ -79,10 +66,6 @@ export default async function AboutPage() {
|
|||||||
res.json(),
|
res.json(),
|
||||||
)) as typeof crowdinContributorsJson;
|
)) as typeof crowdinContributorsJson;
|
||||||
|
|
||||||
const openCollectiveContributors = (await fetch(`${baseServerUrl}/api/about/contributors/opencollective`).then(
|
|
||||||
(res) => res.json(),
|
|
||||||
)) as typeof openCollectiveContributorsJson;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<DynamicBreadcrumb />
|
<DynamicBreadcrumb />
|
||||||
@@ -93,7 +76,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 })}</Title>
|
<Title order={2}>{t("version", { version: attributes.version })}</Title>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Center>
|
</Center>
|
||||||
@@ -148,72 +131,31 @@ export default async function AboutPage() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem value="sponsors">
|
|
||||||
<AccordionControl icon={<IconMoneybagHeart size="1rem" />}>
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Text>{t("accordion.sponsors.title")}</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{t("accordion.sponsors.subtitle", {
|
|
||||||
count: String(openCollectiveContributors.length),
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</AccordionControl>
|
|
||||||
<AccordionPanel>
|
|
||||||
<Flex wrap="wrap" gap="xs">
|
|
||||||
{openCollectiveContributors.map((sponsor) => (
|
|
||||||
<GenericContributorLinkCard
|
|
||||||
key={sponsor.slug}
|
|
||||||
link={`https://opencollective.com/${sponsor.slug}`}
|
|
||||||
image={sponsor.imageUrl}
|
|
||||||
name={sponsor.name}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
</AccordionPanel>
|
|
||||||
</AccordionItem>
|
|
||||||
<AccordionItem value="libraries">
|
<AccordionItem value="libraries">
|
||||||
<AccordionControl icon={<IconLibrary size="1rem" />}>
|
<AccordionControl icon={<IconLibrary size="1rem" />}>
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
<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(dependencies).length),
|
count: String(Object.keys(attributes.dependencies).length),
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</AccordionControl>
|
</AccordionControl>
|
||||||
<AccordionPanel>
|
<AccordionPanel>
|
||||||
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3, lg: 4, xl: 5 }} spacing="md">
|
<List>
|
||||||
{Object.entries(dependencies)
|
{Object.entries(attributes.dependencies)
|
||||||
.filter(([, value]) => !value.includes("workspace:"))
|
|
||||||
.sort(([key1], [key2]) => key1.localeCompare(key2))
|
.sort(([key1], [key2]) => key1.localeCompare(key2))
|
||||||
.map(([name, version]) => (
|
.map(([key, value]) => (
|
||||||
<UnstyledButton
|
<ListItem key={key}>
|
||||||
key={name}
|
{value.includes("workspace:") ? (
|
||||||
component="a"
|
<Text>{key}</Text>
|
||||||
href={`https://www.npmjs.com/package/${name}`}
|
) : (
|
||||||
target="_blank"
|
<a href={`https://www.npmjs.com/package/${key}`}>{key}</a>
|
||||||
>
|
)}
|
||||||
<Card withBorder radius="md" p="xs" className={classes.dependencyCard}>
|
</ListItem>
|
||||||
<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>
|
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</List>
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem value="hotkeys">
|
<AccordionItem value="hotkeys">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { headers } from "next/headers";
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab } from "@mantine/core";
|
import { Stack, Tabs, TabsList, TabsPanel, TabsTab } from "@mantine/core";
|
||||||
|
|
||||||
import { openApiDocument } from "@homarr/api/open-api";
|
import { openApiDocument } from "@homarr/api";
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { extractBaseUrlFromHeaders } from "@homarr/common";
|
import { extractBaseUrlFromHeaders } from "@homarr/common";
|
||||||
@@ -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.permissions.includes("admin")) {
|
if (!session?.user || !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.permissions.includes("admin")) {
|
if (!session?.user || !session.user.permissions.includes("admin")) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
const document = openApiDocument(extractBaseUrlFromHeaders(await headers()));
|
const document = openApiDocument(extractBaseUrlFromHeaders(await headers()));
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ 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.permissions.includes("admin")) {
|
if (!session?.user || !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.permissions.includes("other-view-logs")) {
|
if (!session?.user || !session.user.permissions.includes("other-view-logs")) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
|
|||||||
},
|
},
|
||||||
onSuccess(_, variables) {
|
onSuccess(_, variables) {
|
||||||
form.setInitialValues({
|
form.setInitialValues({
|
||||||
firstDayOfWeek: variables.firstDayOfWeek as DayOfWeek,
|
firstDayOfWeek: variables.firstDayOfWeek,
|
||||||
});
|
});
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
message: t("user.action.changeFirstDayOfWeek.notification.success.message"),
|
message: t("user.action.changeFirstDayOfWeek.notification.success.message"),
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import type { NextRequest } from "next/server";
|
|||||||
import { userAgent } from "next/server";
|
import { userAgent } from "next/server";
|
||||||
import { createOpenApiFetchHandler } from "trpc-to-openapi";
|
import { createOpenApiFetchHandler } from "trpc-to-openapi";
|
||||||
|
|
||||||
import { createTRPCContext } from "@homarr/api";
|
import { appRouter, createTRPCContext } from "@homarr/api";
|
||||||
import { openApiRouter } from "@homarr/api/open-api";
|
|
||||||
import { API_KEY_HEADER_NAME, getSessionFromApiKeyAsync } from "@homarr/auth/api-key";
|
import { API_KEY_HEADER_NAME, getSessionFromApiKeyAsync } from "@homarr/auth/api-key";
|
||||||
import { ipAddressFromHeaders } from "@homarr/common/server";
|
import { ipAddressFromHeaders } from "@homarr/common/server";
|
||||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
@@ -31,7 +30,7 @@ const handlerAsync = async (req: NextRequest) => {
|
|||||||
return createOpenApiFetchHandler({
|
return createOpenApiFetchHandler({
|
||||||
req,
|
req,
|
||||||
endpoint: "/",
|
endpoint: "/",
|
||||||
router: openApiRouter,
|
router: appRouter,
|
||||||
createContext: () => createTRPCContext({ session, headers: req.headers }),
|
createContext: () => createTRPCContext({ session, headers: req.headers }),
|
||||||
onError({ error, path, type }) {
|
onError({ error, path, type }) {
|
||||||
logger.error(new ErrorWithMetadata("tRPC Error occured", { path, type }, { cause: error }));
|
logger.error(new ErrorWithMetadata("tRPC Error occured", { path, type }, { cause: error }));
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
|
|
||||||
import openCollectiveContributors from "../../../../../../../../static-data/opencollective-contributors.json";
|
|
||||||
|
|
||||||
export const GET = () => {
|
|
||||||
return NextResponse.json(openCollectiveContributors);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const dynamic = "force-static";
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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,6 +75,7 @@ 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",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
import { openApiDocument } from "@homarr/api/open-api";
|
import { openApiDocument } from "@homarr/api";
|
||||||
import { extractBaseUrlFromHeaders } from "@homarr/common";
|
import { extractBaseUrlFromHeaders } from "@homarr/common";
|
||||||
|
|
||||||
export function GET(request: Request) {
|
export function GET(request: Request) {
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ 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)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ 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]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ interface LogoProps {
|
|||||||
|
|
||||||
export const Logo = ({ size = 60, shouldUseNextImage = false, src, alt }: LogoProps) =>
|
export const Logo = ({ size = 60, shouldUseNextImage = false, src, alt }: LogoProps) =>
|
||||||
shouldUseNextImage ? (
|
shouldUseNextImage ? (
|
||||||
<Image className="logo" src={src} alt={alt} width={size} height={size} />
|
<Image className="logo header-logo" src={src} alt={alt} width={size} height={size} />
|
||||||
) : (
|
) : (
|
||||||
// we only want to use next/image for logos that we are sure will be preloaded and are allowed
|
// we only want to use next/image for logos that we are sure will be preloaded and are allowed
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img className="logo" src={src} alt={alt} width={size} height={size} />
|
<img className="logo header-logo" src={src} alt={alt} width={size} height={size} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const logoWithTitleSizes = {
|
const logoWithTitleSizes = {
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export async function register() {
|
|
||||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
||||||
await import("@homarr/tasks");
|
|
||||||
await import("@homarr/websocket");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
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";
|
||||||
|
|
||||||
export const getPackageVersion = () => packageJson.version;
|
const getPackageVersion = () => packageJson.version;
|
||||||
export const getDependenciesAsync = async (): Promise<PackageJsonDependencies> => {
|
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,
|
||||||
});
|
});
|
||||||
@@ -19,37 +15,21 @@ export 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) {
|
||||||
const resolvedDependencies = Object.entries(dependenciesOfPackage).map(([name, version]) => {
|
dependencies = { ...dependencies, ...dependenciesOfPackage };
|
||||||
const catalogVersion = catalog.get(name);
|
|
||||||
return [name, catalogVersion ?? version] as const;
|
|
||||||
});
|
|
||||||
dependencies = { ...dependencies, ...Object.fromEntries(resolvedDependencies) };
|
|
||||||
}
|
}
|
||||||
return dependencies;
|
return dependencies;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DependencyCatalog = Map<string, string>;
|
export const getPackageAttributesAsync = async () => {
|
||||||
const parseDependencyCatalogsAsync = async (): Promise<DependencyCatalog> => {
|
return {
|
||||||
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
version: getPackageVersion(),
|
||||||
const workspaceConfigContent = await fsPromises.readFile(
|
dependencies: await getDependenciesAsync(),
|
||||||
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 ?? {}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const workspaceConfigCatalogSchema = z.object({
|
type PackageJsonDependencies = Record<string, string>;
|
||||||
catalog: z.record(z.string(), z.string()).optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type PackageJsonDependencies = Record<string, string>;
|
|
||||||
interface PackageJson {
|
interface PackageJson {
|
||||||
dependencies: PackageJsonDependencies | undefined;
|
dependencies: PackageJsonDependencies | undefined;
|
||||||
}
|
}
|
||||||
+15
-14
@@ -5,11 +5,12 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/main.ts"
|
".": "./src/index.ts"
|
||||||
},
|
},
|
||||||
"main": "./src/main.ts",
|
"main": "./src/main.ts",
|
||||||
"types": "./src/main.ts",
|
"types": "./src/main.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build": "esbuild src/main.ts --bundle --platform=node --loader:.scss=text --external:*.node --external:@opentelemetry/api --external:deasync --external:bcrypt --outfile=tasks.cjs",
|
||||||
"clean": "rm -rf .turbo node_modules",
|
"clean": "rm -rf .turbo node_modules",
|
||||||
"dev": "pnpm with-env tsx ./src/main.ts",
|
"dev": "pnpm with-env tsx ./src/main.ts",
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
"@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/core": "workspace:^",
|
"@homarr/core": "workspace:^",
|
||||||
|
"@homarr/cron-job-api": "workspace:^0.1.0",
|
||||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||||
"@homarr/cron-jobs-core": "workspace:^0.1.0",
|
"@homarr/cron-jobs-core": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
@@ -35,23 +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",
|
||||||
"croner": "catalog:",
|
"dayjs": "^1.11.20",
|
||||||
"dayjs": "catalog:",
|
"dotenv": "^17.3.1",
|
||||||
"dotenv": "catalog:",
|
"fastify": "^5.8.2",
|
||||||
"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": "catalog:",
|
"@types/node": "^24.12.0",
|
||||||
"dotenv-cli": "catalog:",
|
"dotenv-cli": "^11.0.0",
|
||||||
"esbuild": "catalog:",
|
"esbuild": "^0.27.4",
|
||||||
"eslint": "catalog:",
|
"eslint": "^9.39.4",
|
||||||
"prettier": "catalog:",
|
"prettier": "^3.8.1",
|
||||||
"tsx": "catalog:",
|
"tsx": "4.20.4",
|
||||||
"typescript": "catalog:"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Cron } from "croner";
|
import { schedule, validate as validateCron } from "node-cron";
|
||||||
|
|
||||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import type { IJobManager } from "@homarr/cron-job-api";
|
||||||
import type { jobGroup as cronJobGroup, JobGroupKeys } from "@homarr/cron-jobs";
|
import type { jobGroup as cronJobGroup, JobGroupKeys } from "@homarr/cron-jobs";
|
||||||
import type { Database, InferInsertModel } from "@homarr/db";
|
import type { Database, InferInsertModel } from "@homarr/db";
|
||||||
import { eq } from "@homarr/db";
|
import { eq } from "@homarr/db";
|
||||||
@@ -8,7 +9,7 @@ import { cronJobConfigurations } from "@homarr/db/schema";
|
|||||||
|
|
||||||
const logger = createLogger({ module: "jobManager" });
|
const logger = createLogger({ module: "jobManager" });
|
||||||
|
|
||||||
export class JobManager {
|
export class JobManager implements IJobManager {
|
||||||
constructor(
|
constructor(
|
||||||
private db: Database,
|
private db: Database,
|
||||||
private jobGroup: typeof cronJobGroup,
|
private jobGroup: typeof cronJobGroup,
|
||||||
@@ -20,31 +21,28 @@ export class JobManager {
|
|||||||
public async triggerAsync(name: JobGroupKeys): Promise<void> {
|
public async triggerAsync(name: JobGroupKeys): Promise<void> {
|
||||||
await this.jobGroup.runManuallyAsync(name);
|
await this.jobGroup.runManuallyAsync(name);
|
||||||
}
|
}
|
||||||
public stop(name: JobGroupKeys): void {
|
public async stopAsync(name: JobGroupKeys): Promise<void> {
|
||||||
this.jobGroup.stop(name);
|
await this.jobGroup.stopAsync(name);
|
||||||
}
|
}
|
||||||
public async updateIntervalAsync(name: JobGroupKeys, cron: string): Promise<void> {
|
public async updateIntervalAsync(name: JobGroupKeys, cron: string): Promise<void> {
|
||||||
logger.info("Updating cron job interval", { name, expression: cron });
|
logger.info("Updating cron job interval", { name, expression: cron });
|
||||||
const job = this.jobGroup.getJobRegistry().get(name);
|
const job = this.jobGroup.getJobRegistry().get(name);
|
||||||
if (!job) throw new Error(`Job ${name} not found`);
|
if (!job) throw new Error(`Job ${name} not found`);
|
||||||
|
if (!validateCron(cron)) {
|
||||||
|
throw new Error(`Invalid cron expression: ${cron}`);
|
||||||
|
}
|
||||||
if (job.preventCustomInterval && cron !== job.cronExpression) {
|
if (job.preventCustomInterval && cron !== job.cronExpression) {
|
||||||
throw new Error(`Custom cron expressions are not allowed for job ${name}`);
|
throw new Error(`Custom cron expressions are not allowed for job ${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedConfig = await this.updateConfigurationAsync(name, { cronExpression: cron });
|
await this.updateConfigurationAsync(name, { cronExpression: cron });
|
||||||
this.jobGroup.getTask(name)?.stop();
|
await this.jobGroup.getTask(name)?.destroy();
|
||||||
|
|
||||||
this.jobGroup.setTask(
|
this.jobGroup.setTask(
|
||||||
name,
|
name,
|
||||||
new Cron(
|
schedule(cron, () => void job.executeAsync(), {
|
||||||
cron,
|
name,
|
||||||
{
|
}),
|
||||||
name,
|
|
||||||
timezone: job.timezone,
|
|
||||||
paused: !updatedConfig.isEnabled,
|
|
||||||
},
|
|
||||||
() => void job.executeAsync(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
logger.info("Cron job interval updated", { name, expression: cron });
|
logger.info("Cron job interval updated", { name, expression: cron });
|
||||||
}
|
}
|
||||||
@@ -54,7 +52,7 @@ export class JobManager {
|
|||||||
if (!job) throw new Error(`Job ${name} not found`);
|
if (!job) throw new Error(`Job ${name} not found`);
|
||||||
|
|
||||||
await this.updateConfigurationAsync(name, { isEnabled: false });
|
await this.updateConfigurationAsync(name, { isEnabled: false });
|
||||||
this.jobGroup.stop(name);
|
await this.jobGroup.stopAsync(name);
|
||||||
logger.info("Cron job disabled", { name });
|
logger.info("Cron job disabled", { name });
|
||||||
}
|
}
|
||||||
public async enableAsync(name: JobGroupKeys): Promise<void> {
|
public async enableAsync(name: JobGroupKeys): Promise<void> {
|
||||||
@@ -88,27 +86,21 @@ export class JobManager {
|
|||||||
name,
|
name,
|
||||||
configuration: JSON.stringify(configuration),
|
configuration: JSON.stringify(configuration),
|
||||||
});
|
});
|
||||||
return {
|
return;
|
||||||
name,
|
|
||||||
cronExpression: configuration.cronExpression ?? existingConfig.cronExpression,
|
|
||||||
isEnabled: configuration.isEnabled ?? existingConfig.isEnabled,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const job = this.jobGroup.getJobRegistry().get(name);
|
const job = this.jobGroup.getJobRegistry().get(name);
|
||||||
if (!job) throw new Error(`Job ${name} not found`);
|
if (!job) throw new Error(`Job ${name} not found`);
|
||||||
|
|
||||||
const updatedConfig = {
|
await this.db.insert(cronJobConfigurations).values({
|
||||||
name,
|
name,
|
||||||
cronExpression: configuration.cronExpression ?? job.cronExpression,
|
cronExpression: configuration.cronExpression ?? job.cronExpression,
|
||||||
isEnabled: configuration.isEnabled ?? true,
|
isEnabled: configuration.isEnabled ?? true,
|
||||||
};
|
});
|
||||||
await this.db.insert(cronJobConfigurations).values(updatedConfig);
|
|
||||||
logger.debug("Cron job configuration updated", {
|
logger.debug("Cron job configuration updated", {
|
||||||
name,
|
name,
|
||||||
configuration: JSON.stringify(configuration),
|
configuration: JSON.stringify(configuration),
|
||||||
});
|
});
|
||||||
return updatedConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAllAsync(): Promise<
|
public async getAllAsync(): Promise<
|
||||||
+41
-1
@@ -1,12 +1,52 @@
|
|||||||
// This import has to be the first import in the file so that the agent is overridden before any other modules are imported.
|
// This import has to be the first import in the file so that the agent is overridden before any other modules are imported.
|
||||||
import "./overrides";
|
import "./overrides";
|
||||||
|
|
||||||
import { jobGroup } from "@homarr/cron-jobs";
|
import type { FastifyTRPCPluginOptions } from "@trpc/server/adapters/fastify";
|
||||||
|
import { fastifyTRPCPlugin } from "@trpc/server/adapters/fastify";
|
||||||
|
import fastify from "fastify";
|
||||||
|
|
||||||
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||||
|
import type { JobRouter } from "@homarr/cron-job-api";
|
||||||
|
import { jobRouter } from "@homarr/cron-job-api";
|
||||||
|
import { CRON_JOB_API_KEY_HEADER, CRON_JOB_API_PATH, CRON_JOB_API_PORT } from "@homarr/cron-job-api/constants";
|
||||||
|
import { jobGroup } from "@homarr/cron-jobs";
|
||||||
|
import { db } from "@homarr/db";
|
||||||
|
|
||||||
|
import { JobManager } from "./job-manager";
|
||||||
import { onStartAsync } from "./on-start";
|
import { onStartAsync } from "./on-start";
|
||||||
|
|
||||||
|
const logger = createLogger({ module: "tasksMain" });
|
||||||
|
|
||||||
|
const server = fastify({
|
||||||
|
maxParamLength: 5000,
|
||||||
|
});
|
||||||
|
server.register(fastifyTRPCPlugin, {
|
||||||
|
prefix: CRON_JOB_API_PATH,
|
||||||
|
trpcOptions: {
|
||||||
|
router: jobRouter,
|
||||||
|
createContext: ({ req }) => ({
|
||||||
|
manager: new JobManager(db, jobGroup),
|
||||||
|
apiKey: req.headers[CRON_JOB_API_KEY_HEADER] as string | undefined,
|
||||||
|
}),
|
||||||
|
onError({ path, error }) {
|
||||||
|
logger.error(new ErrorWithMetadata("Error in tasks tRPC handler", { path }, { cause: error }));
|
||||||
|
},
|
||||||
|
} satisfies FastifyTRPCPluginOptions<JobRouter>["trpcOptions"],
|
||||||
|
});
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
await onStartAsync();
|
await onStartAsync();
|
||||||
await jobGroup.initializeAsync();
|
await jobGroup.initializeAsync();
|
||||||
await jobGroup.startAllAsync();
|
await jobGroup.startAllAsync();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.listen({ port: CRON_JOB_API_PORT });
|
||||||
|
logger.info("Tasks web server started successfully", { port: CRON_JOB_API_PORT });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
new ErrorWithMetadata("Failed to start tasks web server", { port: CRON_JOB_API_PORT }, { cause: err }),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
@@ -4,12 +4,10 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
|
||||||
".": "./src/main.ts"
|
|
||||||
},
|
|
||||||
"main": "./src/main.ts",
|
"main": "./src/main.ts",
|
||||||
"types": "./src/main.ts",
|
"types": "./src/main.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build": "esbuild src/main.ts --bundle --platform=node --outfile=wssServer.cjs --external:bcrypt --external:@opentelemetry/api --external:deasync --external:cpu-features --loader:.html=text --loader:.scss=text --loader:.node=text",
|
||||||
"clean": "rm -rf .turbo node_modules",
|
"clean": "rm -rf .turbo node_modules",
|
||||||
"dev": "pnpm with-env tsx ./src/main.ts",
|
"dev": "pnpm with-env tsx ./src/main.ts",
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
@@ -27,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": "catalog:",
|
"dotenv": "^17.3.1",
|
||||||
"tsx": "catalog:",
|
"tsx": "4.20.4",
|
||||||
"ws": "catalog:"
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"@types/ws": "catalog:",
|
"@types/ws": "^8.18.1",
|
||||||
"esbuild": "catalog:",
|
"esbuild": "^0.27.4",
|
||||||
"eslint": "catalog:",
|
"eslint": "^9.39.4",
|
||||||
"prettier": "catalog:",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "catalog:"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24.15.0-trixie AS base
|
FROM node:24.14.0-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.9.0"
|
"better-sqlite3": "^12.6.2"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.33.2",
|
"packageManager": "pnpm@10.32.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.15.0",
|
"node": ">=24.14.0",
|
||||||
"pnpm": ">=10.33.2"
|
"pnpm": ">=10.32.1"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|||||||
+5
-6
@@ -9,16 +9,16 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
better-sqlite3:
|
better-sqlite3:
|
||||||
specifier: ^12.9.0
|
specifier: ^12.6.2
|
||||||
version: 12.9.0
|
version: 12.6.2
|
||||||
|
|
||||||
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.9.0:
|
better-sqlite3@12.6.2:
|
||||||
resolution: {integrity: sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==}
|
resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==}
|
||||||
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,7 +93,6 @@ 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:
|
||||||
@@ -148,7 +147,7 @@ snapshots:
|
|||||||
|
|
||||||
base64-js@1.5.1: {}
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
better-sqlite3@12.9.0:
|
better-sqlite3@12.6.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
bindings: 1.5.0
|
bindings: 1.5.0
|
||||||
prebuild-install: 7.1.3
|
prebuild-install: 7.1.3
|
||||||
|
|||||||
+24
-45
@@ -64,13 +64,6 @@
|
|||||||
</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/>
|
||||||
@@ -104,15 +97,15 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">Deluge</p>
|
<p align="center">Deluge</p>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr><td align="center">
|
<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>
|
</td></tr>
|
||||||
<td align="center">
|
<tr><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/>
|
||||||
@@ -153,15 +146,15 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">Home<br/>Assistant</p>
|
<p align="center">Home<br/>Assistant</p>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr><td align="center">
|
<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>
|
</td></tr>
|
||||||
<td align="center">
|
<tr><td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/immich" target="_blank" rel="noreferrer noopener">
|
<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" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/immich.svg" alt="Immich" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -202,15 +195,15 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">Nextcloud</p>
|
<p align="center">Nextcloud</p>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr><td align="center">
|
<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/>
|
||||||
<p align="center">NPM</p>
|
<p align="center">NPM</p>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
<td align="center">
|
<tr><td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/ntfy" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/ntfy" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/ntfy.svg" alt="ntfy" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/ntfy.svg" alt="ntfy" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -251,15 +244,15 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">Pi-hole</p>
|
<p align="center">Pi-hole</p>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr><td align="center">
|
<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/>
|
||||||
<p align="center">Plex</p>
|
<p align="center">Plex</p>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
<td align="center">
|
<tr><td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/prowlarr" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/prowlarr" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/prowlarr.svg" alt="Prowlarr" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/prowlarr.svg" alt="Prowlarr" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -300,22 +293,15 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">Readarr</p>
|
<p align="center">Readarr</p>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr><td align="center">
|
<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/>
|
||||||
<p align="center">SABnzbd</p>
|
<p align="center">SABnzbd</p>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
<td align="center">
|
<tr><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">
|
<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" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/seerr.svg" alt="Seerr" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -337,20 +323,13 @@
|
|||||||
</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>
|
</td>
|
||||||
<tr><td align="center">
|
<td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/tracearr" target="_blank" rel="noreferrer noopener">
|
<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" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/tracearr.svg" alt="Tracearr" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
@@ -370,8 +349,8 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<p align="center">TrueNAS</p>
|
<p align="center">TrueNAS</p>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
<td align="center">
|
<tr><td align="center">
|
||||||
<a href="https://homarr.dev/docs/integrations/unifi-controller" target="_blank" rel="noreferrer noopener">
|
<a href="https://homarr.dev/docs/integrations/unifi-controller" target="_blank" rel="noreferrer noopener">
|
||||||
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png" alt="Unifi Controller" width="90" height="90" />
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png" alt="Unifi Controller" width="90" height="90" />
|
||||||
<br/>
|
<br/>
|
||||||
|
|||||||
+37
-37
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "1.60.1-beta.1",
|
"version": "1.56.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env CI=true turbo build",
|
"build": "cross-env CI=true turbo build",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"db:migration:sqlite:run": "pnpm -F db migration:sqlite:run",
|
"db:migration:sqlite:run": "pnpm -F db migration:sqlite:run",
|
||||||
"db:push": "pnpm -F db push:sqlite",
|
"db:push": "pnpm -F db push:sqlite",
|
||||||
"db:studio": "pnpm -F db studio",
|
"db:studio": "pnpm -F db studio",
|
||||||
"dev": "pnpm -F nextjs dev",
|
"dev": "turbo dev --parallel",
|
||||||
"docker:dev": "docker compose -f ./development/development.docker-compose.yml up",
|
"docker:dev": "docker compose -f ./development/development.docker-compose.yml up",
|
||||||
"format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache",
|
"format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache",
|
||||||
"format:fix": "turbo format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache",
|
"format:fix": "turbo format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache",
|
||||||
@@ -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": "catalog:",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/commit-analyzer": "catalog:",
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
||||||
"@semantic-release/git": "catalog:",
|
"@semantic-release/git": "^10.0.1",
|
||||||
"@semantic-release/github": "catalog:",
|
"@semantic-release/github": "^12.0.6",
|
||||||
"@semantic-release/npm": "catalog:",
|
"@semantic-release/npm": "^13.1.5",
|
||||||
"@semantic-release/release-notes-generator": "catalog:",
|
"@semantic-release/release-notes-generator": "^14.1.0",
|
||||||
"@testcontainers/redis": "catalog:",
|
"@testcontainers/redis": "^11.13.0",
|
||||||
"@turbo/gen": "catalog:",
|
"@turbo/gen": "^2.8.17",
|
||||||
"@vitejs/plugin-react": "catalog:",
|
"@vitejs/plugin-react": "^5.2.0",
|
||||||
"@vitest/coverage-v8": "catalog:",
|
"@vitest/coverage-v8": "^4.1.0",
|
||||||
"@vitest/ui": "catalog:",
|
"@vitest/ui": "^4.1.0",
|
||||||
"conventional-changelog-conventionalcommits": "catalog:",
|
"conventional-changelog-conventionalcommits": "^9.3.0",
|
||||||
"cross-env": "catalog:",
|
"cross-env": "^10.1.0",
|
||||||
"jsdom": "catalog:",
|
"jsdom": "^28.1.0",
|
||||||
"json5": "catalog:",
|
"json5": "^2.2.3",
|
||||||
"prettier": "catalog:",
|
"prettier": "^3.8.1",
|
||||||
"semantic-release": "catalog:",
|
"semantic-release": "^25.0.3",
|
||||||
"testcontainers": "catalog:",
|
"testcontainers": "^11.13.0",
|
||||||
"turbo": "catalog:",
|
"turbo": "^2.8.17",
|
||||||
"typescript": "catalog:",
|
"typescript": "^5.9.3",
|
||||||
"vite-tsconfig-paths": "catalog:",
|
"vite-tsconfig-paths": "^6.1.1",
|
||||||
"vitest": "catalog:"
|
"vitest": "^4.1.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.33.2",
|
"packageManager": "pnpm@10.32.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.15.0",
|
"node": ">=24.14.0",
|
||||||
"pnpm": ">=10.33.2"
|
"pnpm": ">=10.32.1"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
@@ -79,23 +79,23 @@
|
|||||||
"overrides": {
|
"overrides": {
|
||||||
"@babel/helpers@<7.26.10": ">=7.29.2",
|
"@babel/helpers@<7.26.10": ">=7.29.2",
|
||||||
"@babel/runtime@<7.26.10": ">=7.29.2",
|
"@babel/runtime@<7.26.10": ">=7.29.2",
|
||||||
"axios@>=1.0.0 <1.8.2": ">=1.15.2",
|
"axios@>=1.0.0 <1.8.2": ">=1.13.6",
|
||||||
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.5",
|
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.4",
|
||||||
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.5",
|
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.4",
|
||||||
"esbuild@<=0.24.2": ">=0.28.0",
|
"esbuild@<=0.24.2": ">=0.27.4",
|
||||||
"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.12.15",
|
"hono@<4.6.5": ">=4.12.8",
|
||||||
"linkifyjs@<4.3.2": ">=4.3.2",
|
"linkifyjs@<4.3.2": ">=4.3.2",
|
||||||
"nanoid@>=4.0.0 <5.0.9": ">=5.1.9",
|
"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.24.6",
|
"proxmox-api>undici": "7.19.2",
|
||||||
"react-is": "^19.2.5",
|
"react-is": "^19.2.4",
|
||||||
"rollup@>=4.0.0 <4.22.4": ">=4.60.2",
|
"rollup@>=4.0.0 <4.22.4": ">=4.59.0",
|
||||||
"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.2",
|
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.2",
|
||||||
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.2",
|
"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": ">=8.0.10"
|
"vite@>=5.0.0 <=5.4.18": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"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": "catalog:",
|
"@umami/node": "^0.4.0",
|
||||||
"superjson": "catalog:"
|
"superjson": "2.2.6"
|
||||||
},
|
},
|
||||||
"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": "catalog:",
|
"eslint": "^9.39.4",
|
||||||
"typescript": "catalog:"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+18
-18
@@ -9,8 +9,7 @@
|
|||||||
"./client": "./src/client.ts",
|
"./client": "./src/client.ts",
|
||||||
"./server": "./src/server.ts",
|
"./server": "./src/server.ts",
|
||||||
"./websocket": "./src/websocket.ts",
|
"./websocket": "./src/websocket.ts",
|
||||||
"./shared": "./src/shared.ts",
|
"./shared": "./src/shared.ts"
|
||||||
"./open-api": "./src/open-api.ts"
|
|
||||||
},
|
},
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"types": "./index.ts",
|
"types": "./index.ts",
|
||||||
@@ -25,6 +24,7 @@
|
|||||||
"@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/core": "workspace:^0.1.0",
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
|
"@homarr/cron-job-api": "workspace:^0.1.0",
|
||||||
"@homarr/cron-job-status": "workspace:^0.1.0",
|
"@homarr/cron-job-status": "workspace:^0.1.0",
|
||||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
@@ -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": "catalog:",
|
"@kubernetes/client-node": "^1.4.0",
|
||||||
"@tanstack/react-query": "catalog:",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"@trpc/client": "catalog:",
|
"@trpc/client": "^11.13.4",
|
||||||
"@trpc/react-query": "catalog:",
|
"@trpc/react-query": "^11.13.4",
|
||||||
"@trpc/server": "catalog:",
|
"@trpc/server": "^11.13.4",
|
||||||
"@trpc/tanstack-react-query": "catalog:",
|
"@trpc/tanstack-react-query": "^11.13.4",
|
||||||
"lodash.clonedeep": "catalog:",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"next": "catalog:",
|
"next": "16.1.7",
|
||||||
"react": "catalog:",
|
"react": "19.2.4",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "19.2.4",
|
||||||
"superjson": "catalog:",
|
"superjson": "2.2.6",
|
||||||
"trpc-to-openapi": "catalog:",
|
"trpc-to-openapi": "^3.1.0",
|
||||||
"zod": "catalog:"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"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": "catalog:",
|
"eslint": "^9.39.4",
|
||||||
"prettier": "catalog:",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "catalog:"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
||||||
|
|
||||||
|
import { openApiDocument } from "./open-api";
|
||||||
import type { AppRouter } from "./root";
|
import type { AppRouter } from "./root";
|
||||||
import { appRouter } from "./root";
|
import { appRouter } from "./root";
|
||||||
import { createCallerFactory, createTRPCContext } from "./trpc";
|
import { createCallerFactory, createTRPCContext } from "./trpc";
|
||||||
@@ -29,5 +30,5 @@ type RouterInputs = inferRouterInputs<AppRouter>;
|
|||||||
**/
|
**/
|
||||||
type RouterOutputs = inferRouterOutputs<AppRouter>;
|
type RouterOutputs = inferRouterOutputs<AppRouter>;
|
||||||
|
|
||||||
export { createTRPCContext, appRouter, createCaller };
|
export { createTRPCContext, appRouter, createCaller, openApiDocument };
|
||||||
export type { AppRouter, RouterInputs, RouterOutputs };
|
export type { AppRouter, RouterInputs, RouterOutputs };
|
||||||
@@ -2,21 +2,10 @@ import { generateOpenApiDocument } from "trpc-to-openapi";
|
|||||||
|
|
||||||
import { API_KEY_HEADER_NAME } from "@homarr/auth/api-key";
|
import { API_KEY_HEADER_NAME } from "@homarr/auth/api-key";
|
||||||
|
|
||||||
import { appRouter } from "./router/app";
|
import { appRouter } from "./root";
|
||||||
import { infoRouter } from "./router/info";
|
|
||||||
import { inviteRouter } from "./router/invite";
|
|
||||||
import { userRouter } from "./router/user";
|
|
||||||
import { createTRPCRouter } from "./trpc";
|
|
||||||
|
|
||||||
export const openApiRouter = createTRPCRouter({
|
|
||||||
appRouter,
|
|
||||||
infoRouter,
|
|
||||||
inviteRouter,
|
|
||||||
userRouter,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const openApiDocument = (base: string) =>
|
export const openApiDocument = (base: string) =>
|
||||||
generateOpenApiDocument(openApiRouter, {
|
generateOpenApiDocument(appRouter, {
|
||||||
title: "Homarr API documentation",
|
title: "Homarr API documentation",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
baseUrl: base,
|
baseUrl: base,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { hashPasswordAsync } from "@homarr/auth";
|
import { createSaltAsync, 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,6 +14,7 @@ export const apiKeysRouter = createTRPCRouter({
|
|||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
apiKey: false,
|
apiKey: false,
|
||||||
|
salt: false,
|
||||||
},
|
},
|
||||||
with: {
|
with: {
|
||||||
user: {
|
user: {
|
||||||
@@ -28,12 +29,14 @@ 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);
|
const hashedRandomToken = await hashPasswordAsync(randomToken, salt);
|
||||||
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 {
|
||||||
|
|||||||
@@ -2,62 +2,59 @@ import { observable } from "@trpc/server/observable";
|
|||||||
import z from "zod/v4";
|
import z from "zod/v4";
|
||||||
|
|
||||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
|
import { cronExpressionSchema, jobGroupKeys, jobNameSchema } from "@homarr/cron-job-api";
|
||||||
|
import { cronJobApi } from "@homarr/cron-job-api/client";
|
||||||
import type { TaskStatus } from "@homarr/cron-job-status";
|
import type { TaskStatus } from "@homarr/cron-job-status";
|
||||||
import { createCronJobStatusChannel } from "@homarr/cron-job-status";
|
import { createCronJobStatusChannel } from "@homarr/cron-job-status";
|
||||||
import { jobGroup } from "@homarr/cron-jobs";
|
|
||||||
import { JobManager } from "@homarr/cron-jobs/job-manager";
|
|
||||||
|
|
||||||
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
||||||
|
|
||||||
const jobGroupKeys = jobGroup.getKeys();
|
|
||||||
const jobNameSchema = z.enum(jobGroup.getKeys());
|
|
||||||
|
|
||||||
const logger = createLogger({ module: "cronJobsRouter" });
|
const logger = createLogger({ module: "cronJobsRouter" });
|
||||||
|
|
||||||
export const cronJobsRouter = createTRPCRouter({
|
export const cronJobsRouter = createTRPCRouter({
|
||||||
triggerJob: permissionRequiredProcedure
|
triggerJob: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(jobNameSchema)
|
.input(jobNameSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
await new JobManager(ctx.db, jobGroup).triggerAsync(input);
|
await cronJobApi.trigger.mutate(input);
|
||||||
}),
|
}),
|
||||||
startJob: permissionRequiredProcedure
|
startJob: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(jobNameSchema)
|
.input(jobNameSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
await new JobManager(ctx.db, jobGroup).startAsync(input);
|
await cronJobApi.start.mutate(input);
|
||||||
}),
|
}),
|
||||||
stopJob: permissionRequiredProcedure
|
stopJob: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(jobNameSchema)
|
.input(jobNameSchema)
|
||||||
.mutation(({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
new JobManager(ctx.db, jobGroup).stop(input);
|
await cronJobApi.stop.mutate(input);
|
||||||
}),
|
}),
|
||||||
updateJobInterval: permissionRequiredProcedure
|
updateJobInterval: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: jobNameSchema,
|
name: jobNameSchema,
|
||||||
cron: z.string(),
|
cron: cronExpressionSchema,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
await new JobManager(ctx.db, jobGroup).updateIntervalAsync(input.name, input.cron);
|
await cronJobApi.updateInterval.mutate(input);
|
||||||
}),
|
}),
|
||||||
disableJob: permissionRequiredProcedure
|
disableJob: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(jobNameSchema)
|
.input(jobNameSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
await new JobManager(ctx.db, jobGroup).disableAsync(input);
|
await cronJobApi.disable.mutate(input);
|
||||||
}),
|
}),
|
||||||
enableJob: permissionRequiredProcedure
|
enableJob: permissionRequiredProcedure
|
||||||
.requiresPermission("admin")
|
.requiresPermission("admin")
|
||||||
.input(jobNameSchema)
|
.input(jobNameSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
await new JobManager(ctx.db, jobGroup).enableAsync(input);
|
await cronJobApi.enable.mutate(input);
|
||||||
}),
|
}),
|
||||||
getJobs: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => {
|
getJobs: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
|
||||||
return await new JobManager(ctx.db, jobGroup).getAllAsync();
|
return await cronJobApi.getAll.query();
|
||||||
}),
|
}),
|
||||||
subscribeToStatusUpdates: permissionRequiredProcedure.requiresPermission("admin").subscription(() => {
|
subscribeToStatusUpdates: permissionRequiredProcedure.requiresPermission("admin").subscription(() => {
|
||||||
return observable<TaskStatus>((emit) => {
|
return observable<TaskStatus>((emit) => {
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ interface DockerContainer {
|
|||||||
id: string;
|
id: string;
|
||||||
state: ContainerState;
|
state: ContainerState;
|
||||||
image: string;
|
image: string;
|
||||||
ports: Port[] | undefined;
|
ports: Port[];
|
||||||
iconUrl: string | null;
|
iconUrl: string | null;
|
||||||
cpuUsage: number;
|
cpuUsage: number;
|
||||||
memoryUsage: number;
|
memoryUsage: number;
|
||||||
|
|||||||
@@ -208,7 +208,6 @@ 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>,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { MySqlRawQueryResult } from "drizzle-orm/mysql2";
|
|||||||
import type { QueryResult } from "pg";
|
import type { QueryResult } from "pg";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { comparePasswordsAsync, hashPasswordAsync } from "@homarr/auth";
|
import { createSaltAsync, 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";
|
||||||
@@ -11,8 +11,8 @@ 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 type { SupportedAuthProvider } from "@homarr/definitions";
|
|
||||||
import { credentialsAdminGroup } from "@homarr/definitions";
|
import { credentialsAdminGroup } from "@homarr/definitions";
|
||||||
|
import type { SupportedAuthProvider } 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 {
|
||||||
@@ -92,13 +92,15 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, "credentials", input.username);
|
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, "credentials", input.username);
|
||||||
|
|
||||||
const hashedPassword = await hashPasswordAsync(input.password);
|
const salt = await createSaltAsync();
|
||||||
|
const hashedPassword = await hashPasswordAsync(input.password, salt);
|
||||||
|
|
||||||
const userId = createId();
|
const userId = createId();
|
||||||
const user = {
|
const user = {
|
||||||
id: userId,
|
id: userId,
|
||||||
name: input.username,
|
name: input.username,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
|
salt,
|
||||||
};
|
};
|
||||||
|
|
||||||
await handleTransactionsAsync(ctx.db, {
|
await handleTransactionsAsync(ctx.db, {
|
||||||
@@ -143,7 +145,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
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(convertIntersectionToZodObject(userCreateSchema))
|
.input(userCreateSchema)
|
||||||
.output(z.void())
|
.output(z.void())
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
throwIfCredentialsDisabled();
|
throwIfCredentialsDisabled();
|
||||||
@@ -374,7 +376,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(convertIntersectionToZodObject(userChangePasswordApiSchema))
|
.input(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 }) => {
|
||||||
@@ -391,6 +393,7 @@ 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),
|
||||||
@@ -420,7 +423,8 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isPreviousPasswordRequired) {
|
if (isPreviousPasswordRequired) {
|
||||||
const isValid = await comparePasswordsAsync(input.previousPassword, dbUser.password ?? "");
|
const previousPasswordHash = await hashPasswordAsync(input.previousPassword, dbUser.salt ?? "");
|
||||||
|
const isValid = previousPasswordHash === dbUser.password;
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -430,7 +434,8 @@ export const userRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashedPassword = await hashPasswordAsync(input.password);
|
const salt = await createSaltAsync();
|
||||||
|
const hashedPassword = await hashPasswordAsync(input.password, salt);
|
||||||
await ctx.db
|
await ctx.db
|
||||||
.update(users)
|
.update(users)
|
||||||
.set({
|
.set({
|
||||||
@@ -578,7 +583,8 @@ 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 hashedPassword = await hashPasswordAsync(input.password);
|
const salt = await createSaltAsync();
|
||||||
|
const hashedPassword = await hashPasswordAsync(input.password, salt);
|
||||||
|
|
||||||
const userId = createId();
|
const userId = createId();
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
@@ -586,6 +592,7 @@ 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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -3,7 +3,6 @@ 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)),
|
immich: lazy(() => import("./immich").then((mod) => mod.immichRouter)),
|
||||||
notebook: lazy(() => import("./notebook").then((mod) => mod.notebookRouter)),
|
notebook: lazy(() => import("./notebook").then((mod) => mod.notebookRouter)),
|
||||||
@@ -27,7 +26,5 @@ 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)),
|
tracearr: lazy(() => import("./tracearr").then((mod) => mod.tracearrRouter)),
|
||||||
speedtestTracker: lazy(() => import("./speedtest-tracker").then((mod) => mod.speedtestTrackerRouter)),
|
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
|
||||||
import SuperJSON from "superjson";
|
import SuperJSON from "superjson";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
@@ -6,59 +5,45 @@ 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.input(feedsInput).query(async ({ ctx, input }) => {
|
getFeeds: publicProcedure
|
||||||
const urls = (await canAccessAllFeedsAsync(ctx.db, ctx.session))
|
.input(
|
||||||
? input.urls
|
z.object({
|
||||||
: await restrictUrlsAsync(ctx.db, input.urls);
|
urls: z.array(z.string()).max(100),
|
||||||
|
maximumAmountPosts: z.number(),
|
||||||
const rssFeeds = await Promise.all(
|
|
||||||
urls.map(async (url) => {
|
|
||||||
const innerHandler = rssFeedsRequestHandler.handler({
|
|
||||||
url,
|
|
||||||
count: input.maximumAmountPosts,
|
|
||||||
});
|
|
||||||
return await innerHandler.getCachedOrUpdatedDataAsync({
|
|
||||||
forceUpdate: false,
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const urls = (await canAccessAllFeedsAsync(ctx.db, ctx.session))
|
||||||
|
? input.urls
|
||||||
|
: await restrictUrlsAsync(ctx.db, input.urls);
|
||||||
|
|
||||||
return rssFeeds
|
const rssFeeds = await Promise.all(
|
||||||
.flatMap((rssFeed) => rssFeed.data.entries)
|
urls.map(async (url) => {
|
||||||
.slice(0, input.maximumAmountPosts)
|
const innerHandler = rssFeedsRequestHandler.handler({
|
||||||
.sort((entryA, entryB) => {
|
url,
|
||||||
return entryA.published && entryB.published
|
count: input.maximumAmountPosts,
|
||||||
? new Date(entryB.published).getTime() - new Date(entryA.published).getTime()
|
});
|
||||||
: 0;
|
return await innerHandler.getCachedOrUpdatedDataAsync({
|
||||||
});
|
forceUpdate: false,
|
||||||
}),
|
});
|
||||||
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) => {
|
return rssFeeds
|
||||||
const innerHandler = rssFeedsRequestHandler.handler({ url, count: input.maximumAmountPosts });
|
.flatMap((rssFeed) => rssFeed.data.entries)
|
||||||
return innerHandler.subscribe((data) => {
|
.slice(0, input.maximumAmountPosts)
|
||||||
emit.next({ url, entries: data.entries });
|
.sort((entryA, entryB) => {
|
||||||
|
return entryA.published && entryB.published
|
||||||
|
? new Date(entryB.published).getTime() - new Date(entryA.published).getTime()
|
||||||
|
: 0;
|
||||||
});
|
});
|
||||||
});
|
}),
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function canAccessAllFeedsAsync(db: Database, session: Session | null) {
|
export async function canAccessAllFeedsAsync(db: Database, session: Session | null) {
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
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());
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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;
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
@@ -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 { comparePasswordsAsync } from "../security";
|
import { hashPasswordAsync } from "../security";
|
||||||
import { createSessionAsync } from "../server";
|
import { createSessionAsync } from "../server";
|
||||||
|
|
||||||
const logger = createLogger({ module: "apiKeyAuth" });
|
const logger = createLogger({ module: "apiKeyAuth" });
|
||||||
@@ -41,6 +41,7 @@ export const getSessionFromApiKeyAsync = async (
|
|||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
apiKey: true,
|
apiKey: true,
|
||||||
|
salt: true,
|
||||||
},
|
},
|
||||||
with: {
|
with: {
|
||||||
user: {
|
user: {
|
||||||
@@ -59,9 +60,9 @@ export const getSessionFromApiKeyAsync = async (
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValid = await comparePasswordsAsync(apiKey, apiKeyFromDb.apiKey);
|
const hashedApiKey = await hashPasswordAsync(apiKey, apiKeyFromDb.salt);
|
||||||
|
|
||||||
if (!isValid) {
|
if (apiKeyFromDb.apiKey !== hashedApiKey) {
|
||||||
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 { hashPasswordAsync } from "../../security";
|
import { createSaltAsync, 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,9 +118,11 @@ 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),
|
apiKey: await hashPasswordAsync(options.token, salt),
|
||||||
|
salt,
|
||||||
userId: defaultUserId,
|
userId: defaultUserId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-15
@@ -24,30 +24,30 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "catalog:",
|
"@auth/core": "^0.41.1",
|
||||||
"@auth/drizzle-adapter": "catalog:",
|
"@auth/drizzle-adapter": "^1.11.1",
|
||||||
"@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": "catalog:",
|
"bcrypt": "^6.0.0",
|
||||||
"cookies": "catalog:",
|
"cookies": "^0.9.1",
|
||||||
"ldapts": "catalog:",
|
"ldapts": "8.1.7",
|
||||||
"next": "catalog:",
|
"next": "16.1.7",
|
||||||
"next-auth": "catalog:",
|
"next-auth": "5.0.0-beta.30",
|
||||||
"react": "catalog:",
|
"react": "19.2.4",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "19.2.4",
|
||||||
"zod": "catalog:"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"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": "catalog:",
|
"@types/bcrypt": "6.0.0",
|
||||||
"@types/cookies": "catalog:",
|
"@types/cookies": "0.9.2",
|
||||||
"eslint": "catalog:",
|
"eslint": "^9.39.4",
|
||||||
"prettier": "catalog:",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "catalog:"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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";
|
||||||
@@ -6,8 +7,6 @@ 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 (
|
||||||
@@ -24,7 +23,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 comparePasswordsAsync(credentials.password, user.password);
|
const isValidPassword = await bcrypt.compare(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 { hashPasswordAsync } from "../../security";
|
import { createSaltAsync, hashPasswordAsync } from "../../security";
|
||||||
import { authorizeWithBasicCredentialsAsync } from "../credentials/authorization/basic-authorization";
|
import { authorizeWithBasicCredentialsAsync } from "../credentials/authorization/basic-authorization";
|
||||||
|
|
||||||
const defaultUserId = createId();
|
const defaultUserId = createId();
|
||||||
@@ -13,10 +13,12 @@ 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",
|
||||||
password: await hashPasswordAsync("test"),
|
salt,
|
||||||
|
password: await hashPasswordAsync("test", salt),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -32,10 +34,12 @@ 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",
|
||||||
password: await hashPasswordAsync("test"),
|
salt,
|
||||||
|
password: await hashPasswordAsync("test", salt),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -51,10 +55,12 @@ 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",
|
||||||
password: await hashPasswordAsync("test"),
|
salt,
|
||||||
|
password: await hashPasswordAsync("test", salt),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
|
|
||||||
export const hashPasswordAsync = async (password: string) => {
|
export const createSaltAsync = async () => {
|
||||||
return bcrypt.hash(password, 10);
|
return bcrypt.genSalt(10);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export const hashPasswordAsync = async (password: string, salt: string) => {
|
||||||
* Compares a plaintext password with a hashed password and returns whether they match.
|
return bcrypt.hash(password, salt);
|
||||||
* @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,11 +1,25 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { comparePasswordsAsync, hashPasswordAsync } from "../security";
|
import { createSaltAsync, 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 result = await hashPasswordAsync(password);
|
const salt = await createSaltAsync();
|
||||||
|
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);
|
||||||
@@ -13,44 +27,21 @@ 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);
|
const result1 = await hashPasswordAsync(password, salt);
|
||||||
const result2 = await hashPasswordAsync(password2);
|
const result2 = await hashPasswordAsync(password2, salt);
|
||||||
|
|
||||||
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);
|
const result1 = await hashPasswordAsync(password, salt1);
|
||||||
const result2 = await hashPasswordAsync(password);
|
const result2 = await hashPasswordAsync(password, salt2);
|
||||||
|
|
||||||
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": "catalog:",
|
"react": "19.2.4",
|
||||||
"react-dom": "catalog:"
|
"react-dom": "19.2.4"
|
||||||
},
|
},
|
||||||
"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": "catalog:",
|
"eslint": "^9.39.4",
|
||||||
"typescript": "catalog:"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,19 +25,19 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@drizzle-team/brocli": "catalog:",
|
"@drizzle-team/brocli": "^0.11.0",
|
||||||
"@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": "catalog:"
|
"dotenv": "^17.3.1"
|
||||||
},
|
},
|
||||||
"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": "catalog:",
|
"esbuild": "^0.27.4",
|
||||||
"eslint": "catalog:",
|
"eslint": "^9.39.4",
|
||||||
"typescript": "catalog:"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { command, string } from "@drizzle-team/brocli";
|
import { command, string } from "@drizzle-team/brocli";
|
||||||
|
|
||||||
import { hashPasswordAsync } from "@homarr/auth";
|
import { createSaltAsync, 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,8 +67,9 @@ 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);
|
const hashedPassword = await hashPasswordAsync(password, salt);
|
||||||
|
|
||||||
const userId = createId();
|
const userId = createId();
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
@@ -76,6 +77,7 @@ 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?.password) {
|
if (!user?.salt) {
|
||||||
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),
|
password: await hashPasswordAsync(newPassword, user.salt),
|
||||||
})
|
})
|
||||||
.where(eq(users.id, user.id));
|
.where(eq(users.id, user.id));
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const usersUpdatePassword = command({
|
|||||||
where: options.id ? eq(users.id, options.id) : eq(users.name, options.username!),
|
where: options.id ? eq(users.id, options.id) : eq(users.name, options.username!),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user?.password) {
|
if (!user?.salt) {
|
||||||
console.error("User not found or has no credentials record");
|
console.error("User not found or has no credentials record");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ export const usersUpdatePassword = command({
|
|||||||
await db
|
await db
|
||||||
.update(users)
|
.update(users)
|
||||||
.set({
|
.set({
|
||||||
password: await hashPasswordAsync(options.password),
|
password: await hashPasswordAsync(options.password, user.salt),
|
||||||
})
|
})
|
||||||
.where(eq(users.id, user.id));
|
.where(eq(users.id, user.id));
|
||||||
|
|
||||||
|
|||||||
@@ -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": "catalog:",
|
"@paralleldrive/cuid2": "^3.3.0",
|
||||||
"cookie": "catalog:",
|
"cookie": "1.1.1",
|
||||||
"dayjs": "catalog:",
|
"dayjs": "^1.11.20",
|
||||||
"next": "catalog:",
|
"next": "16.1.7",
|
||||||
"octokit": "catalog:",
|
"octokit": "^5.0.5",
|
||||||
"react": "catalog:",
|
"react": "19.2.4",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "19.2.4",
|
||||||
"undici": "catalog:",
|
"undici": "7.18.2",
|
||||||
"zod": "catalog:",
|
"zod": "^4.2.1",
|
||||||
"zod-validation-error": "catalog:"
|
"zod-validation-error": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
"eslint": "catalog:",
|
"eslint": "^9.39.4",
|
||||||
"typescript": "catalog:"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,10 +41,6 @@ export class Stopwatch {
|
|||||||
return dayjs().millisecond(this.startTime).fromNow(true);
|
return dayjs().millisecond(this.startTime).fromNow(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
getElapsedInFormattedMilliseconds() {
|
|
||||||
return `${this.getElapsedInMilliseconds().toFixed(0)}ms`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getElapsedInMilliseconds() {
|
getElapsedInMilliseconds() {
|
||||||
return performance.now() - this.startTime;
|
return performance.now() - this.startTime;
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-14
@@ -37,24 +37,24 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@t3-oss/env-nextjs": "catalog:",
|
"@t3-oss/env-nextjs": "^0.13.10",
|
||||||
"better-sqlite3": "catalog:",
|
"better-sqlite3": "^12.6.2",
|
||||||
"dns-caching": "catalog:",
|
"dns-caching": "^0.2.9",
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "^0.45.1",
|
||||||
"ioredis": "catalog:",
|
"ioredis": "5.10.0",
|
||||||
"mysql2": "catalog:",
|
"mysql2": "3.20.0",
|
||||||
"pg": "catalog:",
|
"pg": "^8.20.0",
|
||||||
"superjson": "catalog:",
|
"superjson": "2.2.6",
|
||||||
"winston": "catalog:",
|
"winston": "3.19.0",
|
||||||
"zod": "catalog:"
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"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": "catalog:",
|
"@types/better-sqlite3": "7.6.13",
|
||||||
"@types/pg": "catalog:",
|
"@types/pg": "^8.18.0",
|
||||||
"eslint": "catalog:",
|
"eslint": "^9.39.4",
|
||||||
"typescript": "catalog:"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
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 =
|
const metadataObject = metadata instanceof ErrorWithMetadata ? metadata.metadata : metadata;
|
||||||
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))
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import baseConfig from "@homarr/eslint-config/base";
|
||||||
|
|
||||||
|
/** @type {import('typescript-eslint').Config} */
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: [],
|
||||||
|
},
|
||||||
|
...baseConfig,
|
||||||
|
];
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "@homarr/cron-job-api",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./env": "./src/env.ts",
|
||||||
|
"./constants": "./src/constants.ts",
|
||||||
|
"./client": "./src/client.ts"
|
||||||
|
},
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rm -rf .turbo node_modules",
|
||||||
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
|
"lint": "eslint",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"prettier": "@homarr/prettier-config",
|
||||||
|
"dependencies": {
|
||||||
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
|
"@homarr/core": "workspace:^0.1.0",
|
||||||
|
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||||
|
"@tanstack/react-query": "^5.90.21",
|
||||||
|
"@trpc/client": "^11.13.4",
|
||||||
|
"@trpc/server": "^11.13.4",
|
||||||
|
"@trpc/tanstack-react-query": "^11.13.4",
|
||||||
|
"node-cron": "^4.2.1",
|
||||||
|
"react": "19.2.4",
|
||||||
|
"zod": "^4.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
|
"@types/node-cron": "^3.0.11",
|
||||||
|
"@types/react": "19.2.14",
|
||||||
|
"eslint": "^9.39.4",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { createTRPCClient, httpLink } from "@trpc/client";
|
||||||
|
|
||||||
|
import type { JobRouter } from ".";
|
||||||
|
import { CRON_JOB_API_KEY_HEADER, CRON_JOB_API_PATH, CRON_JOB_API_PORT } from "./constants";
|
||||||
|
import { env } from "./env";
|
||||||
|
|
||||||
|
export const cronJobApi = createTRPCClient<JobRouter>({
|
||||||
|
links: [
|
||||||
|
httpLink({
|
||||||
|
url: `${getBaseUrl()}${CRON_JOB_API_PATH}`,
|
||||||
|
headers: {
|
||||||
|
[CRON_JOB_API_KEY_HEADER]: env.CRON_JOB_API_KEY,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
function getBaseUrl() {
|
||||||
|
return `http://localhost:${CRON_JOB_API_PORT}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export const CRON_JOB_API_PORT = 3002;
|
||||||
|
export const CRON_JOB_API_PATH = "/trpc";
|
||||||
|
export const CRON_JOB_API_KEY_HEADER = "cron-job-api-key";
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
import { env as commonEnv } from "@homarr/common/env";
|
||||||
|
import { createEnv } from "@homarr/core/infrastructure/env";
|
||||||
|
|
||||||
|
export const env = createEnv({
|
||||||
|
server: {
|
||||||
|
CRON_JOB_API_KEY: commonEnv.NODE_ENV === "development" ? z.string().default("test") : z.string(),
|
||||||
|
},
|
||||||
|
experimental__runtimeEnv: process.env,
|
||||||
|
});
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { initTRPC, TRPCError } from "@trpc/server";
|
||||||
|
import { validate } from "node-cron";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
import type { JobGroupKeys } from "@homarr/cron-jobs";
|
||||||
|
import { jobGroup } from "@homarr/cron-jobs";
|
||||||
|
|
||||||
|
import { env } from "./env";
|
||||||
|
|
||||||
|
export const jobGroupKeys = jobGroup.getKeys();
|
||||||
|
export const jobNameSchema = z.enum(jobGroup.getKeys());
|
||||||
|
|
||||||
|
export interface IJobManager {
|
||||||
|
startAsync(name: JobGroupKeys): Promise<void>;
|
||||||
|
triggerAsync(name: JobGroupKeys): Promise<void>;
|
||||||
|
stopAsync(name: JobGroupKeys): Promise<void>;
|
||||||
|
updateIntervalAsync(name: JobGroupKeys, cron: string): Promise<void>;
|
||||||
|
disableAsync(name: JobGroupKeys): Promise<void>;
|
||||||
|
enableAsync(name: JobGroupKeys): Promise<void>;
|
||||||
|
getAllAsync(): Promise<
|
||||||
|
{
|
||||||
|
name: JobGroupKeys;
|
||||||
|
cron: string;
|
||||||
|
preventManualExecution: boolean;
|
||||||
|
preventCustomInterval: boolean;
|
||||||
|
isEnabled: boolean;
|
||||||
|
}[]
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = initTRPC
|
||||||
|
.context<{
|
||||||
|
manager: IJobManager;
|
||||||
|
apiKey?: string;
|
||||||
|
}>()
|
||||||
|
.create();
|
||||||
|
|
||||||
|
const createTrpcRouter = t.router;
|
||||||
|
const apiKeyProcedure = t.procedure.use(({ ctx, next }) => {
|
||||||
|
if (ctx.apiKey !== env.CRON_JOB_API_KEY) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Missing or invalid API key",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
...ctx,
|
||||||
|
apiKey: undefined, // Clear the API key after checking
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const cronExpressionSchema = z.string().refine((expression) => validate(expression), {
|
||||||
|
error: "Invalid cron expression",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const jobRouter = createTrpcRouter({
|
||||||
|
start: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
|
||||||
|
await ctx.manager.startAsync(input);
|
||||||
|
}),
|
||||||
|
trigger: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
|
||||||
|
await ctx.manager.triggerAsync(input);
|
||||||
|
}),
|
||||||
|
stop: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
|
||||||
|
await ctx.manager.stopAsync(input);
|
||||||
|
}),
|
||||||
|
updateInterval: apiKeyProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
name: jobNameSchema,
|
||||||
|
cron: cronExpressionSchema,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
await ctx.manager.updateIntervalAsync(input.name, input.cron);
|
||||||
|
}),
|
||||||
|
disable: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
|
||||||
|
await ctx.manager.disableAsync(input);
|
||||||
|
}),
|
||||||
|
enable: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
|
||||||
|
await ctx.manager.enableAsync(input);
|
||||||
|
}),
|
||||||
|
getAll: apiKeyProcedure.query(({ ctx }) => {
|
||||||
|
return ctx.manager.getAllAsync();
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type JobRouter = typeof jobRouter;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "@homarr/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||||
|
},
|
||||||
|
"include": ["*.ts", "src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -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": "catalog:",
|
"eslint": "^9.39.4",
|
||||||
"typescript": "catalog:"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,13 +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",
|
||||||
"croner": "catalog:"
|
"node-cron": "^4.2.1"
|
||||||
},
|
},
|
||||||
"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": "catalog:",
|
"@types/node-cron": "^3.0.11",
|
||||||
"typescript": "catalog:"
|
"eslint": "^9.39.4",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Cron } from "croner";
|
import { createTask, validate } from "node-cron";
|
||||||
|
|
||||||
import { Stopwatch } from "@homarr/common";
|
import { Stopwatch } from "@homarr/common";
|
||||||
import type { MaybePromise } from "@homarr/common/types";
|
import type { MaybePromise } from "@homarr/common/types";
|
||||||
@@ -83,15 +83,10 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
|
|||||||
? defaultCronExpression
|
? defaultCronExpression
|
||||||
: (configuration?.cronExpression ?? defaultCronExpression);
|
: (configuration?.cronExpression ?? defaultCronExpression);
|
||||||
|
|
||||||
const scheduledTask = new Cron(
|
const scheduledTask = createTask(cronExpression, () => void catchingCallbackAsync(), {
|
||||||
cronExpression,
|
name,
|
||||||
{
|
timezone: creatorOptions.timezone,
|
||||||
name,
|
});
|
||||||
timezone: creatorOptions.timezone,
|
|
||||||
paused: true,
|
|
||||||
},
|
|
||||||
() => void catchingCallbackAsync(),
|
|
||||||
);
|
|
||||||
creatorOptions.logger.logDebug("The scheduled task for cron job was created", {
|
creatorOptions.logger.logDebug("The scheduled task for cron job was created", {
|
||||||
name,
|
name,
|
||||||
cronExpression: defaultCronExpression,
|
cronExpression: defaultCronExpression,
|
||||||
@@ -121,7 +116,6 @@ const createCallback = <TAllowedNames extends string, TName extends TAllowedName
|
|||||||
},
|
},
|
||||||
preventManualExecution: options.preventManualExecution ?? false,
|
preventManualExecution: options.preventManualExecution ?? false,
|
||||||
preventCustomInterval: options.preventCustomInterval ?? false,
|
preventCustomInterval: options.preventCustomInterval ?? false,
|
||||||
timezone: creatorOptions.timezone,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -138,6 +132,13 @@ export const createCronJobCreator = <TAllowedNames extends string = string>(
|
|||||||
defaultCronExpression: TExpression,
|
defaultCronExpression: TExpression,
|
||||||
options: CreateCronJobOptions = { runOnStart: false },
|
options: CreateCronJobOptions = { runOnStart: false },
|
||||||
) => {
|
) => {
|
||||||
|
creatorOptions.logger.logDebug("Validating cron expression for cron job", {
|
||||||
|
name,
|
||||||
|
cronExpression: defaultCronExpression,
|
||||||
|
});
|
||||||
|
if (!validate(defaultCronExpression)) {
|
||||||
|
throw new Error(`Invalid cron expression '${defaultCronExpression}' for job '${name}'`);
|
||||||
|
}
|
||||||
creatorOptions.logger.logDebug("Cron job expression for cron job is valid", {
|
creatorOptions.logger.logDebug("Cron job expression for cron job is valid", {
|
||||||
name,
|
name,
|
||||||
cronExpression: defaultCronExpression,
|
cronExpression: defaultCronExpression,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Cron } from "croner";
|
import type { ScheduledTask } from "node-cron";
|
||||||
|
|
||||||
import { objectEntries, objectKeys } from "@homarr/common";
|
import { objectEntries, objectKeys } from "@homarr/common";
|
||||||
import { db } from "@homarr/db";
|
import { db } from "@homarr/db";
|
||||||
@@ -34,7 +34,7 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasks = new Map<string, Cron>();
|
const tasks = new Map<string, ScheduledTask>();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initializeAsync: async () => {
|
initializeAsync: async () => {
|
||||||
@@ -62,7 +62,7 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
|||||||
name: job.name,
|
name: job.name,
|
||||||
});
|
});
|
||||||
await job.onStartAsync();
|
await job.onStartAsync();
|
||||||
tasks.get(name as string)?.resume();
|
await tasks.get(name as string)?.start();
|
||||||
},
|
},
|
||||||
startAllAsync: async () => {
|
startAllAsync: async () => {
|
||||||
for (const job of jobRegistry.values()) {
|
for (const job of jobRegistry.values()) {
|
||||||
@@ -74,7 +74,7 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
|||||||
name: job.name,
|
name: job.name,
|
||||||
});
|
});
|
||||||
await job.onStartAsync();
|
await job.onStartAsync();
|
||||||
tasks.get(job.name)?.resume();
|
await tasks.get(job.name)?.start();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
runManuallyAsync: async (name: keyof TJobs) => {
|
runManuallyAsync: async (name: keyof TJobs) => {
|
||||||
@@ -87,23 +87,23 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
|||||||
options.logger.logInfo("Running schedule cron job manually.", {
|
options.logger.logInfo("Running schedule cron job manually.", {
|
||||||
name: job.name,
|
name: job.name,
|
||||||
});
|
});
|
||||||
await tasks.get(name as string)?.trigger();
|
await tasks.get(name as string)?.execute();
|
||||||
},
|
},
|
||||||
stop: (name: keyof TJobs) => {
|
stopAsync: async (name: keyof TJobs) => {
|
||||||
const job = jobRegistry.get(name as string);
|
const job = jobRegistry.get(name as string);
|
||||||
if (!job) return;
|
if (!job) return;
|
||||||
|
|
||||||
options.logger.logInfo("Stopping schedule of cron job.", {
|
options.logger.logInfo("Stopping schedule of cron job.", {
|
||||||
name: job.name,
|
name: job.name,
|
||||||
});
|
});
|
||||||
tasks.get(name as string)?.pause();
|
await tasks.get(name as string)?.stop();
|
||||||
},
|
},
|
||||||
stopAll: () => {
|
stopAllAsync: async () => {
|
||||||
for (const job of jobRegistry.values()) {
|
for (const job of jobRegistry.values()) {
|
||||||
options.logger.logInfo("Stopping schedule of cron job.", {
|
options.logger.logInfo("Stopping schedule of cron job.", {
|
||||||
name: job.name,
|
name: job.name,
|
||||||
});
|
});
|
||||||
tasks.get(job.name)?.pause();
|
await tasks.get(job.name)?.stop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getJobRegistry() {
|
getJobRegistry() {
|
||||||
@@ -112,8 +112,8 @@ export const createJobGroupCreator = <TAllowedNames extends string = string>(
|
|||||||
getTask(name: keyof TJobs) {
|
getTask(name: keyof TJobs) {
|
||||||
return tasks.get(name as string) ?? null;
|
return tasks.get(name as string) ?? null;
|
||||||
},
|
},
|
||||||
setTask(name: keyof TJobs, cron: Cron) {
|
setTask(name: keyof TJobs, task: ScheduledTask) {
|
||||||
tasks.set(name as string, cron);
|
tasks.set(name as string, task);
|
||||||
},
|
},
|
||||||
getKeys() {
|
getKeys() {
|
||||||
return objectKeys(jobs);
|
return objectKeys(jobs);
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts",
|
".": "./index.ts"
|
||||||
"./job-manager": "./src/job-manager.ts"
|
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
@@ -39,14 +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": "catalog:",
|
"semver-parser": "^4.1.8"
|
||||||
"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": "catalog:",
|
"eslint": "^9.39.4",
|
||||||
"typescript": "catalog:"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,6 @@ 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 { 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";
|
||||||
@@ -27,44 +26,32 @@ import { updateCheckerJob } from "./jobs/update-checker";
|
|||||||
import { weatherJob } from "./jobs/weather";
|
import { weatherJob } from "./jobs/weather";
|
||||||
import { createCronJobGroup } from "./lib";
|
import { createCronJobGroup } from "./lib";
|
||||||
|
|
||||||
const getJobGroup = () => {
|
export const jobGroup = createCronJobGroup({
|
||||||
return createCronJobGroup({
|
analytics: analyticsJob,
|
||||||
analytics: analyticsJob,
|
iconsUpdater: iconsUpdaterJob,
|
||||||
iconsUpdater: iconsUpdaterJob,
|
ping: pingJob,
|
||||||
ping: pingJob,
|
smartHomeEntityState: smartHomeEntityStateJob,
|
||||||
smartHomeEntityState: smartHomeEntityStateJob,
|
mediaServer: mediaServerJob,
|
||||||
mediaServer: mediaServerJob,
|
mediaOrganizer: mediaOrganizerJob,
|
||||||
mediaOrganizer: mediaOrganizerJob,
|
downloads: downloadsJob,
|
||||||
downloads: downloadsJob,
|
dnsHole: dnsHoleJob,
|
||||||
dnsHole: dnsHoleJob,
|
mediaRequestStats: mediaRequestStatsJob,
|
||||||
mediaRequestStats: mediaRequestStatsJob,
|
mediaRequestList: mediaRequestListJob,
|
||||||
mediaRequestList: mediaRequestListJob,
|
rssFeeds: rssFeedsJob,
|
||||||
rssFeeds: rssFeedsJob,
|
indexerManager: indexerManagerJob,
|
||||||
indexerManager: indexerManagerJob,
|
healthMonitoring: healthMonitoringJob,
|
||||||
healthMonitoring: healthMonitoringJob,
|
updateChecker: updateCheckerJob,
|
||||||
updateChecker: updateCheckerJob,
|
mediaTranscoding: mediaTranscodingJob,
|
||||||
mediaTranscoding: mediaTranscodingJob,
|
minecraftServerStatus: minecraftServerStatusJob,
|
||||||
minecraftServerStatus: minecraftServerStatusJob,
|
dockerContainers: dockerContainersJob,
|
||||||
dockerContainers: dockerContainersJob,
|
networkController: networkControllerJob,
|
||||||
networkController: networkControllerJob,
|
firewallCpu: firewallCpuJob,
|
||||||
firewallCpu: firewallCpuJob,
|
firewallMemory: firewallMemoryJob,
|
||||||
firewallMemory: firewallMemoryJob,
|
firewallVersion: firewallVersionJob,
|
||||||
firewallVersion: firewallVersionJob,
|
firewallInterfaces: firewallInterfacesJob,
|
||||||
firewallInterfaces: firewallInterfacesJob,
|
refreshNotifications: refreshNotificationsJob,
|
||||||
refreshNotifications: refreshNotificationsJob,
|
weather: weatherJob,
|
||||||
weather: weatherJob,
|
tracearr: tracearrJob,
|
||||||
timetable: timetableJob,
|
});
|
||||||
tracearr: tracearrJob,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add global type augmentation for homarr
|
|
||||||
declare global {
|
|
||||||
var cronJobs: ReturnType<typeof getJobGroup> | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
global.cronJobs ??= getJobGroup();
|
|
||||||
|
|
||||||
export const jobGroup = global.cronJobs;
|
|
||||||
|
|
||||||
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
|
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
import type { MySqlRawQueryResult } from "drizzle-orm/mysql2";
|
|
||||||
import type { QueryResult } from "pg";
|
|
||||||
|
|
||||||
import { createId, splitToNChunks, Stopwatch } from "@homarr/common";
|
import { createId, splitToNChunks, Stopwatch } from "@homarr/common";
|
||||||
import { env } from "@homarr/common/env";
|
import { env } from "@homarr/common/env";
|
||||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||||
import { EVERY_WEEK } from "@homarr/cron-jobs-core/expressions";
|
import { EVERY_WEEK } from "@homarr/cron-jobs-core/expressions";
|
||||||
import type { InferInsertModel } from "@homarr/db";
|
import type { InferInsertModel } from "@homarr/db";
|
||||||
import { and, db, eq, handleTransactionsAsync, inArray, notInArray, or } from "@homarr/db";
|
import { db, handleTransactionsAsync, inArray, sql } from "@homarr/db";
|
||||||
import { iconRepositories, icons } from "@homarr/db/schema";
|
import { iconRepositories, icons } from "@homarr/db/schema";
|
||||||
import { fetchIconsAsync } from "@homarr/icons";
|
import { fetchIconsAsync } from "@homarr/icons";
|
||||||
|
|
||||||
@@ -29,7 +26,7 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
|||||||
logger.info("Fetched icons from repositories", {
|
logger.info("Fetched icons from repositories", {
|
||||||
repositoryCount: repositoryIconGroups.length,
|
repositoryCount: repositoryIconGroups.length,
|
||||||
iconCount: countIcons,
|
iconCount: countIcons,
|
||||||
duration: stopWatch.getElapsedInFormattedMilliseconds(),
|
duration: stopWatch.getElapsedInHumanWords(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const databaseIconRepositories = await db.query.iconRepositories.findMany({
|
const databaseIconRepositories = await db.query.iconRepositories.findMany({
|
||||||
@@ -38,7 +35,7 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const skippedChecksums = new Map<string, string[]>();
|
const skippedChecksums: `${string}.${string}`[] = [];
|
||||||
let countDeleted = 0;
|
let countDeleted = 0;
|
||||||
let countInserted = 0;
|
let countInserted = 0;
|
||||||
|
|
||||||
@@ -53,7 +50,6 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
|||||||
if (!repositoryIconGroup.success) {
|
if (!repositoryIconGroup.success) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const localSkippedChecksums: string[] = [];
|
|
||||||
|
|
||||||
const repositoryInDb = databaseIconRepositories.find(
|
const repositoryInDb = databaseIconRepositories.find(
|
||||||
(dbIconGroup) => dbIconGroup.slug === repositoryIconGroup.slug,
|
(dbIconGroup) => dbIconGroup.slug === repositoryIconGroup.slug,
|
||||||
@@ -70,7 +66,7 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
|||||||
|
|
||||||
for (const icon of repositoryIconGroup.icons) {
|
for (const icon of repositoryIconGroup.icons) {
|
||||||
if (dbIconsInRepository.some((dbIcon) => dbIcon.checksum === icon.checksum)) {
|
if (dbIconsInRepository.some((dbIcon) => dbIcon.checksum === icon.checksum)) {
|
||||||
localSkippedChecksums.push(icon.checksum);
|
skippedChecksums.push(`${iconRepositoryId}.${icon.checksum}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,38 +79,19 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
|||||||
});
|
});
|
||||||
countInserted++;
|
countInserted++;
|
||||||
}
|
}
|
||||||
|
|
||||||
skippedChecksums.set(iconRepositoryId, localSkippedChecksums);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deadIcons = allDbIcons.filter(
|
||||||
|
(icon) => !skippedChecksums.includes(`${icon.iconRepositoryId}.${icon.checksum}`),
|
||||||
|
);
|
||||||
|
|
||||||
const deadIconRepositories = databaseIconRepositories.filter(
|
const deadIconRepositories = databaseIconRepositories.filter(
|
||||||
(iconRepository) => !repositoryIconGroups.some((group) => group.slug === iconRepository.slug),
|
(iconRepository) => !repositoryIconGroups.some((group) => group.slug === iconRepository.slug),
|
||||||
);
|
);
|
||||||
|
|
||||||
const deadIconsFilter = or(
|
|
||||||
...[...skippedChecksums.entries()].map(([iconRepositoryId, checksums]) =>
|
|
||||||
and(eq(icons.iconRepositoryId, iconRepositoryId), notInArray(icons.checksum, checksums)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await handleTransactionsAsync(db, {
|
await handleTransactionsAsync(db, {
|
||||||
async handleAsync(db, schema) {
|
async handleAsync(db, schema) {
|
||||||
await db.transaction(async (transaction) => {
|
await db.transaction(async (transaction) => {
|
||||||
const result = (await transaction.delete(schema.icons).where(deadIconsFilter)) as
|
|
||||||
| MySqlRawQueryResult
|
|
||||||
| QueryResult;
|
|
||||||
|
|
||||||
countDeleted += Array.isArray(result) ? result[0].affectedRows : (result.rowCount ?? 0);
|
|
||||||
|
|
||||||
if (deadIconRepositories.length >= 1) {
|
|
||||||
await transaction.delete(schema.iconRepositories).where(
|
|
||||||
inArray(
|
|
||||||
iconRepositories.id,
|
|
||||||
deadIconRepositories.map((iconRepository) => iconRepository.id),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newIconRepositories.length >= 1) {
|
if (newIconRepositories.length >= 1) {
|
||||||
await transaction.insert(schema.iconRepositories).values(newIconRepositories);
|
await transaction.insert(schema.iconRepositories).values(newIconRepositories);
|
||||||
}
|
}
|
||||||
@@ -125,12 +102,52 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
|||||||
await transaction.insert(schema.icons).values(chunck);
|
await transaction.insert(schema.icons).values(chunck);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (deadIcons.length >= 1) {
|
||||||
|
await transaction.delete(schema.icons).where(
|
||||||
|
inArray(
|
||||||
|
// Combine iconRepositoryId and checksum to allow same icons on different repositories
|
||||||
|
sql`concat(${icons.iconRepositoryId}, '.', ${icons.checksum})`,
|
||||||
|
deadIcons.map((icon) => `${icon.iconRepositoryId}.${icon.checksum}`),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deadIconRepositories.length >= 1) {
|
||||||
|
await transaction.delete(schema.iconRepositories).where(
|
||||||
|
inArray(
|
||||||
|
iconRepositories.id,
|
||||||
|
deadIconRepositories.map((iconRepository) => iconRepository.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
countDeleted += deadIcons.length;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleSync() {
|
handleSync() {
|
||||||
db.transaction((transaction) => {
|
db.transaction((transaction) => {
|
||||||
const result = transaction.delete(icons).where(deadIconsFilter).run();
|
if (newIconRepositories.length >= 1) {
|
||||||
countDeleted += result.changes;
|
transaction.insert(iconRepositories).values(newIconRepositories).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newIcons.length >= 1) {
|
||||||
|
// We only insert 5000 icons at a time to avoid SQLite limitations
|
||||||
|
for (const chunck of splitToNChunks(newIcons, Math.ceil(newIcons.length / 5000))) {
|
||||||
|
transaction.insert(icons).values(chunck).run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (deadIcons.length >= 1) {
|
||||||
|
transaction
|
||||||
|
.delete(icons)
|
||||||
|
.where(
|
||||||
|
inArray(
|
||||||
|
// Combine iconRepositoryId and checksum to allow same icons on different repositories
|
||||||
|
sql`concat(${icons.iconRepositoryId}, '.', ${icons.checksum})`,
|
||||||
|
deadIcons.map((icon) => `${icon.iconRepositoryId}.${icon.checksum}`),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
if (deadIconRepositories.length >= 1) {
|
if (deadIconRepositories.length >= 1) {
|
||||||
transaction
|
transaction
|
||||||
@@ -144,22 +161,13 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
|
|||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newIconRepositories.length >= 1) {
|
countDeleted += deadIcons.length;
|
||||||
transaction.insert(iconRepositories).values(newIconRepositories).run();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newIcons.length >= 1) {
|
|
||||||
// We only insert 5000 icons at a time to avoid SQLite limitations
|
|
||||||
for (const chunck of splitToNChunks(newIcons, Math.ceil(newIcons.length / 5000))) {
|
|
||||||
transaction.insert(icons).values(chunck).run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info("Updated icons in database", {
|
logger.info("Updated icons in database", {
|
||||||
duration: stopWatch.getElapsedInFormattedMilliseconds(),
|
duration: stopWatch.getElapsedInHumanWords(),
|
||||||
added: countInserted,
|
added: countInserted,
|
||||||
deleted: countDeleted,
|
deleted: countDeleted,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
: [],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
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 241 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user