Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6143e25686 | ||
|
|
384d83ef76 | ||
|
|
998161b1d9 | ||
|
|
a6e060265a | ||
|
|
d8b278d716 | ||
|
|
813a91cb01 | ||
|
|
990452a877 | ||
|
|
0165e2e415 | ||
|
|
514c6edc8f | ||
|
|
fdcfb775fa | ||
|
|
0843ccda99 | ||
|
|
2b6eb88a53 | ||
|
|
e65c3cd801 |
No files matched your search
@@ -33,11 +33,6 @@ body:
|
||||
options:
|
||||
# The below comment is used to insert a new version with on-release.yml
|
||||
#NEXT_VERSION#
|
||||
- 1.46.0
|
||||
- 1.45.3
|
||||
- 1.45.2
|
||||
- 1.45.1
|
||||
- 1.45.0
|
||||
- 1.44.0
|
||||
- 1.43.1
|
||||
- 1.43.0
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
name: Extract Build Artifact
|
||||
description: Extracts artifacts from an existing Docker image to be used for from source installation
|
||||
inputs:
|
||||
digest:
|
||||
description: Digest of Docker image to use
|
||||
required: true
|
||||
architecture:
|
||||
description: Name of architecture, will be used to build artifact content (amd64 or arm64)
|
||||
required: true
|
||||
release-tag:
|
||||
description: Tag of the release to which the artifact will be attached
|
||||
required: true
|
||||
repository:
|
||||
description: Repository to which the release belongs, e.g. owner/repo
|
||||
required: true
|
||||
token:
|
||||
description: GitHub token with permissions to upload release assets
|
||||
required: true
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Prebuilt debian dependencies
|
||||
uses: ./.github/actions/prebuilt-debian@dev
|
||||
id: prebuilt-debian
|
||||
with:
|
||||
architecture: ${{ inputs.architecture }}
|
||||
- name: Start docker container for ${{ inputs.architecture }}
|
||||
run: |
|
||||
docker run --name homarr \
|
||||
-e "SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000" \
|
||||
--detach --rm ${{ inputs.digest }}
|
||||
shell: bash
|
||||
- name: Prepare extraction
|
||||
run: |
|
||||
docker exec homarr cp /etc/nginx/templates/nginx.conf /app && \
|
||||
mkdir -p ${{ runner.temp }}/extraction/${{ inputs.architecture }}
|
||||
shell: bash
|
||||
- name: Extract source from ${{ inputs.architecture }} container (alpine)
|
||||
run: |
|
||||
docker exec homarr tar -czf extraction-alpine.tar.gz -C /app . && \
|
||||
docker cp homarr:/app/extraction-alpine.tar.gz ${{ runner.temp }}/extraction/${{ inputs.architecture }}/build-alpine-${{ inputs.architecture }}.tar.gz && \
|
||||
docker exec homarr rm /app/extraction-alpine.tar.gz
|
||||
shell: bash
|
||||
- name: Extract source from ${{ inputs.architecture }} container (debian)
|
||||
run: |
|
||||
docker cp ${{ steps.prebuilt-debian.outputs.path }}/. homarr:/app/build && \
|
||||
docker cp ${{ steps.prebuilt-debian.outputs.path }}/. homarr:/app/node_modules/better-sqlite3/build/Release && \
|
||||
docker exec homarr tar -czf extraction-debian.tar.gz -C /app . && \
|
||||
docker cp homarr:/app/extraction-debian.tar.gz ${{ runner.temp }}/extraction/${{ inputs.architecture }}/build-debian-${{ inputs.architecture }}.tar.gz
|
||||
shell: bash
|
||||
- name: Stop ${{ inputs.architecture }} container
|
||||
if: always()
|
||||
run: docker container remove --force --volumes homarr
|
||||
shell: bash
|
||||
- name: Add build archive to release (alpine)
|
||||
env:
|
||||
GH_TOKEN: ${{ inputs.token }}
|
||||
run: gh release upload --repo ${{ inputs.repository }} ${{ inputs.release-tag }} ${{ runner.temp }}/extraction/${{ inputs.architecture }}/build-alpine-${{ inputs.architecture }}.tar.gz --clobber
|
||||
shell: bash
|
||||
- name: Add build archive to release (debian)
|
||||
env:
|
||||
GH_TOKEN: ${{ inputs.token }}
|
||||
run: gh release upload --repo ${{ inputs.repository }} ${{ inputs.release-tag }} ${{ runner.temp }}/extraction/${{ inputs.architecture }}/build-debian-${{ inputs.architecture }}.tar.gz --clobber
|
||||
shell: bash
|
||||
@@ -1,42 +0,0 @@
|
||||
name: Prebuilt dependencies for debian
|
||||
description: Provides prebuilt dependencies for debian based docker images
|
||||
inputs:
|
||||
architecture:
|
||||
description: Name of architecture, will be used to build docker image (e.g. amd64, arm64)
|
||||
required: true
|
||||
outputs:
|
||||
path:
|
||||
description: Path to extracted prebuilt dependencies
|
||||
value: ${{ runner.temp }}/prebuilts
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build docker image for ${{ inputs.architecture }}
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: false
|
||||
load: true
|
||||
context: ./deployments/prebuilt-debian
|
||||
platforms: linux/${{ inputs.architecture }}
|
||||
tags: prebuilt-debian
|
||||
- name: Start docker container for ${{ inputs.architecture }}
|
||||
run: |
|
||||
docker run --name prebuilt-debian \
|
||||
--detach --rm prebuilt-debian
|
||||
shell: bash
|
||||
- name: Extract prebuilt dependencies from ${{ inputs.architecture }} container
|
||||
run: |
|
||||
mkdir -p ${{ runner.temp }}/prebuilts && \
|
||||
docker cp prebuilt-debian:/app/node_modules/better-sqlite3/build/Release/better_sqlite3.node ${{ runner.temp }}/prebuilts/better_sqlite3.node
|
||||
shell: bash
|
||||
- name: Stop ${{ inputs.architecture }} container
|
||||
if: always()
|
||||
run: docker container remove --force --volumes prebuilt-debian
|
||||
shell: bash
|
||||
@@ -8,11 +8,10 @@ permissions: {}
|
||||
jobs:
|
||||
approve-automatic-prs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
if: github.actor_id == 158783068 || github.actor_id == 190541745 || github.actor_id == 210161987 # Id of renovate bot and crowdin bot see https://api.github.com/users/homarr-renovate%5Bbot%5D and https://api.github.com/users/homarr-crowdin%5Bbot%5D and https://api.github.com/users/homarr-update-contributors%5Bbot%5D
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
- name: Obtain token
|
||||
id: obtainToken
|
||||
uses: actions/create-github-app-token@v2
|
||||
|
||||
@@ -23,9 +23,8 @@ env:
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup
|
||||
uses: ./tooling/github/setup
|
||||
@@ -39,9 +38,8 @@ jobs:
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup
|
||||
uses: ./tooling/github/setup
|
||||
@@ -51,9 +49,8 @@ jobs:
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup
|
||||
uses: ./tooling/github/setup
|
||||
@@ -63,9 +60,8 @@ jobs:
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup
|
||||
uses: ./tooling/github/setup
|
||||
@@ -81,10 +77,9 @@ jobs:
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup
|
||||
uses: ./tooling/github/setup
|
||||
- name: Build docker image
|
||||
@@ -106,9 +101,8 @@ jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup
|
||||
uses: ./tooling/github/setup
|
||||
- name: Copy env
|
||||
|
||||
@@ -13,8 +13,7 @@ permissions:
|
||||
jobs:
|
||||
validate-pull-request-title:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -10,12 +10,11 @@ permissions:
|
||||
|
||||
jobs:
|
||||
download-crowdin-translations:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Obtain token
|
||||
id: obtainToken
|
||||
|
||||
@@ -14,11 +14,10 @@ jobs:
|
||||
# Don't run this action if the downloaded translations are being pushed
|
||||
if: "!contains(github.event.head_commit.message, 'chore(lang)')"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Upload Crowdin translations
|
||||
uses: crowdin/github-action@v2
|
||||
|
||||
@@ -32,13 +32,12 @@ jobs:
|
||||
release:
|
||||
name: Create tag and release
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' }}
|
||||
outputs:
|
||||
version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }}
|
||||
git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }}
|
||||
skipped: ${{ env.SKIP_RELEASE }}
|
||||
|
||||
steps:
|
||||
- run: echo "Skipping release for workflow_dispatch event"
|
||||
if: env.SKIP_RELEASE == 'true'
|
||||
@@ -56,7 +55,7 @@ jobs:
|
||||
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
|
||||
permission-contents: write # required to commit package.json & changelog changes, merge them to dev and publish the release
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
with:
|
||||
persist-credentials: false
|
||||
@@ -65,7 +64,7 @@ jobs:
|
||||
- uses: actions/setup-node@v6
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
with:
|
||||
node-version: 24.12.0
|
||||
node-version: 24.11.1
|
||||
cache: "pnpm"
|
||||
- run: npm i -g pnpm
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
@@ -113,11 +112,10 @@ jobs:
|
||||
name: Build docker image for amd64
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
digest: ${{ steps.build.outputs.digest }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ needs.release.outputs.git_ref }}
|
||||
|
||||
@@ -148,15 +146,15 @@ jobs:
|
||||
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true
|
||||
env:
|
||||
SKIP_ENV_VALIDATION: true
|
||||
|
||||
build-arm64:
|
||||
name: Build docker image for arm64
|
||||
needs: release
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
digest: ${{ steps.build.outputs.digest }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ needs.release.outputs.git_ref }}
|
||||
|
||||
@@ -187,42 +185,11 @@ jobs:
|
||||
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true
|
||||
env:
|
||||
SKIP_ENV_VALIDATION: true
|
||||
extract-asset-amd64:
|
||||
name: Extract amd64 asset from docker image
|
||||
needs: [release, build-amd64]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- name: Extract amd64
|
||||
if: needs.release.outputs.skipped == 'false'
|
||||
uses: homarr-labs/homarr/.github/actions/extract-build-artifact@dev
|
||||
with:
|
||||
digest: "${{ env.GHCR_REPO }}@${{ needs.build-amd64.outputs.digest }}"
|
||||
architecture: amd64
|
||||
release-tag: ${{ needs.release.outputs.version }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: ${{ github.repository }}
|
||||
|
||||
extract-asset-arm64:
|
||||
name: Extract arm64 asset from docker image
|
||||
needs: [release, build-arm64]
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- name: Extract arm64
|
||||
if: needs.release.outputs.skipped == 'false'
|
||||
uses: homarr-labs/homarr/.github/actions/extract-build-artifact@dev
|
||||
with:
|
||||
digest: "${{ env.GHCR_REPO }}@${{ needs.build-arm64.outputs.digest }}"
|
||||
architecture: arm64
|
||||
release-tag: ${{ needs.release.outputs.version }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: ${{ github.repository }}
|
||||
publish:
|
||||
name: Complete deployment and notify
|
||||
needs: [release, build-amd64, build-arm64, extract-asset-amd64, extract-asset-arm64]
|
||||
needs: [release, build-amd64, build-arm64]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
NEXT_VERSION: ${{ needs.release.outputs.version }}
|
||||
DEPLOY_LATEST: ${{ github.ref_name == 'main' }}
|
||||
@@ -255,13 +222,6 @@ jobs:
|
||||
${{ env.GHCR_REPO }}@${{ needs.build-amd64.outputs.digest }} \
|
||||
${{ env.GHCR_REPO }}@${{ needs.build-arm64.outputs.digest }}
|
||||
|
||||
- name: Publish release
|
||||
if: needs.release.outputs.skipped == 'false'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release edit --repo ${{ github.repository }} ${{ needs.release.outputs.version }} --draft=false
|
||||
|
||||
- name: Discord notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
@@ -18,7 +18,6 @@ permissions:
|
||||
jobs:
|
||||
create-and-merge-pr:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- name: Discord notification
|
||||
if: ${{ github.events.inputs.send-notifications }}
|
||||
@@ -28,7 +27,7 @@ jobs:
|
||||
with:
|
||||
args: "Automatic 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
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get Next Version
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
name: "[Deployments] Validate prebuilt debian dependencies"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["*"]
|
||||
paths: [".github/actions/prebuilt-debian/**", "deployments/prebuilt-debian/**"]
|
||||
|
||||
jobs:
|
||||
prebuilt-debian-validate-amd64:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Validate prebuilt dependencies for amd64
|
||||
id: validate-amd64
|
||||
uses: ./.github/actions/prebuilt-debian
|
||||
with:
|
||||
architecture: amd64
|
||||
|
||||
- name: Check extracted files for amd64
|
||||
run: |
|
||||
if [ ! -f "${{ steps.validate-amd64.outputs.path }}/better_sqlite3.node" ]; then
|
||||
echo "better_sqlite3.node not found for amd64!"
|
||||
exit 1
|
||||
fi
|
||||
prebuilt-debian-validate-arm64:
|
||||
runs-on: ubuntu-24.04-
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Validate prebuilt dependencies for arm64
|
||||
id: validate-arm64
|
||||
uses: ./.github/actions/prebuilt-debian
|
||||
with:
|
||||
architecture: arm64
|
||||
|
||||
- name: Check extracted files for arm64
|
||||
run: |
|
||||
if [ ! -f "${{ steps.validate-arm64.outputs.path }}/better_sqlite3.node" ]; then
|
||||
echo "better_sqlite3.node not found for arm64!"
|
||||
exit 1
|
||||
fi
|
||||
@@ -11,9 +11,8 @@ on:
|
||||
jobs:
|
||||
renovate-validate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
- run: |
|
||||
npx --yes --package renovate -- \
|
||||
renovate-config-validator --strict .github/renovate.json5
|
||||
@@ -8,7 +8,6 @@ jobs:
|
||||
trigger-docs-release:
|
||||
name: Trigger Documentation Release
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- name: Obtain token
|
||||
id: obtainToken
|
||||
@@ -40,7 +39,6 @@ jobs:
|
||||
update-bug-report-template:
|
||||
name: Update Bug Report Template
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Obtain token
|
||||
id: obtainToken
|
||||
@@ -51,7 +49,7 @@ jobs:
|
||||
permission-contents: write # required to commit to branch
|
||||
permission-pull-requests: write # required to create pr & enable automerge
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
token: ${{ steps.obtainToken.outputs.token }}
|
||||
- name: Setup
|
||||
@@ -69,7 +67,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: create-pull-request
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ steps.obtainToken.outputs.token }}
|
||||
branch: update-bug-report-template
|
||||
|
||||
@@ -10,7 +10,6 @@ jobs:
|
||||
if: ${{ !startsWith(github.head_ref, 'renovate/') }}
|
||||
name: Skip Stability Days
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
steps:
|
||||
- name: Add status check
|
||||
env:
|
||||
|
||||
@@ -14,7 +14,6 @@ permissions:
|
||||
jobs:
|
||||
update-contributors:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22]
|
||||
@@ -29,7 +28,7 @@ jobs:
|
||||
permission-pull-requests: write # required to create pr & enable automerge
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||
|
||||
@@ -54,7 +53,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: create-pull-request
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ steps.obtainToken.outputs.token }}
|
||||
branch: update-contributors
|
||||
|
||||
@@ -17,7 +17,6 @@ jobs:
|
||||
group: update-integration
|
||||
cancel-in-progress: false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Obtain token
|
||||
id: obtainToken
|
||||
@@ -28,7 +27,7 @@ jobs:
|
||||
permission-contents: write # required to commit to branch
|
||||
permission-pull-requests: write # required to create pr & enable automerge
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
|
||||
- name: Setup
|
||||
@@ -45,7 +44,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: create-pull-request
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ steps.obtainToken.outputs.token }}
|
||||
branch: update-integrations-readme
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
{
|
||||
"successComment": false,
|
||||
"failComment": false,
|
||||
"draftRelease": true,
|
||||
"releaseBodyTemplate": "<%= _.truncate(nextRelease.notes, { 'length': 124000, 'omission': '' }) %>"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,67 +1,3 @@
|
||||
## [1.46.0](https://github.com/homarr-labs/homarr/compare/v1.45.3...v1.46.0) (2025-12-12)
|
||||
|
||||
### Features
|
||||
|
||||
* **docker:** add widget options to sort containers ([#4636](https://github.com/homarr-labs/homarr/issues/4636)) ([dd24925](https://github.com/homarr-labs/homarr/commit/dd24925d906dc13d73c52a47fdd07bec89798789))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add autocomplete and id attributes for password manager compatibility ([#4602](https://github.com/homarr-labs/homarr/issues/4602)) ([681aee0](https://github.com/homarr-labs/homarr/commit/681aee05f07a6e09371ed089d2b9ef361494c416))
|
||||
* **deps:** update dependency @tanstack/react-query to ^5.90.12 ([#4615](https://github.com/homarr-labs/homarr/issues/4615)) ([efc0384](https://github.com/homarr-labs/homarr/commit/efc0384b372fadafb65f61f715f08edd2a8f8573))
|
||||
* **deps:** update dependency better-sqlite3 to ^12.5.0 ([#4569](https://github.com/homarr-labs/homarr/issues/4569)) ([a20248d](https://github.com/homarr-labs/homarr/commit/a20248d0b689ab1ca475f606edfca458897ce442))
|
||||
* **deps:** update dependency drizzle-kit to ^0.31.8 ([#4618](https://github.com/homarr-labs/homarr/issues/4618)) ([5cbab28](https://github.com/homarr-labs/homarr/commit/5cbab28099d41b8ade6cc8a42e69c4191310296c))
|
||||
* **deps:** update dependency drizzle-orm to ^0.45.0 ([#4619](https://github.com/homarr-labs/homarr/issues/4619)) ([cab5d19](https://github.com/homarr-labs/homarr/commit/cab5d19141a38fbb977f4ef6e9c2ddbfdfea6aff))
|
||||
* **deps:** update dependency ldapts to v8.0.12 ([#4613](https://github.com/homarr-labs/homarr/issues/4613)) ([4689ec9](https://github.com/homarr-labs/homarr/commit/4689ec9933e5286c0d19c66989023b5b3376ffb1))
|
||||
* **deps:** update dependency ldapts to v8.0.13 ([#4626](https://github.com/homarr-labs/homarr/issues/4626)) ([8a14a6c](https://github.com/homarr-labs/homarr/commit/8a14a6cb456a6f16975372f64451e3b54ebfd88d))
|
||||
* **deps:** update dependency ldapts to v8.0.14 ([#4627](https://github.com/homarr-labs/homarr/issues/4627)) ([e37325f](https://github.com/homarr-labs/homarr/commit/e37325f5e0314b13d031fba1bff591b881baf8d1))
|
||||
* **deps:** update dependency winston to v3.19.0 ([#4632](https://github.com/homarr-labs/homarr/issues/4632)) ([3b1c564](https://github.com/homarr-labs/homarr/commit/3b1c564a5abbf1d1fca6ecab46e173c8f6938130))
|
||||
* **deps:** update next monorepo to v16.0.10 [security] ([#4634](https://github.com/homarr-labs/homarr/issues/4634)) ([2b2c0e7](https://github.com/homarr-labs/homarr/commit/2b2c0e7e3a813525bd977276df6e5594bc283055))
|
||||
* **deps:** update react monorepo to v19.2.1 ([#4608](https://github.com/homarr-labs/homarr/issues/4608)) ([f6d16fe](https://github.com/homarr-labs/homarr/commit/f6d16fe44bf239fb85a4ee3e9ae6491131315dce))
|
||||
* **deps:** update tiptap monorepo to v3 (major) ([#3612](https://github.com/homarr-labs/homarr/issues/3612)) ([d111eee](https://github.com/homarr-labs/homarr/commit/d111eee0e0a41e55de87049f3cb0a9154ae82ea1))
|
||||
* **deps:** update turbo monorepo to ^2.6.2 ([#4612](https://github.com/homarr-labs/homarr/issues/4612)) ([7de45e6](https://github.com/homarr-labs/homarr/commit/7de45e6fe50ccf4333959f924889463a22316984))
|
||||
* **deps:** update turbo monorepo to ^2.6.3 ([#4622](https://github.com/homarr-labs/homarr/issues/4622)) ([85689ca](https://github.com/homarr-labs/homarr/commit/85689caf6c25b80cb4ce04a0baf2980a1a4deece))
|
||||
* format issues ([1dd9694](https://github.com/homarr-labs/homarr/commit/1dd9694719ec4dcefe3c8d221015ba9aecede78c))
|
||||
* improve run script ([#4609](https://github.com/homarr-labs/homarr/issues/4609)) ([d084dfb](https://github.com/homarr-labs/homarr/commit/d084dfb4705fca68bb9f9465ec9564050d0b04b2))
|
||||
* **plex:** test connection not fully checking connection ([#4617](https://github.com/homarr-labs/homarr/issues/4617)) ([6a90839](https://github.com/homarr-labs/homarr/commit/6a908397cd8a6189123b91ea9d68f683af2bed05))
|
||||
* **transmission:** total download incorrect for cross seeded torrents ([#3211](https://github.com/homarr-labs/homarr/issues/3211)) ([0b4ca93](https://github.com/homarr-labs/homarr/commit/0b4ca93aa13e9838b481daed5703640d30a52ffc))
|
||||
|
||||
## [1.45.3](https://github.com/homarr-labs/homarr/compare/v1.45.2...v1.45.3) (2025-12-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency glob to v13 ([#4584](https://github.com/homarr-labs/homarr/issues/4584)) ([020cbd7](https://github.com/homarr-labs/homarr/commit/020cbd72d387c9f50f4eddcbc2685829cf7787de))
|
||||
* **deps:** update dependency jotai to ^2.15.2 ([#4597](https://github.com/homarr-labs/homarr/issues/4597)) ([ed638d4](https://github.com/homarr-labs/homarr/commit/ed638d47679594eab3cc19a12473722644ebf694))
|
||||
* **deps:** update dependency next-intl to v4.5.7 ([#4598](https://github.com/homarr-labs/homarr/issues/4598)) ([6b28aa8](https://github.com/homarr-labs/homarr/commit/6b28aa86d0bad29e4806592079a7f5d2e22b5ade))
|
||||
* **deps:** update dependency next-intl to v4.5.8 ([#4606](https://github.com/homarr-labs/homarr/issues/4606)) ([4f92e52](https://github.com/homarr-labs/homarr/commit/4f92e5265a40c636f303406e728e88cc056da23c))
|
||||
* **deps:** update dependency prettier to ^3.7.4 ([#4582](https://github.com/homarr-labs/homarr/issues/4582)) ([5480982](https://github.com/homarr-labs/homarr/commit/54809823e9edeacc245b560747b622aba683b3bf))
|
||||
* **deps:** update dependency typescript-eslint to ^8.48.1 ([#4438](https://github.com/homarr-labs/homarr/issues/4438)) ([d88c602](https://github.com/homarr-labs/homarr/commit/d88c6027fe3accf7a6872ada8aa2724d30bf691c))
|
||||
|
||||
## [1.45.2](https://github.com/homarr-labs/homarr/compare/v1.45.1...v1.45.2) (2025-12-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add ipv6 support for internal nginx proxy ([2c48b18](https://github.com/homarr-labs/homarr/commit/2c48b18968a74cb7150c246ebb2505d07764f109))
|
||||
* disable react-compiler due to issue with mantine-react-table data updates ([#4593](https://github.com/homarr-labs/homarr/issues/4593)) ([10572ca](https://github.com/homarr-labs/homarr/commit/10572ca962081961fb63b3ad9f164c610edf35e4))
|
||||
|
||||
## [1.45.1](https://github.com/homarr-labs/homarr/compare/v1.45.0...v1.45.1) (2025-12-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update dependency chroma-js to ^3.1.4 ([#4570](https://github.com/homarr-labs/homarr/issues/4570)) ([d6fc46d](https://github.com/homarr-labs/homarr/commit/d6fc46df3b5e2dd79ff6f9a3dd3a3e2050a0eeef))
|
||||
* **deps:** update dependency chroma-js to ^3.2.0 ([#4585](https://github.com/homarr-labs/homarr/issues/4585)) ([b2c46d3](https://github.com/homarr-labs/homarr/commit/b2c46d33d6cc769e89e208fd29d3a1d143cf74c7))
|
||||
* **deps:** update dependency isomorphic-dompurify to ^2.33.0 ([#4581](https://github.com/homarr-labs/homarr/issues/4581)) ([ca01a31](https://github.com/homarr-labs/homarr/commit/ca01a31fe5cbce53eb0e6bd1a943320c092161ef))
|
||||
* **deps:** update dependency next-intl to v4.5.6 ([#4571](https://github.com/homarr-labs/homarr/issues/4571)) ([3062dda](https://github.com/homarr-labs/homarr/commit/3062ddafbe0bc1dbac9259af4036f0fa32d85237))
|
||||
* **deps:** update dependency sass to ^1.94.2 ([#4572](https://github.com/homarr-labs/homarr/issues/4572)) ([7a8c50f](https://github.com/homarr-labs/homarr/commit/7a8c50fa4c5224dd74af3c17f56e2e5b8d6a4f43))
|
||||
* **deps:** update dependency superjson to v2.2.6 ([#4573](https://github.com/homarr-labs/homarr/issues/4573)) ([deb5e9d](https://github.com/homarr-labs/homarr/commit/deb5e9d96092069e296ba473eaad58367b516f85))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.30.3 ([#4574](https://github.com/homarr-labs/homarr/issues/4574)) ([d2c1dc3](https://github.com/homarr-labs/homarr/commit/d2c1dc3cc1dd72fc9fc8ae9613467fa0b0b8d75a))
|
||||
* **deps:** update dependency zod to ^4.1.13 ([#4575](https://github.com/homarr-labs/homarr/issues/4575)) ([879a77f](https://github.com/homarr-labs/homarr/commit/879a77ff2a81b0d0a17d1487eaf6d8539a955301))
|
||||
* **deps:** update mantine monorepo to ^8.3.9 ([#4576](https://github.com/homarr-labs/homarr/issues/4576)) ([d5d59dd](https://github.com/homarr-labs/homarr/commit/d5d59dd2561f0be2f4f4c689f33c6a9a7840a21c))
|
||||
* **deps:** update nextjs monorepo to v16.0.5 ([#4577](https://github.com/homarr-labs/homarr/issues/4577)) ([16f12d9](https://github.com/homarr-labs/homarr/commit/16f12d98e6cdae50fe2989db0911cb1350fcb93b))
|
||||
* **deps:** update tanstack-query monorepo ([#4578](https://github.com/homarr-labs/homarr/issues/4578)) ([59bcbc8](https://github.com/homarr-labs/homarr/commit/59bcbc8b4de4a0a8cee8adf6336c7c10477c766a))
|
||||
* **deps:** update testcontainers-node monorepo to ^11.9.0 ([#4583](https://github.com/homarr-labs/homarr/issues/4583)) ([10bdd74](https://github.com/homarr-labs/homarr/commit/10bdd74b2b287cb08ec0926147ac0e7fcfd492bb))
|
||||
* **deps:** update trpc monorepo to ^11.7.2 ([#4579](https://github.com/homarr-labs/homarr/issues/4579)) ([3453482](https://github.com/homarr-labs/homarr/commit/3453482ed43644025c39dee8e20549e1beb86be0))
|
||||
* **security:** rce vulnerability in nextjs ([b40deba](https://github.com/homarr-labs/homarr/commit/b40deba9ff1a3d629b6027565378d9f3ad2f1980))
|
||||
|
||||
## [1.45.0](https://github.com/homarr-labs/homarr/compare/v1.44.0...v1.45.0) (2025-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:24.12.0-alpine AS base
|
||||
FROM node:24.11.1-alpine AS base
|
||||
|
||||
FROM base AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Importing env files here to validate on build
|
||||
import "@homarr/auth/env";
|
||||
import "@homarr/core/infrastructure/db/env";
|
||||
import "@homarr/db/env";
|
||||
import "@homarr/common/env";
|
||||
import "@homarr/core/infrastructure/logs/env";
|
||||
import "@homarr/log/env";
|
||||
import "@homarr/docker/env";
|
||||
|
||||
import type { NextConfig } from "next";
|
||||
@@ -20,8 +20,7 @@ const withNextIntl = createNextIntlPlugin({
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
reactStrictMode: true,
|
||||
// react compiler breaks mantine-react-table, so disabled for now
|
||||
//reactCompiler: true,
|
||||
reactCompiler: true,
|
||||
/** We already do typechecking as separate tasks in CI */
|
||||
typescript: { ignoreBuildErrors: true },
|
||||
/**
|
||||
|
||||
+28
-28
@@ -36,6 +36,7 @@
|
||||
"@homarr/icons": "workspace:^0.1.0",
|
||||
"@homarr/image-proxy": "workspace:^0.1.0",
|
||||
"@homarr/integrations": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^",
|
||||
"@homarr/modals": "workspace:^0.1.0",
|
||||
"@homarr/modals-collection": "workspace:^0.1.0",
|
||||
"@homarr/notifications": "workspace:^0.1.0",
|
||||
@@ -49,62 +50,61 @@
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@homarr/widgets": "workspace:^0.1.0",
|
||||
"@mantine/colors-generator": "^8.3.10",
|
||||
"@mantine/core": "^8.3.10",
|
||||
"@mantine/dropzone": "^8.3.10",
|
||||
"@mantine/hooks": "^8.3.10",
|
||||
"@mantine/modals": "^8.3.10",
|
||||
"@mantine/tiptap": "^8.3.10",
|
||||
"@mantine/colors-generator": "^8.3.8",
|
||||
"@mantine/core": "^8.3.8",
|
||||
"@mantine/dropzone": "^8.3.8",
|
||||
"@mantine/hooks": "^8.3.8",
|
||||
"@mantine/modals": "^8.3.8",
|
||||
"@mantine/tiptap": "^8.3.8",
|
||||
"@million/lint": "1.0.14",
|
||||
"@tabler/icons-react": "^3.35.0",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@tanstack/react-query-devtools": "^5.91.1",
|
||||
"@tanstack/react-query": "^5.90.10",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"@tanstack/react-query-next-experimental": "^5.91.0",
|
||||
"@trpc/client": "^11.8.0",
|
||||
"@trpc/next": "^11.8.0",
|
||||
"@trpc/react-query": "^11.8.0",
|
||||
"@trpc/server": "^11.8.0",
|
||||
"@trpc/client": "^11.7.1",
|
||||
"@trpc/next": "^11.7.1",
|
||||
"@trpc/react-query": "^11.7.1",
|
||||
"@trpc/server": "^11.7.1",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"chroma-js": "^3.2.0",
|
||||
"chroma-js": "^3.1.2",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.19",
|
||||
"dotenv": "^17.2.3",
|
||||
"flag-icons": "^7.5.0",
|
||||
"glob": "^13.0.0",
|
||||
"isomorphic-dompurify": "^2.34.0",
|
||||
"jotai": "^2.16.0",
|
||||
"glob": "^12.0.0",
|
||||
"isomorphic-dompurify": "^2.32.0",
|
||||
"jotai": "^2.15.1",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "16.0.10",
|
||||
"next": "16.0.3",
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-joyride": "^2.9.3",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"sass": "^1.96.0",
|
||||
"superjson": "2.2.6",
|
||||
"swagger-ui-react": "^5.31.0",
|
||||
"sass": "^1.94.1",
|
||||
"superjson": "2.2.5",
|
||||
"swagger-ui-react": "^5.30.2",
|
||||
"use-deep-compare-effect": "^1.8.1",
|
||||
"zod": "^4.1.13"
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/chroma-js": "3.1.2",
|
||||
"@types/node": "^24.10.4",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react": "19.2.6",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint": "^9.39.1",
|
||||
"node-loader": "^2.1.0",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -64,23 +64,16 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
|
||||
<Stack gap="xl">
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack gap="lg">
|
||||
<TextInput
|
||||
label={t("field.username.label")}
|
||||
id="username"
|
||||
autoComplete="username"
|
||||
{...form.getInputProps("username")}
|
||||
/>
|
||||
<TextInput label={t("field.username.label")} autoComplete="off" {...form.getInputProps("username")} />
|
||||
<CustomPasswordInput
|
||||
withPasswordRequirements
|
||||
label={t("field.password.label")}
|
||||
id="password"
|
||||
autoComplete="new-password"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label={t("field.passwordConfirm.label")}
|
||||
id="password-confirm"
|
||||
autoComplete="new-password"
|
||||
{...form.getInputProps("confirmPassword")}
|
||||
/>
|
||||
|
||||
@@ -116,18 +116,8 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
|
||||
<>
|
||||
<form onSubmit={form.onSubmit((credentials) => void signInAsync(credentials.provider, credentials))}>
|
||||
<Stack gap="lg">
|
||||
<TextInput
|
||||
label={t("field.username.label")}
|
||||
id="username"
|
||||
autoComplete="username"
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<PasswordInput
|
||||
label={t("field.password.label")}
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<TextInput label={t("field.username.label")} {...form.getInputProps("name")} />
|
||||
<PasswordInput label={t("field.password.label")} {...form.getInputProps("password")} />
|
||||
|
||||
{providers.includes("credentials") && (
|
||||
<Stack gap="sm">
|
||||
|
||||
@@ -11,9 +11,8 @@ import { IntegrationProvider } from "@homarr/auth/client";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { getIntegrationsWithPermissionsAsync } from "@homarr/auth/server";
|
||||
import { isNullOrWhitespace } from "@homarr/common";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
import { prefetchForKindAsync } from "@homarr/widgets/prefetch";
|
||||
|
||||
@@ -23,8 +22,6 @@ import type { Board, Item } from "../_types";
|
||||
import { DynamicClientBoard } from "./_dynamic-client";
|
||||
import { BoardContentHeaderActions } from "./_header-actions";
|
||||
|
||||
const logger = createLogger({ module: "createBoardContentPage" });
|
||||
|
||||
export type Params = Record<string, unknown>;
|
||||
|
||||
interface Props<TParams extends Params> {
|
||||
@@ -60,13 +57,7 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
|
||||
|
||||
for (const [kind, items] of itemsMap) {
|
||||
await prefetchForKindAsync(kind, queryClient, items).catch((error) => {
|
||||
logger.error(
|
||||
new ErrorWithMetadata(
|
||||
"Failed to prefetch widget",
|
||||
{ widgetKind: kind, itemCount: items.length },
|
||||
{ cause: error },
|
||||
),
|
||||
);
|
||||
logger.error(new Error("Failed to prefetch widget", { cause: error }));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
IconSettings,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { applyAutoLayoutToBoard, clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
@@ -165,7 +165,7 @@ const EditModeMenu = () => {
|
||||
});
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
if (isEditMode) return saveBoard(board);
|
||||
if (isEditMode) return saveBoard(applyAutoLayoutToBoard(board));
|
||||
open();
|
||||
}, [board, isEditMode, saveBoard, open]);
|
||||
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Fieldset, Grid, Group, Input, NumberInput, Slider, Stack, Text, TextInput } from "@mantine/core";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
ChipGroup,
|
||||
Fieldset,
|
||||
Grid,
|
||||
Group,
|
||||
Input,
|
||||
InputWrapper,
|
||||
NumberInput,
|
||||
RadioGroup,
|
||||
SimpleGrid,
|
||||
Slider,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { createId } from "@homarr/common";
|
||||
import { boardLayoutModes } from "@homarr/definitions";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { CustomRadioCard } from "@homarr/ui";
|
||||
import { boardSaveLayoutsSchema } from "@homarr/validation/board";
|
||||
|
||||
import type { Board } from "../../_types";
|
||||
@@ -25,6 +45,8 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
||||
const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), {
|
||||
initialValues: {
|
||||
layouts: board.layouts,
|
||||
layoutMode: board.layoutMode,
|
||||
baseLayoutId: board.baseLayoutId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -38,6 +60,53 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<RadioGroup {...form.getInputProps("layoutMode")} label={t("board.field.layoutMode.label")}>
|
||||
<SimpleGrid cols={{ sm: 2, xs: 1 }} spacing="md">
|
||||
{boardLayoutModes.values.map((layoutMode) => (
|
||||
<Box w="100%" key={layoutMode} pos="relative">
|
||||
<CustomRadioCard
|
||||
value={layoutMode}
|
||||
label={t(`board.field.layoutMode.option.${layoutMode}.label`)}
|
||||
description={t(`board.field.layoutMode.option.${layoutMode}.description`)}
|
||||
/>
|
||||
{layoutMode === boardLayoutModes.defaultValue && (
|
||||
<Badge pos="absolute" top="8px" right="8px" variant="light" size="sm">
|
||||
{t("common.select.badge.recommended")}
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</RadioGroup>
|
||||
|
||||
{form.values.layoutMode === "auto" && (
|
||||
<InputWrapper label={t("board.field.baseLayout.label")} description={t("board.field.baseLayout.description")}>
|
||||
<ChipGroup {...form.getInputProps("baseLayoutId")} multiple={false}>
|
||||
<SimpleGrid
|
||||
cols={{
|
||||
lg: Math.min(6, form.values.layouts.length),
|
||||
sm: Math.min(4, form.values.layouts.length),
|
||||
xs: Math.min(2, form.values.layouts.length),
|
||||
}}
|
||||
spacing="md"
|
||||
mt="sm"
|
||||
>
|
||||
{form.values.layouts.map((layout) => (
|
||||
<Chip
|
||||
key={layout.id}
|
||||
value={layout.id}
|
||||
w="100%"
|
||||
size="sm"
|
||||
styles={{ label: { width: "100%", justifyContent: "center" } }}
|
||||
>
|
||||
{layout.name}
|
||||
</Chip>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</ChipGroup>
|
||||
</InputWrapper>
|
||||
)}
|
||||
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={500}>{t("board.setting.section.layout.responsive.title")}</Text>
|
||||
@@ -82,18 +151,21 @@ export const LayoutSettingsContent = ({ board }: Props) => {
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
{form.values.layouts.length >= 2 && (
|
||||
{form.values.layouts.length >= 2 && form.values.baseLayoutId !== layout.id && (
|
||||
<Group justify="end">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => {
|
||||
form.setValues((previous) =>
|
||||
previous.layouts !== undefined && previous.layouts.length >= 2
|
||||
? {
|
||||
layouts: form.values.layouts.filter((filteredLayout) => filteredLayout.id !== layout.id),
|
||||
}
|
||||
: previous,
|
||||
);
|
||||
form.setValues((previous) => {
|
||||
if (!previous.layouts) return previous;
|
||||
if (previous.layouts.length < 2) return previous;
|
||||
if (previous.baseLayoutId === layout.id) return previous;
|
||||
|
||||
return {
|
||||
...previous,
|
||||
layouts: previous.layouts.filter((filteredLayout) => filteredLayout.id !== layout.id),
|
||||
};
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("common.action.remove")}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { TRPCError } from "@trpc/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { BoardProvider } from "@homarr/boards/context";
|
||||
import { EditModeProvider } from "@homarr/boards/edit-mode";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { MainHeader } from "~/components/layout/header";
|
||||
import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo";
|
||||
@@ -18,8 +18,6 @@ import { CustomCss } from "./(content)/_custom-css";
|
||||
import { BoardReadyProvider } from "./(content)/_ready-context";
|
||||
import { BoardMantineProvider } from "./(content)/_theme";
|
||||
|
||||
const logger = createLogger({ module: "createBoardLayout" });
|
||||
|
||||
interface CreateBoardLayoutProps<TParams extends Params> {
|
||||
headerActions: JSX.Element;
|
||||
getInitialBoardAsync: (params: TParams) => Promise<Board>;
|
||||
|
||||
@@ -12,11 +12,7 @@ import { groupCreateSchema } from "@homarr/validation/group";
|
||||
|
||||
export const InitGroup = () => {
|
||||
const t = useI18n();
|
||||
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/init");
|
||||
},
|
||||
});
|
||||
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation();
|
||||
const form = useZodForm(groupCreateSchema, {
|
||||
initialValues: {
|
||||
name: "",
|
||||
@@ -25,6 +21,9 @@ export const InitGroup = () => {
|
||||
|
||||
const handleSubmitAsync = async (values: z.infer<typeof groupCreateSchema>) => {
|
||||
await mutateAsync(values, {
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/init");
|
||||
},
|
||||
onError(error) {
|
||||
if (error.data?.code === "CONFLICT") {
|
||||
form.setErrors({ name: t("common.zod.errors.custom.groupNameTaken") });
|
||||
|
||||
@@ -17,11 +17,7 @@ import { settingsInitSchema } from "@homarr/validation/settings";
|
||||
export const InitSettings = () => {
|
||||
const tSection = useScopedI18n("management.page.settings.section");
|
||||
const t = useI18n();
|
||||
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/init");
|
||||
},
|
||||
});
|
||||
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation();
|
||||
const form = useZodForm(settingsInitSchema, { initialValues: defaultServerSettings });
|
||||
|
||||
form.watch("analytics.enableGeneral", ({ value }) => {
|
||||
@@ -35,7 +31,11 @@ export const InitSettings = () => {
|
||||
});
|
||||
|
||||
const handleSubmitAsync = async (values: z.infer<typeof settingsInitSchema>) => {
|
||||
await mutateAsync(values);
|
||||
await mutateAsync(values, {
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/init");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -26,19 +26,19 @@ export const InitUserForm = () => {
|
||||
|
||||
const handleSubmitAsync = async (values: FormType) => {
|
||||
await mutateAsync(values, {
|
||||
onSuccess() {
|
||||
async onSuccess() {
|
||||
showSuccessNotification({
|
||||
title: tUser("notification.success.title"),
|
||||
message: tUser("notification.success.message"),
|
||||
});
|
||||
|
||||
void signIn("credentials", {
|
||||
await signIn("credentials", {
|
||||
name: values.username,
|
||||
password: values.password,
|
||||
redirect: false,
|
||||
}).then(async () => {
|
||||
await revalidatePathActionAsync("/init");
|
||||
});
|
||||
|
||||
await revalidatePathActionAsync("/init");
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification({
|
||||
|
||||
@@ -84,6 +84,9 @@ export const BoardCardMenuDropdown = ({ board }: BoardCardMenuDropdownProps) =>
|
||||
id: board.id,
|
||||
name: board.name,
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await revalidatePathActionAsync("/manage/boards");
|
||||
},
|
||||
});
|
||||
}, [board.id, board.name, openDuplicateModal]);
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
import { createMetaTitle } from "~/metadata";
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { Title, SimpleGrid, Card } from "@mantine/core";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const session = await auth();
|
||||
if (!session?.user || !session.user.permissions.includes("admin")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const t = await getScopedI18n("management");
|
||||
|
||||
return {
|
||||
title: createMetaTitle(t("metaTitle")),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ToursPage() {
|
||||
const t = await getScopedI18n("tours");
|
||||
return (
|
||||
<ManageContainer>
|
||||
<DynamicBreadcrumb/>
|
||||
<Title>{t("page.title")}</Title>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 5 }}>
|
||||
<Card></Card>
|
||||
<Card></Card>
|
||||
<Card></Card>
|
||||
<Card></Card>
|
||||
<Card></Card>
|
||||
<Card></Card>
|
||||
</SimpleGrid>
|
||||
</ManageContainer>
|
||||
)
|
||||
}
|
||||
@@ -86,11 +86,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: createIntegrationAsync, isPending } = clientApi.integration.create.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/manage/integrations");
|
||||
},
|
||||
});
|
||||
const { mutateAsync: createIntegrationAsync, isPending } = clientApi.integration.create.useMutation();
|
||||
const [error, setError] = useState<null | AnyMappedTestConnectionError>(null);
|
||||
|
||||
const handleSubmitAsync = async ({ appId, appHref, hasApp, ...values }: FormType) => {
|
||||
@@ -120,7 +116,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
app,
|
||||
},
|
||||
{
|
||||
onSuccess(data) {
|
||||
async onSuccess(data) {
|
||||
// We do it this way as we are unable to send a typesafe error through onError
|
||||
if (data?.error) {
|
||||
setError(data.error);
|
||||
@@ -136,7 +132,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
message: t("integration.page.create.notification.success.message"),
|
||||
});
|
||||
|
||||
router.push("/manage/integrations");
|
||||
await revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
|
||||
},
|
||||
onError: () => {
|
||||
showErrorNotification({
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
IconUserFilled,
|
||||
IconUsers,
|
||||
IconUsersGroup,
|
||||
IconMotorbikeFilled
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { auth } from "@homarr/auth/next";
|
||||
@@ -159,11 +158,6 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
|
||||
label: t("items.help.label"),
|
||||
icon: IconHelpSquareRoundedFilled,
|
||||
items: [
|
||||
{
|
||||
label: t("items.help.items.tours"),
|
||||
icon: IconMotorbikeFilled,
|
||||
href: "/manage/help/tours"
|
||||
},
|
||||
{
|
||||
label: t("items.help.items.documentation"),
|
||||
icon: IconBook2,
|
||||
|
||||
+3
-6
@@ -15,11 +15,7 @@ interface RemoveCertificateProps {
|
||||
|
||||
export const RemoveCertificate = ({ fileName }: RemoveCertificateProps) => {
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const { mutateAsync } = clientApi.certificates.removeCertificate.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/manage/tools/certificates");
|
||||
},
|
||||
});
|
||||
const { mutateAsync } = clientApi.certificates.removeCertificate.useMutation();
|
||||
const t = useI18n();
|
||||
|
||||
const handleClick = () => {
|
||||
@@ -31,11 +27,12 @@ export const RemoveCertificate = ({ fileName }: RemoveCertificateProps) => {
|
||||
await mutateAsync(
|
||||
{ fileName },
|
||||
{
|
||||
onSuccess() {
|
||||
async onSuccess() {
|
||||
showSuccessNotification({
|
||||
title: t("certificate.action.remove.notification.success.title"),
|
||||
message: t("certificate.action.remove.notification.success.message"),
|
||||
});
|
||||
await revalidatePathActionAsync("/manage/tools/certificates");
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
|
||||
+3
-6
@@ -15,11 +15,7 @@ interface RemoveHostnameActionIconProps {
|
||||
}
|
||||
|
||||
export const RemoveHostnameActionIcon = (input: RemoveHostnameActionIconProps) => {
|
||||
const { mutateAsync } = clientApi.certificates.removeTrustedHostname.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/manage/tools/certificates/hostnames");
|
||||
},
|
||||
});
|
||||
const { mutateAsync } = clientApi.certificates.removeTrustedHostname.useMutation();
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const t = useI18n();
|
||||
|
||||
@@ -30,7 +26,8 @@ export const RemoveHostnameActionIcon = (input: RemoveHostnameActionIconProps) =
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
async onConfirm() {
|
||||
await mutateAsync(input, {
|
||||
onSuccess() {
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/manage/tools/certificates/hostnames");
|
||||
showSuccessNotification({
|
||||
title: t("certificate.action.removeHostname.notification.success.title"),
|
||||
message: t("certificate.action.removeHostname.notification.success.message"),
|
||||
|
||||
@@ -206,16 +206,15 @@ const ContainerActionBarButton = (props: ContainerActionBarButtonProps) => {
|
||||
const t = useScopedI18n("docker.action");
|
||||
const utils = clientApi.useUtils();
|
||||
|
||||
const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation({
|
||||
async onSettled() {
|
||||
await utils.docker.getContainers.invalidate();
|
||||
},
|
||||
});
|
||||
const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation();
|
||||
|
||||
const handleClickAsync = async () => {
|
||||
await mutateAsync(
|
||||
{ ids: props.selectedIds },
|
||||
{
|
||||
async onSettled() {
|
||||
await utils.docker.getContainers.invalidate();
|
||||
},
|
||||
onSuccess() {
|
||||
showSuccessNotification({
|
||||
title: t(`${props.action}.notification.success.title`),
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { Select } from "@mantine/core";
|
||||
|
||||
import type { LogLevel } from "@homarr/core/infrastructure/logs/constants";
|
||||
import { logLevelConfiguration, logLevels } from "@homarr/core/infrastructure/logs/constants";
|
||||
import type { LogLevel } from "@homarr/log/constants";
|
||||
import { logLevelConfiguration, logLevels } from "@homarr/log/constants";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { useLogContext } from "./log-context";
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { createContext, useContext, useMemo, useState } from "react";
|
||||
|
||||
import type { LogLevel } from "@homarr/core/infrastructure/logs/constants";
|
||||
import { logLevels } from "@homarr/core/infrastructure/logs/constants";
|
||||
import type { LogLevel } from "@homarr/log/constants";
|
||||
import { logLevels } from "@homarr/log/constants";
|
||||
|
||||
const LogContext = createContext<{
|
||||
level: LogLevel;
|
||||
|
||||
@@ -7,7 +7,7 @@ import "@xterm/xterm/css/xterm.css";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { logsEnv } from "@homarr/core/infrastructure/logs/env";
|
||||
import { env } from "@homarr/log/env";
|
||||
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { fullHeightWithoutHeaderAndFooter } from "~/constants";
|
||||
@@ -35,7 +35,7 @@ export default async function LogsManagementPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<LogContextProvider defaultLevel={logsEnv.LEVEL}>
|
||||
<LogContextProvider defaultLevel={env.LOG_LEVEL}>
|
||||
<Group justify="space-between" align="center" wrap="nowrap">
|
||||
<DynamicBreadcrumb />
|
||||
<LogLevelSelection />
|
||||
|
||||
+7
-8
@@ -18,12 +18,7 @@ interface UserProfileAvatarForm {
|
||||
}
|
||||
|
||||
export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
||||
const { mutate } = clientApi.user.setProfileImage.useMutation({
|
||||
async onSuccess() {
|
||||
// Revalidate all as the avatar is used in multiple places
|
||||
await revalidatePathActionAsync("/");
|
||||
},
|
||||
});
|
||||
const { mutate } = clientApi.user.setProfileImage.useMutation();
|
||||
const [opened, { toggle }] = useDisclosure(false);
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const t = useI18n();
|
||||
@@ -43,7 +38,9 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
||||
image: base64Url,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
async onSuccess() {
|
||||
// Revalidate all as the avatar is used in multiple places
|
||||
await revalidatePathActionAsync("/");
|
||||
showSuccessNotification({
|
||||
message: tManageAvatar("changeImage.notification.success.message"),
|
||||
});
|
||||
@@ -77,7 +74,9 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
|
||||
image: null,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
async onSuccess() {
|
||||
// Revalidate all as the avatar is used in multiple places
|
||||
await revalidatePathActionAsync("/");
|
||||
showSuccessNotification({
|
||||
message: tManageAvatar("removeImage.notification.success.message"),
|
||||
});
|
||||
|
||||
@@ -25,11 +25,7 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
|
||||
const [innerOwnerId, setInnerOwnerId] = useState(group.ownerId);
|
||||
const { openModal } = useModalAction(UserSelectModal);
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const { mutateAsync } = clientApi.group.transferOwnership.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync(`/manage/users/groups/${group.id}`);
|
||||
},
|
||||
});
|
||||
const { mutateAsync } = clientApi.group.transferOwnership.useMutation();
|
||||
|
||||
const handleTransfer = useCallback(() => {
|
||||
openModal(
|
||||
@@ -51,7 +47,7 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
|
||||
userId: id,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
async onSuccess() {
|
||||
setInnerOwnerId(id);
|
||||
showSuccessNotification({
|
||||
title: tRoot("common.notification.transfer.success"),
|
||||
@@ -60,6 +56,7 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
|
||||
user: name,
|
||||
}),
|
||||
});
|
||||
await revalidatePathActionAsync(`/manage/users/groups/${group.id}`);
|
||||
},
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
|
||||
@@ -56,19 +56,16 @@ export const GroupsTable = ({ groups, initialGroupIds, hasFilter }: GroupsTableP
|
||||
() => initialGroupIds.some((groupId, index) => groupIds.indexOf(groupId) !== index),
|
||||
[groupIds, initialGroupIds],
|
||||
);
|
||||
const { mutateAsync, isPending } = clientApi.group.savePositions.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/manage/users/groups");
|
||||
},
|
||||
});
|
||||
const { mutateAsync, isPending } = clientApi.group.savePositions.useMutation();
|
||||
const handleSavePositionsAsync = async () => {
|
||||
await mutateAsync(
|
||||
{ positions: groupIds },
|
||||
{
|
||||
onSuccess() {
|
||||
async onSuccess() {
|
||||
showSuccessNotification({
|
||||
message: t("group.action.changePosition.notification.success.message"),
|
||||
});
|
||||
await revalidatePathActionAsync("/manage/users/groups");
|
||||
},
|
||||
onError() {
|
||||
showSuccessNotification({
|
||||
|
||||
@@ -6,12 +6,9 @@ import { appRouter, createTRPCContext } from "@homarr/api";
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { hashPasswordAsync } from "@homarr/auth";
|
||||
import { createSessionAsync } from "@homarr/auth/server";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||
import { db, eq } from "@homarr/db";
|
||||
import { apiKeys } from "@homarr/db/schema";
|
||||
|
||||
const logger = createLogger({ module: "trpcOpenApiRoute" });
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
const handlerAsync = async (req: NextRequest) => {
|
||||
const apiKeyHeaderValue = req.headers.get("ApiKey");
|
||||
@@ -30,7 +27,7 @@ const handlerAsync = async (req: NextRequest) => {
|
||||
router: appRouter,
|
||||
createContext: () => createTRPCContext({ session, headers: req.headers }),
|
||||
onError({ error, path, type }) {
|
||||
logger.error(new ErrorWithMetadata("tRPC Error occured", { path, type }, { cause: error }));
|
||||
logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause }));
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -51,10 +48,9 @@ const getSessionOrDefaultFromHeadersAsync = async (
|
||||
const [apiKeyId, apiKey] = apiKeyHeaderValue.split(".");
|
||||
|
||||
if (!apiKeyId || !apiKey) {
|
||||
logger.warn("An attempt to authenticate over API has failed due to invalid API key format", {
|
||||
ipAdress,
|
||||
userAgent,
|
||||
});
|
||||
logger.warn(
|
||||
`An attempt to authenticate over API has failed due to invalid API key format ip='${ipAdress}' userAgent='${userAgent}'`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -78,21 +74,18 @@ const getSessionOrDefaultFromHeadersAsync = async (
|
||||
});
|
||||
|
||||
if (!apiKeyFromDb) {
|
||||
logger.warn("An attempt to authenticate over API has failed", { ipAdress, userAgent });
|
||||
logger.warn(`An attempt to authenticate over API has failed ip='${ipAdress}' userAgent='${userAgent}'`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const hashedApiKey = await hashPasswordAsync(apiKey, apiKeyFromDb.salt);
|
||||
|
||||
if (apiKeyFromDb.apiKey !== hashedApiKey) {
|
||||
logger.warn("An attempt to authenticate over API has failed", { ipAdress, userAgent });
|
||||
logger.warn(`An attempt to authenticate over API has failed ip='${ipAdress}' userAgent='${userAgent}'`);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info("Read session from API request and found user", {
|
||||
name: apiKeyFromDb.user.name,
|
||||
id: apiKeyFromDb.user.id,
|
||||
});
|
||||
logger.info(`Read session from API request and found user ${apiKeyFromDb.user.name} (${apiKeyFromDb.user.id})`);
|
||||
return await createSessionAsync(db, apiKeyFromDb.user);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { createHandlersAsync } from "@homarr/auth";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||
|
||||
const logger = createLogger({ module: "nextAuthRoute" });
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
export const GET = async (req: NextRequest) => {
|
||||
const { handlers } = await createHandlersAsync(extractProvider(req), isSecureCookieEnabled(req));
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { performance } from "perf_hooks";
|
||||
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||
import { db } from "@homarr/db";
|
||||
import { logger } from "@homarr/log";
|
||||
import { handshakeAsync } from "@homarr/redis";
|
||||
|
||||
const logger = createLogger({ module: "healthLiveRoute" });
|
||||
|
||||
export async function GET() {
|
||||
const timeBeforeHealthCheck = performance.now();
|
||||
const response = await executeAndAggregateAllHealthChecksAsync();
|
||||
logger.info("Completed healthcheck", { elapsed: `${performance.now() - timeBeforeHealthCheck}ms` });
|
||||
logger.info(`Completed healthcheck after ${performance.now() - timeBeforeHealthCheck}ms`);
|
||||
|
||||
if (response.status === "healthy") {
|
||||
return new Response(JSON.stringify(response), {
|
||||
@@ -76,7 +73,7 @@ const executeHealthCheckSafelyAsync = async (
|
||||
};
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
logger.error(new ErrorWithMetadata("Healthcheck failed", { name }, { cause: error }));
|
||||
logger.error(`Healthcheck '${name}' has failed: ${error}`);
|
||||
return {
|
||||
status: "unhealthy",
|
||||
values: {
|
||||
|
||||
@@ -3,10 +3,7 @@ import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||
import { appRouter, createTRPCContext } from "@homarr/api";
|
||||
import { trpcPath } from "@homarr/api/shared";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||
|
||||
const logger = createLogger({ module: "trpcRoute" });
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
/**
|
||||
* Configure basic CORS headers
|
||||
@@ -34,7 +31,7 @@ const handler = auth(async (req) => {
|
||||
req,
|
||||
createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }),
|
||||
onError({ error, path, type }) {
|
||||
logger.error(new ErrorWithMetadata("tRPC Error occured", { path, type }, { cause: error }));
|
||||
logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause }));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export class BoardMockBuilder {
|
||||
private readonly board: Board;
|
||||
|
||||
constructor(board?: Partial<Omit<Board, "groupPermissions" | "userPermissions" | "sections" | "items" | "layouts">>) {
|
||||
const baseLayoutId = createId();
|
||||
this.board = {
|
||||
id: createId(),
|
||||
backgroundImageRepeat: "no-repeat",
|
||||
@@ -41,12 +42,14 @@ export class BoardMockBuilder {
|
||||
items: [],
|
||||
layouts: [
|
||||
{
|
||||
id: createId(),
|
||||
id: baseLayoutId,
|
||||
name: "Base",
|
||||
columnCount: 12,
|
||||
breakpoint: 0,
|
||||
},
|
||||
],
|
||||
baseLayoutId,
|
||||
layoutMode: "auto",
|
||||
...board,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ import "server-only";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
|
||||
const logger = createLogger({ module: "trpcCatchError" });
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
export const catchTrpcNotFound = (err: unknown) => {
|
||||
if (err instanceof TRPCError && err.code === "NOT_FOUND") {
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"@homarr/analytics": "workspace:^0.1.0",
|
||||
"@homarr/auth": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/core": "workspace:^",
|
||||
"@homarr/cron-job-api": "workspace:^0.1.0",
|
||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||
"@homarr/cron-jobs-core": "workspace:^0.1.0",
|
||||
@@ -31,6 +30,7 @@
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
"@homarr/icons": "workspace:^0.1.0",
|
||||
"@homarr/integrations": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^",
|
||||
"@homarr/ping": "workspace:^0.1.0",
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"@homarr/request-handler": "workspace:^0.1.0",
|
||||
@@ -40,18 +40,18 @@
|
||||
"dayjs": "^1.11.19",
|
||||
"dotenv": "^17.2.3",
|
||||
"fastify": "^5.6.2",
|
||||
"superjson": "2.2.6",
|
||||
"superjson": "2.2.5",
|
||||
"undici": "7.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node": "^24.10.4",
|
||||
"@types/node": "^24.10.1",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"esbuild": "^0.27.1",
|
||||
"eslint": "^9.39.2",
|
||||
"prettier": "^3.7.4",
|
||||
"esbuild": "^0.27.0",
|
||||
"eslint": "^9.39.1",
|
||||
"prettier": "^3.6.2",
|
||||
"tsx": "4.20.4",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { schedule, validate as validateCron } from "node-cron";
|
||||
|
||||
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 { Database, InferInsertModel } from "@homarr/db";
|
||||
import { eq } from "@homarr/db";
|
||||
import { cronJobConfigurations } from "@homarr/db/schema";
|
||||
|
||||
const logger = createLogger({ module: "jobManager" });
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
export class JobManager implements IJobManager {
|
||||
constructor(
|
||||
@@ -25,7 +23,7 @@ export class JobManager implements IJobManager {
|
||||
await this.jobGroup.stopAsync(name);
|
||||
}
|
||||
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="${name}" expression="${cron}"`);
|
||||
const job = this.jobGroup.getJobRegistry().get(name);
|
||||
if (!job) throw new Error(`Job ${name} not found`);
|
||||
if (!validateCron(cron)) {
|
||||
@@ -40,22 +38,22 @@ export class JobManager implements IJobManager {
|
||||
name,
|
||||
}),
|
||||
);
|
||||
logger.info("Cron job interval updated", { name, expression: cron });
|
||||
logger.info(`Cron job interval updated name="${name}" expression="${cron}"`);
|
||||
}
|
||||
public async disableAsync(name: JobGroupKeys): Promise<void> {
|
||||
logger.info("Disabling cron job", { name });
|
||||
logger.info(`Disabling cron job name="${name}"`);
|
||||
const job = this.jobGroup.getJobRegistry().get(name);
|
||||
if (!job) throw new Error(`Job ${name} not found`);
|
||||
|
||||
await this.updateConfigurationAsync(name, { isEnabled: false });
|
||||
await this.jobGroup.stopAsync(name);
|
||||
logger.info("Cron job disabled", { name });
|
||||
logger.info(`Cron job disabled name="${name}"`);
|
||||
}
|
||||
public async enableAsync(name: JobGroupKeys): Promise<void> {
|
||||
logger.info("Enabling cron job", { name });
|
||||
logger.info(`Enabling cron job name="${name}"`);
|
||||
await this.updateConfigurationAsync(name, { isEnabled: true });
|
||||
await this.jobGroup.startAsync(name);
|
||||
logger.info("Cron job enabled", { name });
|
||||
logger.info(`Cron job enabled name="${name}"`);
|
||||
}
|
||||
|
||||
private async updateConfigurationAsync(
|
||||
@@ -66,11 +64,9 @@ export class JobManager implements IJobManager {
|
||||
where: (table, { eq }) => eq(table.name, name),
|
||||
});
|
||||
|
||||
logger.debug("Updating cron job configuration", {
|
||||
name,
|
||||
configuration: JSON.stringify(configuration),
|
||||
exists: Boolean(existingConfig),
|
||||
});
|
||||
logger.debug(
|
||||
`Updating cron job configuration name="${name}" configuration="${JSON.stringify(configuration)}" exists="${Boolean(existingConfig)}"`,
|
||||
);
|
||||
|
||||
if (existingConfig) {
|
||||
await this.db
|
||||
@@ -78,10 +74,7 @@ export class JobManager implements IJobManager {
|
||||
// prevent updating the name, as it is the primary key
|
||||
.set({ ...configuration, name: undefined })
|
||||
.where(eq(cronJobConfigurations.name, name));
|
||||
logger.debug("Cron job configuration updated", {
|
||||
name,
|
||||
configuration: JSON.stringify(configuration),
|
||||
});
|
||||
logger.debug(`Cron job configuration updated name="${name}" configuration="${JSON.stringify(configuration)}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,10 +86,7 @@ export class JobManager implements IJobManager {
|
||||
cronExpression: configuration.cronExpression ?? job.cronExpression,
|
||||
isEnabled: configuration.isEnabled ?? true,
|
||||
});
|
||||
logger.debug("Cron job configuration updated", {
|
||||
name,
|
||||
configuration: JSON.stringify(configuration),
|
||||
});
|
||||
logger.debug(`Cron job configuration updated name="${name}" configuration="${JSON.stringify(configuration)}"`);
|
||||
}
|
||||
|
||||
public async getAllAsync(): Promise<
|
||||
|
||||
@@ -5,19 +5,16 @@ 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 { logger } from "@homarr/log";
|
||||
|
||||
import { JobManager } from "./job-manager";
|
||||
import { onStartAsync } from "./on-start";
|
||||
|
||||
const logger = createLogger({ module: "tasksMain" });
|
||||
|
||||
const server = fastify({
|
||||
maxParamLength: 5000,
|
||||
});
|
||||
@@ -30,7 +27,7 @@ server.register(fastifyTRPCPlugin, {
|
||||
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 }));
|
||||
logger.error(new Error(`Error in tasks tRPC handler path="${path}"`, { cause: error }));
|
||||
},
|
||||
} satisfies FastifyTRPCPluginOptions<JobRouter>["trpcOptions"],
|
||||
});
|
||||
@@ -42,11 +39,9 @@ void (async () => {
|
||||
|
||||
try {
|
||||
await server.listen({ port: CRON_JOB_API_PORT });
|
||||
logger.info("Tasks web server started successfully", { 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 }),
|
||||
);
|
||||
logger.error(new Error(`Failed to start tasks web server port="${CRON_JOB_API_PORT}"`, { cause: err }));
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { logger } from "@homarr/log";
|
||||
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
|
||||
|
||||
const logger = createLogger({ module: "invalidateUpdateCheckerCache" });
|
||||
const localLogger = logger.child({ module: "invalidateUpdateCheckerCache" });
|
||||
|
||||
/**
|
||||
* Invalidates the update checker cache on startup to ensure fresh data.
|
||||
@@ -11,8 +11,8 @@ export async function invalidateUpdateCheckerCacheAsync() {
|
||||
try {
|
||||
const handler = updateCheckerRequestHandler.handler({});
|
||||
await handler.invalidateAsync();
|
||||
logger.debug("Update checker cache invalidated");
|
||||
localLogger.debug("Update checker cache invalidated");
|
||||
} catch (error) {
|
||||
logger.error(new Error("Failed to invalidate update checker cache", { cause: error }));
|
||||
localLogger.error(new Error("Failed to invalidate update checker cache", { cause: error }));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { env } from "@homarr/auth/env";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { db, eq, inArray } from "@homarr/db";
|
||||
import { sessions, users } from "@homarr/db/schema";
|
||||
import { supportedAuthProviders } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
const logger = createLogger({ module: "sessionCleanup" });
|
||||
const localLogger = logger.child({ module: "sessionCleanup" });
|
||||
|
||||
/**
|
||||
* Deletes sessions for users that have inactive auth providers.
|
||||
@@ -29,13 +29,11 @@ export async function cleanupSessionsAsync() {
|
||||
await db.delete(sessions).where(inArray(sessions.userId, userIds));
|
||||
|
||||
if (sessionsWithInactiveProviders.length > 0) {
|
||||
logger.info("Deleted sessions for inactive providers", {
|
||||
count: userIds.length,
|
||||
});
|
||||
localLogger.info(`Deleted sessions for inactive providers count=${userIds.length}`);
|
||||
} else {
|
||||
logger.debug("No sessions to delete");
|
||||
localLogger.debug("No sessions to delete");
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(new Error("Failed to clean up sessions", { cause: error }));
|
||||
localLogger.error(new Error("Failed to clean up sessions", { cause: error }));
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,9 @@
|
||||
"@homarr/api": "workspace:^0.1.0",
|
||||
"@homarr/auth": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/core": "workspace:^",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^",
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"dotenv": "^17.2.3",
|
||||
@@ -34,9 +34,9 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"esbuild": "^0.27.1",
|
||||
"eslint": "^9.39.2",
|
||||
"prettier": "^3.7.4",
|
||||
"esbuild": "^0.27.0",
|
||||
"eslint": "^9.39.1",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,8 @@ import { WebSocketServer } from "ws";
|
||||
import { appRouter, createTRPCContext } from "@homarr/api/websocket";
|
||||
import { getSessionFromToken, sessionTokenCookieName } from "@homarr/auth";
|
||||
import { parseCookies } from "@homarr/common";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { db } from "@homarr/db";
|
||||
|
||||
const logger = createLogger({ module: "websocketMain" });
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
const wss = new WebSocketServer({
|
||||
port: 3001,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
FROM node:24.12.0-trixie AS base
|
||||
WORKDIR /app
|
||||
COPY package.json .
|
||||
COPY pnpm-lock.yaml .
|
||||
RUN corepack enable pnpm && pnpm install --frozen-lockfile
|
||||
CMD ["sleep", "60s"]
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "homarr-prebuilt-debian",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^12.5.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.25.0",
|
||||
"engines": {
|
||||
"node": ">=24.12.0",
|
||||
"pnpm": ">=10.25.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"better-sqlite3"
|
||||
]
|
||||
}
|
||||
}
|
||||
-286
@@ -1,286 +0,0 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
better-sqlite3:
|
||||
specifier: ^12.5.0
|
||||
version: 12.5.0
|
||||
|
||||
packages:
|
||||
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
better-sqlite3@12.5.0:
|
||||
resolution: {integrity: sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==}
|
||||
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
|
||||
|
||||
bindings@1.5.0:
|
||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||
|
||||
bl@4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
|
||||
buffer@5.7.1:
|
||||
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||
|
||||
chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
|
||||
decompress-response@6.0.0:
|
||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
deep-extend@0.6.0:
|
||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||
|
||||
expand-template@2.0.3:
|
||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||
|
||||
fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
|
||||
github-from-package@0.0.0:
|
||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
ini@1.3.8:
|
||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||
|
||||
mimic-response@3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
mkdirp-classic@0.5.3:
|
||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||
|
||||
napi-build-utils@2.0.0:
|
||||
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
|
||||
|
||||
node-abi@3.85.0:
|
||||
resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
prebuild-install@7.1.3:
|
||||
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
pump@3.0.3:
|
||||
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
|
||||
|
||||
rc@1.2.8:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
|
||||
readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
semver@7.7.3:
|
||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
simple-concat@1.0.1:
|
||||
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||
|
||||
simple-get@4.0.1:
|
||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||
|
||||
string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
strip-json-comments@2.0.1:
|
||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
tar-fs@2.1.4:
|
||||
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
|
||||
|
||||
tar-stream@2.2.0:
|
||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
better-sqlite3@12.5.0:
|
||||
dependencies:
|
||||
bindings: 1.5.0
|
||||
prebuild-install: 7.1.3
|
||||
|
||||
bindings@1.5.0:
|
||||
dependencies:
|
||||
file-uri-to-path: 1.0.0
|
||||
|
||||
bl@4.1.0:
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
buffer@5.7.1:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
chownr@1.1.4: {}
|
||||
|
||||
decompress-response@6.0.0:
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
|
||||
deep-extend@0.6.0: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
|
||||
expand-template@2.0.3: {}
|
||||
|
||||
file-uri-to-path@1.0.0: {}
|
||||
|
||||
fs-constants@1.0.0: {}
|
||||
|
||||
github-from-package@0.0.0: {}
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
ini@1.3.8: {}
|
||||
|
||||
mimic-response@3.1.0: {}
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
mkdirp-classic@0.5.3: {}
|
||||
|
||||
napi-build-utils@2.0.0: {}
|
||||
|
||||
node-abi@3.85.0:
|
||||
dependencies:
|
||||
semver: 7.7.3
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
prebuild-install@7.1.3:
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
expand-template: 2.0.3
|
||||
github-from-package: 0.0.0
|
||||
minimist: 1.2.8
|
||||
mkdirp-classic: 0.5.3
|
||||
napi-build-utils: 2.0.0
|
||||
node-abi: 3.85.0
|
||||
pump: 3.0.3
|
||||
rc: 1.2.8
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 2.1.4
|
||||
tunnel-agent: 0.6.0
|
||||
|
||||
pump@3.0.3:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.5
|
||||
once: 1.4.0
|
||||
|
||||
rc@1.2.8:
|
||||
dependencies:
|
||||
deep-extend: 0.6.0
|
||||
ini: 1.3.8
|
||||
minimist: 1.2.8
|
||||
strip-json-comments: 2.0.1
|
||||
|
||||
readable-stream@3.6.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
semver@7.7.3: {}
|
||||
|
||||
simple-concat@1.0.1: {}
|
||||
|
||||
simple-get@4.0.1:
|
||||
dependencies:
|
||||
decompress-response: 6.0.0
|
||||
once: 1.4.0
|
||||
simple-concat: 1.0.1
|
||||
|
||||
string_decoder@1.3.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
strip-json-comments@2.0.1: {}
|
||||
|
||||
tar-fs@2.1.4:
|
||||
dependencies:
|
||||
chownr: 1.1.4
|
||||
mkdirp-classic: 0.5.3
|
||||
pump: 3.0.3
|
||||
tar-stream: 2.2.0
|
||||
|
||||
tar-stream@2.2.0:
|
||||
dependencies:
|
||||
bl: 4.1.0
|
||||
end-of-stream: 1.4.5
|
||||
fs-constants: 1.0.0
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
@@ -5,7 +5,6 @@ import Database from "better-sqlite3";
|
||||
import { BetterSQLite3Database, drizzle } from "drizzle-orm/better-sqlite3";
|
||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||
|
||||
import { DB_CASING } from "../../packages/core/src/infrastructure/db/constants";
|
||||
import * as sqliteSchema from "../../packages/db/schema/sqlite";
|
||||
|
||||
export const createSqliteDbFileAsync = async () => {
|
||||
@@ -17,7 +16,7 @@ export const createSqliteDbFileAsync = async () => {
|
||||
const connection = new Database(localDbUrl);
|
||||
const db = drizzle(connection, {
|
||||
schema: sqliteSchema,
|
||||
casing: DB_CASING,
|
||||
casing: "snake_case",
|
||||
});
|
||||
|
||||
await migrate(db, {
|
||||
|
||||
+1
-2
@@ -5,7 +5,6 @@ events {
|
||||
http {
|
||||
server {
|
||||
listen 7575;
|
||||
listen [::]:7575;
|
||||
|
||||
# Route websockets traffic to port 3001
|
||||
location /websockets {
|
||||
@@ -26,4 +25,4 @@ http {
|
||||
client_max_body_size 32M;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-19
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "1.46.0",
|
||||
"version": "1.45.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env CI=true turbo build",
|
||||
@@ -40,29 +40,29 @@
|
||||
"@semantic-release/commit-analyzer": "^13.0.1",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/github": "^12.0.2",
|
||||
"@semantic-release/npm": "^13.1.3",
|
||||
"@semantic-release/npm": "^13.1.2",
|
||||
"@semantic-release/release-notes-generator": "^14.1.0",
|
||||
"@testcontainers/redis": "^11.10.0",
|
||||
"@turbo/gen": "^2.6.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^4.0.15",
|
||||
"@vitest/ui": "^4.0.15",
|
||||
"@testcontainers/redis": "^11.8.1",
|
||||
"@turbo/gen": "^2.6.1",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@vitest/coverage-v8": "^4.0.10",
|
||||
"@vitest/ui": "^4.0.10",
|
||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"jsdom": "^27.3.0",
|
||||
"jsdom": "^27.2.0",
|
||||
"json5": "^2.2.3",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier": "^3.6.2",
|
||||
"semantic-release": "^25.0.2",
|
||||
"testcontainers": "^11.10.0",
|
||||
"turbo": "^2.6.3",
|
||||
"testcontainers": "^11.8.1",
|
||||
"turbo": "^2.6.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.0.15"
|
||||
"vitest": "^4.0.10"
|
||||
},
|
||||
"packageManager": "pnpm@10.25.0",
|
||||
"packageManager": "pnpm@10.22.0",
|
||||
"engines": {
|
||||
"node": ">=24.12.0",
|
||||
"pnpm": ">=10.25.0"
|
||||
"node": ">=24.11.1",
|
||||
"pnpm": ">=10.22.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
@@ -82,20 +82,20 @@
|
||||
"axios@>=1.0.0 <1.8.2": ">=1.13.2",
|
||||
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
|
||||
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
|
||||
"esbuild@<=0.24.2": ">=0.27.1",
|
||||
"esbuild@<=0.24.2": ">=0.27.0",
|
||||
"form-data@>=4.0.0 <4.0.4": ">=4.0.5",
|
||||
"hono@<4.6.5": ">=4.11.0",
|
||||
"hono@<4.6.5": ">=4.10.6",
|
||||
"linkifyjs@<4.3.2": ">=4.3.2",
|
||||
"nanoid@>=4.0.0 <5.0.9": ">=5.1.6",
|
||||
"prismjs@<1.30.0": ">=1.30.0",
|
||||
"proxmox-api>undici": "7.16.0",
|
||||
"react-is": "^19.2.3",
|
||||
"react-is": "^19.2.0",
|
||||
"rollup@>=4.0.0 <4.22.4": ">=4.53.3",
|
||||
"sha.js@<=2.4.11": ">=2.4.12",
|
||||
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
|
||||
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
|
||||
"tmp@<=0.2.3": ">=0.2.5",
|
||||
"vite@>=5.0.0 <=5.4.18": ">=7.2.7"
|
||||
"vite@>=5.0.0 <=5.4.18": ">=7.2.2"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@types/node-unifi": "patches/@types__node-unifi.patch",
|
||||
|
||||
@@ -22,17 +22,17 @@
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/core": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@umami/node": "^0.4.0",
|
||||
"superjson": "2.2.6"
|
||||
"superjson": "2.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
import type { UmamiEventData } from "@umami/node";
|
||||
import { Umami } from "@umami/node";
|
||||
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { count, db } from "@homarr/db";
|
||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||
import { integrations, items, users } from "@homarr/db/schema";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { defaultServerSettings } from "@homarr/server-settings";
|
||||
|
||||
import { Stopwatch } from "../../common/src";
|
||||
import { UMAMI_HOST_URL, UMAMI_WEBSITE_ID } from "./constants";
|
||||
|
||||
const logger = createLogger({ module: "analytics" });
|
||||
|
||||
export const sendServerAnalyticsAsync = async () => {
|
||||
const stopWatch = new Stopwatch();
|
||||
const analyticsSettings = await getServerSettingByKeyAsync(db, "analytics");
|
||||
|
||||
+13
-12
@@ -33,6 +33,7 @@
|
||||
"@homarr/docker": "workspace:^0.1.0",
|
||||
"@homarr/icons": "workspace:^0.1.0",
|
||||
"@homarr/integrations": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^",
|
||||
"@homarr/old-import": "workspace:^0.1.0",
|
||||
"@homarr/old-schema": "workspace:^0.1.0",
|
||||
"@homarr/ping": "workspace:^0.1.0",
|
||||
@@ -42,25 +43,25 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@kubernetes/client-node": "^1.4.0",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@trpc/client": "^11.8.0",
|
||||
"@trpc/react-query": "^11.8.0",
|
||||
"@trpc/server": "^11.8.0",
|
||||
"@trpc/tanstack-react-query": "^11.8.0",
|
||||
"@tanstack/react-query": "^5.90.10",
|
||||
"@trpc/client": "^11.7.1",
|
||||
"@trpc/react-query": "^11.7.1",
|
||||
"@trpc/server": "^11.7.1",
|
||||
"@trpc/tanstack-react-query": "^11.7.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"next": "16.0.10",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"superjson": "2.2.6",
|
||||
"next": "16.0.3",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"superjson": "2.2.5",
|
||||
"trpc-to-openapi": "^3.1.0",
|
||||
"zod": "^4.1.13"
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
"prettier": "^3.7.4",
|
||||
"eslint": "^9.39.1",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -17,3 +17,5 @@ export const fetchApi = createTRPCClient<AppRouter>({
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export { applyAutoLayoutToBoard } from "./router/board/board-layout";
|
||||
@@ -5,7 +5,7 @@ import { z } from "zod/v4";
|
||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||
import { createId } from "@homarr/common";
|
||||
import type { DeviceType } from "@homarr/common/server";
|
||||
import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db";
|
||||
import type { Database, InferInsertModel, InferSelectModel } from "@homarr/db";
|
||||
import { and, asc, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db";
|
||||
import { createDbInsertCollectionWithoutTransaction } from "@homarr/db/collection";
|
||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||
@@ -27,13 +27,11 @@ import {
|
||||
sections,
|
||||
users,
|
||||
} from "@homarr/db/schema";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import {
|
||||
emptySuperJSON,
|
||||
everyoneGroup,
|
||||
getPermissionsWithChildren,
|
||||
getPermissionsWithParents,
|
||||
widgetKinds,
|
||||
} from "@homarr/definitions";
|
||||
import { importOldmarrAsync } from "@homarr/old-import";
|
||||
import { importJsonFileSchema } from "@homarr/old-import/shared";
|
||||
@@ -50,13 +48,11 @@ import {
|
||||
boardSaveSchema,
|
||||
} from "@homarr/validation/board";
|
||||
import { byIdSchema } from "@homarr/validation/common";
|
||||
import { zodUnionFromArray } from "@homarr/validation/enums";
|
||||
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
||||
import { sectionSchema, sharedItemSchema } from "@homarr/validation/shared";
|
||||
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
||||
import { generateResponsiveGridFor } from "./board/grid-algorithm";
|
||||
import { applyAutoLayoutToBoard, getUpdatedBoardLayout } from "./board/board-layout";
|
||||
import { getFullBoardWithWhereAsync } from "./board/full-board-query";
|
||||
|
||||
export const boardRouter = createTRPCRouter({
|
||||
exists: permissionRequiredProcedure
|
||||
@@ -279,6 +275,7 @@ export const boardRouter = createTRPCRouter({
|
||||
name: input.name,
|
||||
isPublic: input.isPublic,
|
||||
creatorId: ctx.session.user.id,
|
||||
layoutMode: input.layoutMode,
|
||||
});
|
||||
createBoardCollection.sections.push({
|
||||
id: createId(),
|
||||
@@ -287,8 +284,9 @@ export const boardRouter = createTRPCRouter({
|
||||
yOffset: 0,
|
||||
boardId,
|
||||
});
|
||||
const baseLayoutId = createId();
|
||||
createBoardCollection.layouts.push({
|
||||
id: createId(),
|
||||
id: baseLayoutId,
|
||||
name: "Base",
|
||||
columnCount: input.columnCount,
|
||||
breakpoint: 0,
|
||||
@@ -296,6 +294,10 @@ export const boardRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
await createBoardCollection.insertAllAsync(ctx.db);
|
||||
// Define the base layout (only for automatic layout mode)
|
||||
if (input.layoutMode === "auto") {
|
||||
await ctx.db.update(boards).set({ baseLayoutId }).where(eq(boards.id, boardId));
|
||||
}
|
||||
|
||||
if (!user?.homeBoardId) {
|
||||
await ctx.db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, ctx.session.user.id));
|
||||
@@ -582,13 +584,15 @@ export const boardRouter = createTRPCRouter({
|
||||
|
||||
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
|
||||
|
||||
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||
const board = await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||
return applyAutoLayoutToBoard(board);
|
||||
}),
|
||||
getBoardByName: publicProcedure.input(boardByNameSchema).query(async ({ input, ctx }) => {
|
||||
const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase());
|
||||
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
|
||||
|
||||
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||
const board = await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
|
||||
return applyAutoLayoutToBoard(board);
|
||||
}),
|
||||
saveLayouts: protectedProcedure.input(boardSaveLayoutsSchema).mutation(async ({ ctx, input }) => {
|
||||
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
|
||||
@@ -596,13 +600,32 @@ export const boardRouter = createTRPCRouter({
|
||||
const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
|
||||
|
||||
const addedLayouts = filterAddedItems(input.layouts, board.layouts);
|
||||
const removedLayouts = filterRemovedItems(input.layouts, board.layouts);
|
||||
|
||||
if (input.layoutMode === "auto" && input.baseLayoutId === null) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Base layout must be set when using automatic layout mode",
|
||||
});
|
||||
}
|
||||
|
||||
if (removedLayouts.some((layout) => layout.id === input.baseLayoutId)) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Cannot remove base layout",
|
||||
});
|
||||
}
|
||||
|
||||
const layoutsToInsert: InferInsertModel<typeof layouts>[] = [];
|
||||
const itemSectionLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = [];
|
||||
const sectionLayoutsToInsert: InferInsertModel<typeof sectionLayouts>[] = [];
|
||||
|
||||
let baseLayoutId = input.baseLayoutId;
|
||||
for (const addedLayout of addedLayouts) {
|
||||
const layoutId = createId();
|
||||
if (addedLayout.id === input.baseLayoutId) {
|
||||
baseLayoutId = layoutId;
|
||||
}
|
||||
|
||||
layoutsToInsert.push({
|
||||
id: layoutId,
|
||||
@@ -709,11 +732,18 @@ export const boardRouter = createTRPCRouter({
|
||||
.where(eq(layouts.id, updatedLayout.id));
|
||||
}
|
||||
|
||||
const removedLayouts = filterRemovedItems(input.layouts, board.layouts);
|
||||
const removedLayoutIds = removedLayouts.map((layout) => layout.id);
|
||||
if (removedLayoutIds.length > 0) {
|
||||
await ctx.db.delete(layouts).where(inArray(layouts.id, removedLayoutIds));
|
||||
}
|
||||
|
||||
await ctx.db
|
||||
.update(boards)
|
||||
.set({
|
||||
baseLayoutId,
|
||||
layoutMode: input.layoutMode,
|
||||
})
|
||||
.where(eq(boards.id, input.id));
|
||||
}),
|
||||
savePartialBoardSettings: protectedProcedure
|
||||
.input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() })))
|
||||
@@ -1408,244 +1438,6 @@ const noBoardWithSimilarNameAsync = async (db: Database, name: string, ignoredId
|
||||
}
|
||||
};
|
||||
|
||||
const getUpdatedBoardLayout = (
|
||||
board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>,
|
||||
options: {
|
||||
previous: {
|
||||
layoutId: string;
|
||||
columnCount: number;
|
||||
};
|
||||
current: {
|
||||
layoutId: string;
|
||||
columnCount: number;
|
||||
};
|
||||
},
|
||||
) => {
|
||||
const itemSectionLayoutsCollection: InferInsertModel<typeof itemLayouts>[] = [];
|
||||
const sectionLayoutsCollection: InferInsertModel<typeof sectionLayouts>[] = [];
|
||||
|
||||
const elements = getElementsForLayout(board, options.previous.layoutId);
|
||||
const rootSections = board.sections.filter((section) => section.kind !== "dynamic");
|
||||
|
||||
for (const rootSection of rootSections) {
|
||||
const result = generateResponsiveGridFor({
|
||||
items: elements,
|
||||
previousWidth: options.previous.columnCount,
|
||||
width: options.current.columnCount,
|
||||
sectionId: rootSection.id,
|
||||
});
|
||||
|
||||
itemSectionLayoutsCollection.push(
|
||||
...board.items
|
||||
.map((item): InferInsertModel<typeof itemLayouts> | null => {
|
||||
const currentElement = result.items.find((element) => element.type === "item" && element.id === item.id);
|
||||
|
||||
if (!currentElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
itemId: item.id,
|
||||
layoutId: options.current.layoutId,
|
||||
sectionId: currentElement.sectionId,
|
||||
height: currentElement.height,
|
||||
width: currentElement.width,
|
||||
xOffset: currentElement.xOffset,
|
||||
yOffset: currentElement.yOffset,
|
||||
};
|
||||
})
|
||||
.filter((item) => item !== null),
|
||||
);
|
||||
|
||||
sectionLayoutsCollection.push(
|
||||
...board.sections
|
||||
.filter((section) => section.kind === "dynamic")
|
||||
.map((section): InferInsertModel<typeof sectionLayouts> | null => {
|
||||
const currentElement = result.items.find(
|
||||
(element) => element.type === "section" && element.id === section.id,
|
||||
);
|
||||
|
||||
if (!currentElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
layoutId: options.current.layoutId,
|
||||
sectionId: section.id,
|
||||
parentSectionId: currentElement.sectionId,
|
||||
height: currentElement.height,
|
||||
width: currentElement.width,
|
||||
xOffset: currentElement.xOffset,
|
||||
yOffset: currentElement.yOffset,
|
||||
};
|
||||
})
|
||||
.filter((section) => section !== null),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
itemSectionLayouts: itemSectionLayoutsCollection,
|
||||
sectionLayouts: sectionLayoutsCollection,
|
||||
};
|
||||
};
|
||||
|
||||
const getElementsForLayout = (board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>, layoutId: string) => {
|
||||
const sectionElements = board.sections
|
||||
.filter((section) => section.kind === "dynamic")
|
||||
.map((section) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const clonedLayout = section.layouts.find((sectionLayout) => sectionLayout.layoutId === layoutId)!;
|
||||
|
||||
return {
|
||||
id: section.id,
|
||||
type: "section" as const,
|
||||
height: clonedLayout.height,
|
||||
width: clonedLayout.width,
|
||||
xOffset: clonedLayout.xOffset,
|
||||
yOffset: clonedLayout.yOffset,
|
||||
sectionId: clonedLayout.parentSectionId,
|
||||
};
|
||||
});
|
||||
|
||||
const itemElements = board.items.map((item) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const clonedLayout = item.layouts.find((itemLayout) => itemLayout.layoutId === layoutId)!;
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
type: "item" as const,
|
||||
height: clonedLayout.height,
|
||||
width: clonedLayout.width,
|
||||
xOffset: clonedLayout.xOffset,
|
||||
yOffset: clonedLayout.yOffset,
|
||||
sectionId: clonedLayout.sectionId,
|
||||
};
|
||||
});
|
||||
|
||||
return [...itemElements, ...sectionElements];
|
||||
};
|
||||
|
||||
const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, userId: string | null) => {
|
||||
const groupsOfCurrentUser = await db.query.groupMembers.findMany({
|
||||
where: eq(groupMembers.userId, userId ?? ""),
|
||||
});
|
||||
const board = await db.query.boards.findFirst({
|
||||
where,
|
||||
with: {
|
||||
creator: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
},
|
||||
},
|
||||
sections: {
|
||||
with: {
|
||||
collapseStates: {
|
||||
where: eq(sectionCollapseStates.userId, userId ?? ""),
|
||||
},
|
||||
layouts: true,
|
||||
},
|
||||
},
|
||||
items: {
|
||||
with: {
|
||||
integrations: {
|
||||
with: {
|
||||
integration: true,
|
||||
},
|
||||
},
|
||||
layouts: true,
|
||||
},
|
||||
},
|
||||
layouts: true,
|
||||
userPermissions: {
|
||||
where: eq(boardUserPermissions.userId, userId ?? ""),
|
||||
columns: {
|
||||
permission: true,
|
||||
},
|
||||
},
|
||||
groupPermissions: {
|
||||
where: inArray(boardGroupPermissions.groupId, groupsOfCurrentUser.map((group) => group.groupId).concat("")),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!board) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Board not found",
|
||||
});
|
||||
}
|
||||
|
||||
const { sections, items, layouts, ...otherBoardProperties } = board;
|
||||
|
||||
return {
|
||||
...otherBoardProperties,
|
||||
layouts: layouts
|
||||
.map(({ boardId: _, ...layout }) => layout)
|
||||
.sort((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint),
|
||||
sections: sections.map(({ collapseStates, ...section }) =>
|
||||
parseSection({
|
||||
...section,
|
||||
xOffset: section.xOffset,
|
||||
yOffset: section.yOffset,
|
||||
options: superjson.parse(section.options ?? emptySuperJSON),
|
||||
layouts: section.layouts.map((layout) => ({
|
||||
xOffset: layout.xOffset,
|
||||
yOffset: layout.yOffset,
|
||||
width: layout.width,
|
||||
height: layout.height,
|
||||
parentSectionId: layout.parentSectionId,
|
||||
layoutId: layout.layoutId,
|
||||
})),
|
||||
collapsed: collapseStates.at(0)?.collapsed ?? false,
|
||||
}),
|
||||
),
|
||||
items: items.map(({ integrations: itemIntegrations, ...item }) =>
|
||||
parseItem({
|
||||
...item,
|
||||
layouts: item.layouts.map((layout) => ({
|
||||
xOffset: layout.xOffset,
|
||||
yOffset: layout.yOffset,
|
||||
width: layout.width,
|
||||
height: layout.height,
|
||||
layoutId: layout.layoutId,
|
||||
sectionId: layout.sectionId,
|
||||
})),
|
||||
integrationIds: itemIntegrations.map((item) => item.integration.id),
|
||||
advancedOptions: superjson.parse<BoardItemAdvancedOptions>(item.advancedOptions),
|
||||
options: superjson.parse<Record<string, unknown>>(item.options),
|
||||
}),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const forKind = <T extends WidgetKind>(kind: T) =>
|
||||
z.object({
|
||||
kind: z.literal(kind),
|
||||
options: z.record(z.string(), z.unknown()),
|
||||
});
|
||||
|
||||
const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kind))).and(sharedItemSchema);
|
||||
|
||||
const parseItem = (item: unknown) => {
|
||||
const result = outputItemSchema.safeParse(item);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const parseSection = (section: unknown) => {
|
||||
const result = sectionSchema.safeParse(section);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const filterAddedItems = <TInput extends { id: string }>(inputArray: TInput[], dbArray: TInput[]) =>
|
||||
inputArray.filter((inputItem) => !dbArray.some((dbItem) => dbItem.id === inputItem.id));
|
||||
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import type { Modify } from "@homarr/common/types";
|
||||
import type { InferInsertModel } from "@homarr/db";
|
||||
import type { itemLayouts, sectionLayouts } from "@homarr/db/schema";
|
||||
|
||||
import type { getFullBoardWithWhereAsync } from "./full-board-query";
|
||||
import { generateResponsiveGridFor } from "./grid-algorithm";
|
||||
|
||||
type DynamicSectionLayout = Modify<InferInsertModel<typeof sectionLayouts>, { parentSectionId: string }>;
|
||||
|
||||
export const getUpdatedBoardLayout = (
|
||||
board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>,
|
||||
options: {
|
||||
previous: {
|
||||
layoutId: string;
|
||||
columnCount: number;
|
||||
};
|
||||
current: {
|
||||
layoutId: string;
|
||||
columnCount: number;
|
||||
};
|
||||
},
|
||||
) => {
|
||||
const itemSectionLayoutsCollection: InferInsertModel<typeof itemLayouts>[] = [];
|
||||
const sectionLayoutsCollection: DynamicSectionLayout[] = [];
|
||||
|
||||
const elements = getElementsForLayout(board, options.previous.layoutId);
|
||||
const rootSections = board.sections.filter((section) => section.kind !== "dynamic");
|
||||
|
||||
for (const rootSection of rootSections) {
|
||||
const result = generateResponsiveGridFor({
|
||||
items: elements,
|
||||
previousWidth: options.previous.columnCount,
|
||||
width: options.current.columnCount,
|
||||
sectionId: rootSection.id,
|
||||
});
|
||||
|
||||
itemSectionLayoutsCollection.push(
|
||||
...board.items
|
||||
.map((item): InferInsertModel<typeof itemLayouts> | null => {
|
||||
const currentElement = result.items.find((element) => element.type === "item" && element.id === item.id);
|
||||
|
||||
if (!currentElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
itemId: item.id,
|
||||
layoutId: options.current.layoutId,
|
||||
sectionId: currentElement.sectionId,
|
||||
height: currentElement.height,
|
||||
width: currentElement.width,
|
||||
xOffset: currentElement.xOffset,
|
||||
yOffset: currentElement.yOffset,
|
||||
};
|
||||
})
|
||||
.filter((item) => item !== null),
|
||||
);
|
||||
|
||||
sectionLayoutsCollection.push(
|
||||
...board.sections
|
||||
.filter((section) => section.kind === "dynamic")
|
||||
.map((section): DynamicSectionLayout | null => {
|
||||
const currentElement = result.items.find(
|
||||
(element) => element.type === "section" && element.id === section.id,
|
||||
);
|
||||
|
||||
if (!currentElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
layoutId: options.current.layoutId,
|
||||
sectionId: section.id,
|
||||
parentSectionId: currentElement.sectionId,
|
||||
height: currentElement.height,
|
||||
width: currentElement.width,
|
||||
xOffset: currentElement.xOffset,
|
||||
yOffset: currentElement.yOffset,
|
||||
};
|
||||
})
|
||||
.filter((section) => section !== null),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
itemSectionLayouts: itemSectionLayoutsCollection,
|
||||
sectionLayouts: sectionLayoutsCollection,
|
||||
};
|
||||
};
|
||||
|
||||
const getElementsForLayout = (board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>, layoutId: string) => {
|
||||
const sectionElements = board.sections
|
||||
.filter((section) => section.kind === "dynamic")
|
||||
.map((section) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const clonedLayout = section.layouts.find((sectionLayout) => sectionLayout.layoutId === layoutId)!;
|
||||
|
||||
return {
|
||||
id: section.id,
|
||||
type: "section" as const,
|
||||
height: clonedLayout.height,
|
||||
width: clonedLayout.width,
|
||||
xOffset: clonedLayout.xOffset,
|
||||
yOffset: clonedLayout.yOffset,
|
||||
sectionId: clonedLayout.parentSectionId,
|
||||
};
|
||||
});
|
||||
|
||||
const itemElements = board.items.map((item) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const clonedLayout = item.layouts.find((itemLayout) => itemLayout.layoutId === layoutId)!;
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
type: "item" as const,
|
||||
height: clonedLayout.height,
|
||||
width: clonedLayout.width,
|
||||
xOffset: clonedLayout.xOffset,
|
||||
yOffset: clonedLayout.yOffset,
|
||||
sectionId: clonedLayout.sectionId,
|
||||
};
|
||||
});
|
||||
|
||||
return [...itemElements, ...sectionElements];
|
||||
};
|
||||
|
||||
export const applyAutoLayoutToBoard = (
|
||||
board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>,
|
||||
): Awaited<ReturnType<typeof getFullBoardWithWhereAsync>> => {
|
||||
if (board.layoutMode === "custom") return board;
|
||||
|
||||
const baseLayout = board.layouts.find((layout) => layout.id === board.baseLayoutId) ?? null;
|
||||
if (!baseLayout) return board;
|
||||
|
||||
const updatedLayoutMap = new Map<string, ReturnType<typeof getUpdatedBoardLayout>>();
|
||||
for (const layout of board.layouts) {
|
||||
if (layout.id === baseLayout.id) continue;
|
||||
|
||||
updatedLayoutMap.set(
|
||||
layout.id,
|
||||
getUpdatedBoardLayout(board, {
|
||||
previous: {
|
||||
layoutId: baseLayout.id,
|
||||
columnCount: baseLayout.columnCount,
|
||||
},
|
||||
current: {
|
||||
layoutId: layout.id,
|
||||
columnCount: layout.columnCount,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...board,
|
||||
items: board.items.map((item) => ({
|
||||
...item,
|
||||
layouts: item.layouts.map(
|
||||
(layoutSection) =>
|
||||
updatedLayoutMap
|
||||
.get(layoutSection.layoutId)
|
||||
?.itemSectionLayouts.find(
|
||||
(updatedLayoutSection) =>
|
||||
updatedLayoutSection.itemId === item.id && updatedLayoutSection.layoutId === layoutSection.layoutId,
|
||||
) ?? layoutSection,
|
||||
),
|
||||
})),
|
||||
sections: board.sections.map((section) =>
|
||||
"layouts" in section
|
||||
? {
|
||||
...section,
|
||||
layouts: section.layouts.map(
|
||||
(sectionLayout) =>
|
||||
updatedLayoutMap
|
||||
.get(sectionLayout.layoutId)
|
||||
?.sectionLayouts.find(
|
||||
(updatedSectionLayout) =>
|
||||
updatedSectionLayout.sectionId === section.id &&
|
||||
updatedSectionLayout.layoutId === sectionLayout.layoutId,
|
||||
) ?? sectionLayout,
|
||||
),
|
||||
}
|
||||
: section,
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
import z from "zod";
|
||||
|
||||
import type { Database, SQL } from "@homarr/db";
|
||||
import { eq, inArray } from "@homarr/db";
|
||||
import { boardGroupPermissions, boardUserPermissions, groupMembers, sectionCollapseStates } from "@homarr/db/schema";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { emptySuperJSON, widgetKinds } from "@homarr/definitions";
|
||||
import { zodUnionFromArray } from "@homarr/validation/enums";
|
||||
import { sectionSchema, sharedItemSchema } from "@homarr/validation/shared";
|
||||
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
|
||||
|
||||
export const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, userId: string | null) => {
|
||||
const groupsOfCurrentUser = await db.query.groupMembers.findMany({
|
||||
where: eq(groupMembers.userId, userId ?? ""),
|
||||
});
|
||||
const board = await db.query.boards.findFirst({
|
||||
where,
|
||||
with: {
|
||||
creator: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
image: true,
|
||||
},
|
||||
},
|
||||
sections: {
|
||||
with: {
|
||||
collapseStates: {
|
||||
where: eq(sectionCollapseStates.userId, userId ?? ""),
|
||||
},
|
||||
layouts: true,
|
||||
},
|
||||
},
|
||||
items: {
|
||||
with: {
|
||||
integrations: {
|
||||
with: {
|
||||
integration: true,
|
||||
},
|
||||
},
|
||||
layouts: true,
|
||||
},
|
||||
},
|
||||
layouts: true,
|
||||
userPermissions: {
|
||||
where: eq(boardUserPermissions.userId, userId ?? ""),
|
||||
columns: {
|
||||
permission: true,
|
||||
},
|
||||
},
|
||||
groupPermissions: {
|
||||
where: inArray(boardGroupPermissions.groupId, groupsOfCurrentUser.map((group) => group.groupId).concat("")),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!board) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Board not found",
|
||||
});
|
||||
}
|
||||
|
||||
const { sections, items, layouts, ...otherBoardProperties } = board;
|
||||
|
||||
return {
|
||||
...otherBoardProperties,
|
||||
layouts: layouts
|
||||
.map(({ boardId: _, ...layout }) => layout)
|
||||
.sort((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint),
|
||||
sections: sections.map(({ collapseStates, ...section }) =>
|
||||
parseSection({
|
||||
...section,
|
||||
xOffset: section.xOffset,
|
||||
yOffset: section.yOffset,
|
||||
options: superjson.parse(section.options ?? emptySuperJSON),
|
||||
layouts: section.layouts.map((layout) => ({
|
||||
xOffset: layout.xOffset,
|
||||
yOffset: layout.yOffset,
|
||||
width: layout.width,
|
||||
height: layout.height,
|
||||
parentSectionId: layout.parentSectionId,
|
||||
layoutId: layout.layoutId,
|
||||
})),
|
||||
collapsed: collapseStates.at(0)?.collapsed ?? false,
|
||||
}),
|
||||
),
|
||||
items: items.map(({ integrations: itemIntegrations, ...item }) =>
|
||||
parseItem({
|
||||
...item,
|
||||
layouts: item.layouts.map((layout) => ({
|
||||
xOffset: layout.xOffset,
|
||||
yOffset: layout.yOffset,
|
||||
width: layout.width,
|
||||
height: layout.height,
|
||||
layoutId: layout.layoutId,
|
||||
sectionId: layout.sectionId,
|
||||
})),
|
||||
integrationIds: itemIntegrations.map((item) => item.integration.id),
|
||||
advancedOptions: superjson.parse<BoardItemAdvancedOptions>(item.advancedOptions),
|
||||
options: superjson.parse<Record<string, unknown>>(item.options),
|
||||
}),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const forKind = <T extends WidgetKind>(kind: T) =>
|
||||
z.object({
|
||||
kind: z.literal(kind),
|
||||
options: z.record(z.string(), z.unknown()),
|
||||
});
|
||||
|
||||
const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kind))).and(sharedItemSchema);
|
||||
|
||||
const parseItem = (item: unknown) => {
|
||||
const result = outputItemSchema.safeParse(item);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
return result.data;
|
||||
};
|
||||
|
||||
const parseSection = (section: unknown) => {
|
||||
const result = sectionSchema.safeParse(section);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
return result.data;
|
||||
};
|
||||
@@ -56,8 +56,7 @@ export const generateResponsiveGridFor = ({
|
||||
dynamicSectionHeightMap.set(dynamicSection.id, result.height);
|
||||
}
|
||||
|
||||
// Return same positions for items in the current section
|
||||
if (width >= previousWidth) {
|
||||
if (width === previousWidth) {
|
||||
return {
|
||||
height: Math.max(...itemsOfCurrentSection.map((item) => item.yOffset + item.height)),
|
||||
items: newItems.concat(normalizedItems),
|
||||
|
||||
@@ -99,11 +99,12 @@ qlllmmmmmnnrrrst
|
||||
ulllmmmmmnnrrrvw
|
||||
xyz äö`;
|
||||
|
||||
// Just add two empty columns to the right
|
||||
const eighteenColumns = sixteenColumns
|
||||
.split("\n")
|
||||
.map((line, index) => (index === 0 ? line : `${line} `))
|
||||
.join("\n");
|
||||
const eighteenColumns = `
|
||||
abbccccddddeeefghi
|
||||
jbbccccddddeeeklll
|
||||
mmmmmnnopqseeetlll
|
||||
mmmmmnnrrruvwxylll
|
||||
mmmmmnnrrrzäö `;
|
||||
|
||||
const tenColumns = `
|
||||
abbcccceee
|
||||
|
||||
@@ -4,15 +4,13 @@ import { zfd } from "zod-form-data";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { and, eq } from "@homarr/db";
|
||||
import { trustedCertificateHostnames } from "@homarr/db/schema";
|
||||
import { logger } from "@homarr/log";
|
||||
import { certificateValidFileNameSchema, checkCertificateFile } from "@homarr/validation/certificates";
|
||||
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
||||
|
||||
const logger = createLogger({ module: "certificateRouter" });
|
||||
|
||||
export const certificateRouter = createTRPCRouter({
|
||||
addCertificate: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import z from "zod/v4";
|
||||
|
||||
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 { createCronJobStatusChannel } from "@homarr/cron-job-status";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
||||
|
||||
const logger = createLogger({ module: "cronJobsRouter" });
|
||||
|
||||
export const cronJobsRouter = createTRPCRouter({
|
||||
triggerJob: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
|
||||
@@ -3,7 +3,6 @@ import { z } from "zod/v4";
|
||||
|
||||
import { createId, objectEntries } from "@homarr/common";
|
||||
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, asc, eq, handleTransactionsAsync, inArray, like, or } from "@homarr/db";
|
||||
import {
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
integrationSecretKindObject,
|
||||
} from "@homarr/definitions";
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import { logger } from "@homarr/log";
|
||||
import { byIdSchema } from "@homarr/validation/common";
|
||||
import {
|
||||
integrationCreateSchema,
|
||||
@@ -40,8 +40,6 @@ import { throwIfActionForbiddenAsync } from "./integration-access";
|
||||
import { MissingSecretError, testConnectionAsync } from "./integration-test-connection";
|
||||
import { mapTestConnectionError } from "./map-test-connection-error";
|
||||
|
||||
const logger = createLogger({ module: "integrationRouter" });
|
||||
|
||||
export const integrationRouter = createTRPCRouter({
|
||||
all: publicProcedure.query(async ({ ctx }) => {
|
||||
const groupsOfCurrentUser = await ctx.db.query.groupMembers.findMany({
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { decryptSecret } from "@homarr/common/server";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||
import type { Integration } from "@homarr/db/schema";
|
||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions } from "@homarr/definitions";
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
|
||||
const logger = createLogger({ module: "integrationTestConnection" });
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
type FormIntegration = Omit<Integration, "appId"> & {
|
||||
secrets: {
|
||||
@@ -38,13 +35,8 @@ export const testConnectionAsync = async (
|
||||
};
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
new ErrorWithMetadata(
|
||||
"Failed to decrypt secret from database",
|
||||
{
|
||||
integrationName: integration.name,
|
||||
integrationKind: integration.kind,
|
||||
secretKind: secret.kind,
|
||||
},
|
||||
new Error(
|
||||
`Failed to decrypt secret from database integration="${integration.name}" secretKind="${secret.kind}"`,
|
||||
{ cause: error },
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { V1NodeList, VersionInfo } from "@kubernetes/client-node";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { ClusterResourceCount, KubernetesCluster } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||
@@ -128,6 +129,7 @@ export const clusterRouter = createTRPCRouter({
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Unable to retrieve cluster", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching Kubernetes cluster",
|
||||
@@ -163,6 +165,7 @@ export const clusterRouter = createTRPCRouter({
|
||||
{ label: "volumes", count: volumes.items.length },
|
||||
];
|
||||
} catch (error) {
|
||||
logger.error("Unable to retrieve cluster resource counts", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching Kubernetes resources count",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { KubernetesBaseResource } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||
@@ -24,6 +25,7 @@ export const configMapsRouter = createTRPCRouter({
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Unable to retrieve configMaps", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching Kubernetes ConfigMaps",
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { V1HTTPIngressPath, V1Ingress, V1IngressRule } from "@kubernetes/cl
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { KubernetesIngress, KubernetesIngressPath, KubernetesIngressRuleAndPath } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||
@@ -42,6 +43,7 @@ export const ingressesRouter = createTRPCRouter({
|
||||
|
||||
return ingresses.items.map(mapIngress);
|
||||
} catch (error) {
|
||||
logger.error("Unable to retrieve ingresses", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching Kubernetes ingresses",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { KubernetesNamespace, KubernetesNamespaceState } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||
@@ -24,6 +25,7 @@ export const namespacesRouter = createTRPCRouter({
|
||||
} satisfies KubernetesNamespace;
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Unable to retrieve namespaces", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching Kubernetes namespaces",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { KubernetesNode, KubernetesNodeState } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||
@@ -56,6 +57,7 @@ export const nodesRouter = createTRPCRouter({
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Unable to retrieve nodes", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching Kubernetes nodes",
|
||||
|
||||
@@ -2,15 +2,13 @@ import type { KubeConfig, V1OwnerReference } from "@kubernetes/client-node";
|
||||
import { AppsV1Api } from "@kubernetes/client-node";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import type { KubernetesPod } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||
import { KubernetesClient } from "../kubernetes-client";
|
||||
|
||||
const logger = createLogger({ module: "podsRouter" });
|
||||
|
||||
export const podsRouter = createTRPCRouter({
|
||||
getPods: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
@@ -57,6 +55,7 @@ export const podsRouter = createTRPCRouter({
|
||||
|
||||
return pods;
|
||||
} catch (error) {
|
||||
logger.error("Unable to retrieve pods", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching Kubernetes pods",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { KubernetesSecret } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||
@@ -24,6 +25,7 @@ export const secretsRouter = createTRPCRouter({
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Unable to retrieve secrets", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching Kubernetes secrets",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { KubernetesService } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||
@@ -28,6 +29,7 @@ export const servicesRouter = createTRPCRouter({
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Unable to retrieve services", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching Kubernetes services",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { KubernetesVolume } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { kubernetesMiddleware } from "../../../middlewares/kubernetes";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc";
|
||||
@@ -30,6 +31,7 @@ export const volumesRouter = createTRPCRouter({
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Unable to retrieve volumes", error);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An error occurred while fetching Kubernetes Volumes",
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import z from "zod/v4";
|
||||
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { logLevels } from "@homarr/core/infrastructure/logs/constants";
|
||||
import { logger } from "@homarr/log";
|
||||
import { logLevels } from "@homarr/log/constants";
|
||||
import type { LoggerMessage } from "@homarr/redis";
|
||||
import { loggingChannel } from "@homarr/redis";
|
||||
import { zodEnumFromArray } from "@homarr/validation/enums";
|
||||
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
||||
|
||||
const logger = createLogger({ module: "logRouter" });
|
||||
|
||||
export const logRouter = createTRPCRouter({
|
||||
subscribe: permissionRequiredProcedure
|
||||
.requiresPermission("other-view-logs")
|
||||
|
||||
@@ -2,11 +2,11 @@ import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { createId } from "@homarr/common";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { asc, eq, like } from "@homarr/db";
|
||||
import { getServerSettingByKeyAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||
import { searchEngines, users } from "@homarr/db/schema";
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import { logger } from "@homarr/log";
|
||||
import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common";
|
||||
import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine";
|
||||
import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
|
||||
@@ -14,8 +14,6 @@ import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/va
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
||||
|
||||
const logger = createLogger({ module: "searchEngineRouter" });
|
||||
|
||||
export const searchEngineRouter = createTRPCRouter({
|
||||
getPaginated: protectedProcedure.input(paginatedSchema).query(async ({ input, ctx }) => {
|
||||
const whereQuery = input.search ? like(searchEngines.name, `%${input.search.trim()}%`) : undefined;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { logger } from "@homarr/log";
|
||||
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
|
||||
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
|
||||
|
||||
const logger = createLogger({ module: "updateCheckerRouter" });
|
||||
|
||||
export const updateCheckerRouter = createTRPCRouter({
|
||||
getAvailableUpdates: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
|
||||
try {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { z } from "zod/v4";
|
||||
|
||||
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
|
||||
import { createId } from "@homarr/common";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, eq, like } from "@homarr/db";
|
||||
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
|
||||
@@ -11,6 +10,7 @@ import { boards, groupMembers, groupPermissions, groups, invites, users } from "
|
||||
import { selectUserSchema } from "@homarr/db/validationSchemas";
|
||||
import { credentialsAdminGroup } from "@homarr/definitions";
|
||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
import { byIdSchema } from "@homarr/validation/common";
|
||||
import type { userBaseCreateSchema } from "@homarr/validation/user";
|
||||
import {
|
||||
@@ -39,8 +39,6 @@ import { throwIfCredentialsDisabled } from "./invite/checks";
|
||||
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
||||
import { changeSearchPreferencesAsync, changeSearchPreferencesInputSchema } from "./user/change-search-preferences";
|
||||
|
||||
const logger = createLogger({ module: "userRouter" });
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
initUser: onboardingProcedure
|
||||
.requiresStep("user")
|
||||
@@ -366,11 +364,9 @@ export const userRouter = createTRPCRouter({
|
||||
// Admins can change the password of other users without providing the previous password
|
||||
const isPreviousPasswordRequired = ctx.session.user.id === input.userId;
|
||||
|
||||
logger.info("Changing user password", {
|
||||
actorId: ctx.session.user.id,
|
||||
targetUserId: input.userId,
|
||||
previousPasswordRequired: isPreviousPasswordRequired,
|
||||
});
|
||||
logger.info(
|
||||
`User ${user.id} is changing password for user ${input.userId}, previous password is required: ${isPreviousPasswordRequired}`,
|
||||
);
|
||||
|
||||
if (isPreviousPasswordRequired) {
|
||||
const previousPasswordHash = await hashPasswordAsync(input.previousPassword, dbUser.salt ?? "");
|
||||
|
||||
@@ -4,6 +4,7 @@ import { observable } from "@trpc/server/observable";
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import type { Indexer } from "@homarr/integrations/types";
|
||||
import { logger } from "@homarr/log";
|
||||
import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-manager";
|
||||
|
||||
import type { IntegrationAction } from "../../middlewares/integration";
|
||||
@@ -60,10 +61,10 @@ export const indexerManagerRouter = createTRPCRouter({
|
||||
ctx.integrations.map(async (integration) => {
|
||||
const client = await createIntegrationAsync(integration);
|
||||
await client.testAllAsync().catch((err) => {
|
||||
logger.error("indexer-manager router - ", err);
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Failed to test all indexers for ${integration.name} (${integration.id})`,
|
||||
cause: err,
|
||||
});
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -14,14 +14,12 @@ import { ZodError } from "zod/v4";
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { FlattenError } from "@homarr/common";
|
||||
import { userAgent } from "@homarr/common/server";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { db } from "@homarr/db";
|
||||
import type { GroupPermissionKey, OnboardingStep } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { getOnboardingOrFallbackAsync } from "./router/onboard/onboard-queries";
|
||||
|
||||
const logger = createLogger({ module: "trpc" });
|
||||
|
||||
/**
|
||||
* 1. CONTEXT
|
||||
*
|
||||
@@ -38,7 +36,7 @@ export const createTRPCContext = (opts: { headers: Headers; session: Session | n
|
||||
const session = opts.session;
|
||||
const source = opts.headers.get("x-trpc-source") ?? "unknown";
|
||||
|
||||
logger.info("Received tRPC request", { source, userId: session?.user.id, userName: session?.user.name });
|
||||
logger.info(`tRPC request from ${source} by user '${session?.user.name} (${session?.user.id})'`, session?.user);
|
||||
|
||||
return {
|
||||
session,
|
||||
|
||||
@@ -3,9 +3,9 @@ import { cookies } from "next/headers";
|
||||
import NextAuth from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { db } from "@homarr/db";
|
||||
import type { SupportedAuthProvider } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { createAdapter } from "./adapter";
|
||||
import { createSessionCallback } from "./callbacks";
|
||||
@@ -18,8 +18,6 @@ import { OidcProvider } from "./providers/oidc/oidc-provider";
|
||||
import { createRedirectUri } from "./redirect";
|
||||
import { expireDateAfter, generateSessionToken, sessionTokenCookieName } from "./session";
|
||||
|
||||
const logger = createLogger({ module: "authConfiguration" });
|
||||
|
||||
// See why it's unknown in the [...nextauth]/route.ts file
|
||||
export const createConfiguration = (
|
||||
provider: SupportedAuthProvider | "unknown",
|
||||
|
||||
+22
-36
@@ -2,17 +2,15 @@ import { cookies } from "next/headers";
|
||||
import dayjs from "dayjs";
|
||||
import type { NextAuthConfig } from "next-auth";
|
||||
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { and, eq, inArray } from "@homarr/db";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { groupMembers, groups, users } from "@homarr/db/schema";
|
||||
import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { env } from "./env";
|
||||
import { extractProfileName } from "./providers/oidc/oidc-provider";
|
||||
|
||||
const logger = createLogger({ module: "authEvents" });
|
||||
|
||||
export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["events"], undefined>["signIn"] => {
|
||||
return async ({ user, profile }) => {
|
||||
logger.debug(`SignIn EventHandler for user: ${JSON.stringify(user)} . profile: ${JSON.stringify(profile)}`);
|
||||
@@ -45,11 +43,9 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
|
||||
|
||||
if (dbUser.name !== user.name) {
|
||||
await db.update(users).set({ name: user.name }).where(eq(users.id, user.id));
|
||||
logger.info("Username for user of credentials provider has changed.", {
|
||||
userId: user.id,
|
||||
oldName: dbUser.name,
|
||||
newName: user.name,
|
||||
});
|
||||
logger.info(
|
||||
`Username for user of credentials provider has changed. user=${user.id} old=${dbUser.name} new=${user.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (profile) {
|
||||
@@ -60,11 +56,9 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
|
||||
|
||||
if (dbUser.name !== profileUsername) {
|
||||
await db.update(users).set({ name: profileUsername }).where(eq(users.id, user.id));
|
||||
logger.info("Username for user of oidc provider has changed.", {
|
||||
userId: user.id,
|
||||
oldName: dbUser.name,
|
||||
newName: profileUsername,
|
||||
});
|
||||
logger.info(
|
||||
`Username for user of oidc provider has changed. user=${user.id} old='${dbUser.name}' new='${profileUsername}'`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -73,13 +67,11 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
|
||||
!dbUser.image?.startsWith("data:")
|
||||
) {
|
||||
await db.update(users).set({ image: profile.picture }).where(eq(users.id, user.id));
|
||||
logger.info("Profile picture for user of oidc provider has changed.", {
|
||||
userId: user.id,
|
||||
});
|
||||
logger.info(`Profile picture for user of oidc provider has changed. user=${user.id}'`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("User logged in", { userId: user.id, userName: dbUser.name, timestamp: dayjs().format() });
|
||||
logger.info(`User '${dbUser.name}' logged in at ${dayjs().format()}`);
|
||||
|
||||
// We use a cookie as localStorage is not shared with server (otherwise flickering would occur)
|
||||
(await cookies()).set(colorSchemeCookieKey, dbUser.colorScheme, {
|
||||
@@ -104,7 +96,7 @@ const addUserToEveryoneGroupIfNotMemberAsync = async (db: Database, userId: stri
|
||||
userId,
|
||||
groupId: dbEveryoneGroup.id,
|
||||
});
|
||||
logger.info("Added user to everyone group.", { userId });
|
||||
logger.info(`Added user to everyone group. user=${userId}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,10 +118,9 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s
|
||||
);
|
||||
|
||||
if (missingExternalGroupsForUser.length > 0) {
|
||||
logger.debug("Homarr does not have the user in certain groups.", {
|
||||
user: userId,
|
||||
count: missingExternalGroupsForUser.length,
|
||||
});
|
||||
logger.debug(
|
||||
`Homarr does not have the user in certain groups. user=${userId} count=${missingExternalGroupsForUser.length}`,
|
||||
);
|
||||
|
||||
const groupIds = await db.query.groups.findMany({
|
||||
columns: {
|
||||
@@ -138,10 +129,7 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s
|
||||
where: inArray(groups.name, missingExternalGroupsForUser),
|
||||
});
|
||||
|
||||
logger.debug("Homarr has found groups in the database user is not in.", {
|
||||
user: userId,
|
||||
count: groupIds.length,
|
||||
});
|
||||
logger.debug(`Homarr has found groups in the database user is not in. user=${userId} count=${groupIds.length}`);
|
||||
|
||||
if (groupIds.length > 0) {
|
||||
await db.insert(groupMembers).values(
|
||||
@@ -151,9 +139,9 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s
|
||||
})),
|
||||
);
|
||||
|
||||
logger.info("Added user to groups successfully.", { user: userId, count: groupIds.length });
|
||||
logger.info(`Added user to groups successfully. user=${userId} count=${groupIds.length}`);
|
||||
} else {
|
||||
logger.debug("User is already in all groups of Homarr.", { user: userId });
|
||||
logger.debug(`User is already in all groups of Homarr. user=${userId}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,10 +154,9 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s
|
||||
);
|
||||
|
||||
if (groupsUserIsNoLongerMemberOfExternally.length > 0) {
|
||||
logger.debug("Homarr has the user in certain groups that LDAP does not have.", {
|
||||
user: userId,
|
||||
count: groupsUserIsNoLongerMemberOfExternally.length,
|
||||
});
|
||||
logger.debug(
|
||||
`Homarr has the user in certain groups that LDAP does not have. user=${userId} count=${groupsUserIsNoLongerMemberOfExternally.length}`,
|
||||
);
|
||||
|
||||
await db.delete(groupMembers).where(
|
||||
and(
|
||||
@@ -181,9 +168,8 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s
|
||||
),
|
||||
);
|
||||
|
||||
logger.info("Removed user from groups successfully.", {
|
||||
user: userId,
|
||||
count: groupsUserIsNoLongerMemberOfExternally.length,
|
||||
});
|
||||
logger.info(
|
||||
`Removed user from groups successfully. user=${userId} count=${groupsUserIsNoLongerMemberOfExternally.length}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -30,15 +30,16 @@
|
||||
"@homarr/core": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"cookies": "^0.9.1",
|
||||
"ldapts": "8.0.23",
|
||||
"next": "16.0.10",
|
||||
"ldapts": "8.0.9",
|
||||
"next": "16.0.3",
|
||||
"next-auth": "5.0.0-beta.30",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"zod": "^4.1.13"
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
@@ -46,8 +47,8 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/bcrypt": "6.0.0",
|
||||
"@types/cookies": "0.9.2",
|
||||
"eslint": "^9.39.2",
|
||||
"prettier": "^3.7.4",
|
||||
"eslint": "^9.39.1",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, eq } from "@homarr/db";
|
||||
import { users } from "@homarr/db/schema";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { userSignInSchema } from "@homarr/validation/user";
|
||||
|
||||
const logger = createLogger({ module: "basicAuthorization" });
|
||||
|
||||
export const authorizeWithBasicCredentialsAsync = async (
|
||||
db: Database,
|
||||
credentials: z.infer<typeof userSignInSchema>,
|
||||
@@ -18,19 +16,19 @@ export const authorizeWithBasicCredentialsAsync = async (
|
||||
});
|
||||
|
||||
if (!user?.password) {
|
||||
logger.info("User not found", { userName: credentials.name });
|
||||
logger.info(`user ${credentials.name} was not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info("User is trying to log in. Checking password...", { userName: user.name });
|
||||
logger.info(`user ${user.name} is trying to log in. checking password...`);
|
||||
const isValidPassword = await bcrypt.compare(credentials.password, user.password);
|
||||
|
||||
if (!isValidPassword) {
|
||||
logger.warn("Password for user was incorrect", { userName: user.name });
|
||||
logger.warn(`password for user ${user.name} was incorrect`);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info("User successfully authorized", { userName: user.name });
|
||||
logger.info(`user ${user.name} successfully authorized`);
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import { CredentialsSignin } from "@auth/core/errors";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { createId } from "@homarr/common";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { createId, extractErrorMessage } from "@homarr/common";
|
||||
import type { Database, InferInsertModel } from "@homarr/db";
|
||||
import { and, eq } from "@homarr/db";
|
||||
import { users } from "@homarr/db/schema";
|
||||
import type { ldapSignInSchema } from "@homarr/validation/user";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { userSignInSchema } from "@homarr/validation/user";
|
||||
|
||||
import { env } from "../../../env";
|
||||
import { LdapClient } from "../ldap-client";
|
||||
|
||||
const logger = createLogger({ module: "ldapAuthorization" });
|
||||
|
||||
export const authorizeWithLdapCredentialsAsync = async (
|
||||
db: Database,
|
||||
credentials: z.infer<typeof ldapSignInSchema>,
|
||||
credentials: z.infer<typeof userSignInSchema>,
|
||||
) => {
|
||||
logger.info("User is trying to log in using LDAP. Connecting to LDAP server...", { userName: credentials.name });
|
||||
logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`);
|
||||
const client = new LdapClient();
|
||||
await client
|
||||
.bindAsync({
|
||||
@@ -25,7 +23,8 @@ export const authorizeWithLdapCredentialsAsync = async (
|
||||
password: env.AUTH_LDAP_BIND_PASSWORD,
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new CredentialsSignin("Failed to connect to LDAP server", { cause: error });
|
||||
logger.error(`Failed to connect to LDAP server ${extractErrorMessage(error)}`);
|
||||
throw new CredentialsSignin();
|
||||
});
|
||||
|
||||
logger.info("Connected to LDAP server. Searching for user...");
|
||||
@@ -39,31 +38,24 @@ export const authorizeWithLdapCredentialsAsync = async (
|
||||
attributes: [env.AUTH_LDAP_USERNAME_ATTRIBUTE, env.AUTH_LDAP_USER_MAIL_ATTRIBUTE],
|
||||
},
|
||||
})
|
||||
.then((entries) => {
|
||||
if (entries.length > 1) {
|
||||
logger.warn(`Multiple LDAP users found for ${credentials.name}, expected only one.`);
|
||||
throw new CredentialsSignin();
|
||||
}
|
||||
|
||||
return entries.at(0);
|
||||
});
|
||||
.then((entries) => entries.at(0));
|
||||
|
||||
if (!ldapUser) {
|
||||
throw new CredentialsSignin(`User not found in LDAP username="${credentials.name}"`);
|
||||
logger.warn(`User ${credentials.name} not found in LDAP`);
|
||||
throw new CredentialsSignin();
|
||||
}
|
||||
|
||||
// Validate email
|
||||
const mailResult = await z.string().email().safeParseAsync(ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE]);
|
||||
|
||||
if (!mailResult.success) {
|
||||
logger.error("User found in LDAP but with invalid or non-existing Email", {
|
||||
userName: credentials.name,
|
||||
emailValue: ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE],
|
||||
});
|
||||
throw new CredentialsSignin("User found in LDAP but with invalid or non-existing Email");
|
||||
logger.error(
|
||||
`User ${credentials.name} found but with invalid or non-existing Email. Not Supported: "${ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE]}"`,
|
||||
);
|
||||
throw new CredentialsSignin();
|
||||
}
|
||||
|
||||
logger.info("User found in LDAP. Logging in...", { userName: credentials.name });
|
||||
logger.info(`User ${credentials.name} found in LDAP. Logging in...`);
|
||||
|
||||
// Bind with user credentials to check if the password is correct
|
||||
const userClient = new LdapClient();
|
||||
@@ -73,12 +65,12 @@ export const authorizeWithLdapCredentialsAsync = async (
|
||||
password: credentials.password,
|
||||
})
|
||||
.catch(() => {
|
||||
logger.warn("Wrong credentials for user", { userName: credentials.name });
|
||||
logger.warn(`Wrong credentials for user ${credentials.name}`);
|
||||
throw new CredentialsSignin();
|
||||
});
|
||||
await userClient.disconnectAsync();
|
||||
|
||||
logger.info("User credentials are correct. Retrieving user groups...", { userName: credentials.name });
|
||||
logger.info(`User ${credentials.name} logged in successfully, retrieving user groups...`);
|
||||
|
||||
const userGroups = await client
|
||||
.searchAsync({
|
||||
@@ -94,7 +86,7 @@ export const authorizeWithLdapCredentialsAsync = async (
|
||||
})
|
||||
.then((entries) => entries.map((entry) => entry.cn).filter((group): group is string => group !== undefined));
|
||||
|
||||
logger.info("User groups retrieved", { userName: credentials.name, groups: userGroups.length });
|
||||
logger.info(`Found ${userGroups.length} groups for user ${credentials.name}.`);
|
||||
|
||||
await client.disconnectAsync();
|
||||
|
||||
@@ -112,7 +104,7 @@ export const authorizeWithLdapCredentialsAsync = async (
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
logger.info("User not found in the database. Creating...", { userName: credentials.name });
|
||||
logger.info(`User ${credentials.name} not found in the database. Creating...`);
|
||||
|
||||
const insertUser = {
|
||||
id: createId(),
|
||||
@@ -127,7 +119,7 @@ export const authorizeWithLdapCredentialsAsync = async (
|
||||
|
||||
user = insertUser;
|
||||
|
||||
logger.info("User created successfully", { userName: credentials.name });
|
||||
logger.info(`User ${credentials.name} created successfully.`);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type Credentials from "@auth/core/providers/credentials";
|
||||
|
||||
import type { Database } from "@homarr/db";
|
||||
import { ldapSignInSchema, userSignInSchema } from "@homarr/validation/user";
|
||||
import { userSignInSchema } from "@homarr/validation/user";
|
||||
|
||||
import { authorizeWithBasicCredentialsAsync } from "./authorization/basic-authorization";
|
||||
import { authorizeWithLdapCredentialsAsync } from "./authorization/ldap-authorization";
|
||||
@@ -28,7 +28,7 @@ export const createLdapConfiguration = (db: Database) =>
|
||||
name: "Ldap",
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
async authorize(credentials) {
|
||||
const data = await ldapSignInSchema.parseAsync(credentials);
|
||||
const data = await userSignInSchema.parseAsync(credentials);
|
||||
return await authorizeWithLdapCredentialsAsync(db, data).catch(() => null);
|
||||
},
|
||||
}) satisfies CredentialsConfiguration;
|
||||
@@ -25,14 +25,14 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/api": "workspace:^0.1.0",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { usePathname } from "next/navigation";
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
|
||||
import { useEditMode } from "./edit-mode";
|
||||
import { updateBoardName } from "./updater";
|
||||
|
||||
const BoardContext = createContext<{
|
||||
@@ -83,10 +84,12 @@ export const getCurrentLayout = (board: RouterOutputs["board"]["getBoardByName"]
|
||||
export const useCurrentLayout = () => {
|
||||
const board = useRequiredBoard();
|
||||
const [currentLayout, setCurrentLayout] = useState(getCurrentLayout(board));
|
||||
const [isEditMode] = useEditMode();
|
||||
|
||||
const onResize = useCallback(() => {
|
||||
if (isEditMode && board.layoutMode === "auto") return;
|
||||
setCurrentLayout(getCurrentLayout(board));
|
||||
}, [board]);
|
||||
}, [board, isEditMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
@@ -97,6 +100,10 @@ export const useCurrentLayout = () => {
|
||||
};
|
||||
}, [onResize]);
|
||||
|
||||
if (board.layoutMode === "auto" && isEditMode && board.baseLayoutId !== null) {
|
||||
return board.baseLayoutId;
|
||||
}
|
||||
|
||||
return currentLayout;
|
||||
};
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint": "^9.39.1",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
Loaded 100 of 322 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user