Compare commits

..
Author SHA1 Message Date
Releases Homarr b0e0b1d43f chore(release): 1.46.0 [skip ci] 2025-12-12 19:17:52 +00:00
homarr-releases[bot] cc403b89eb chore(release): automatic release v1.46.0 2025-12-12 19:16:55 +00:00
Meier Lukas dd24925d90 feat(docker): add widget options to sort containers (#4636) 2025-12-12 19:59:28 +01:00
ManuelandMeier Lukas 984b29d951 ci: publish build archives to release for bare metal installs (#4535)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-12-12 19:51:19 +01:00
Meier Lukas 2b2c0e7e3a fix(deps): update next monorepo to v16.0.10 [security] (#4634) 2025-12-12 15:44:35 +01:00
homarr-update-contributors[bot] e7b3be2edb chore: update contributors (#4633)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2025-12-12 12:38:42 +00:00
homarr-renovate[bot] 3b1c564a5a fix(deps): update dependency winston to v3.19.0 (#4632)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-12 08:39:42 +00:00
homarr-renovate[bot] e37325f5e0 fix(deps): update dependency ldapts to v8.0.14 (#4627)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-11 21:31:12 +00:00
homarr-renovate[bot] 8a14a6cb45 fix(deps): update dependency ldapts to v8.0.13 (#4626)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-11 00:37:25 +01:00
homarr-renovate[bot]andMeier Lukas d111eee0e0 fix(deps): update tiptap monorepo to v3 (major) (#3612)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-12-11 00:35:55 +01:00
Meier Lukas 0b4ca93aa1 fix(transmission): total download incorrect for cross seeded torrents (#3211) 2025-12-10 20:50:39 +01:00
Meier Lukas 6a908397cd fix(plex): test connection not fully checking connection (#4617) 2025-12-10 13:28:28 +01:00
homarr-renovate[bot] 85689caf6c fix(deps): update turbo monorepo to ^2.6.3 (#4622)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-09 18:13:32 +00:00
homarr-renovate[bot]andMeier Lukas cab5d19141 fix(deps): update dependency drizzle-orm to ^0.45.0 (#4619)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-12-09 19:07:54 +01:00
homarr-renovate[bot] 5cbab28099 fix(deps): update dependency drizzle-kit to ^0.31.8 (#4618)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-09 16:39:05 +01:00
Meier Lukas 1dd9694719 fix: format issues 2025-12-09 16:34:54 +01:00
Copilotandajnart 681aee05f0 fix: add autocomplete and id attributes for password manager compatibility (#4602)
Co-authored-by: ajnart <49837342+ajnart@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2025-12-09 15:59:33 +01:00
homarr-renovate[bot] c3ed7090c2 chore(deps): update dependency esbuild@&lt;&#x3d;0.24.2 to >=0.27.1 (#4611)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-09 15:51:42 +01:00
homarr-renovate[bot]andMeier Lukas a20248d0b6 fix(deps): update dependency better-sqlite3 to ^12.5.0 (#4569)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-12-09 15:51:11 +01:00
homarr-crowdin[bot] 5a95cd3753 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-12-09 14:49:00 +00:00
homarr-renovate[bot] efc0384b37 fix(deps): update dependency @tanstack/react-query to ^5.90.12 (#4615)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-09 14:48:34 +00:00
homarr-renovate[bot] fb04875253 chore(deps): update dependency esbuild to ^0.27.1 (#4610)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-09 15:46:59 +01:00
homarr-renovate[bot] 4689ec9933 fix(deps): update dependency ldapts to v8.0.12 (#4613)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-09 05:29:10 +00:00
TobiasandMeier Lukas d084dfb470 fix: improve run script (#4609)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-12-08 22:59:59 +01:00
homarr-renovate[bot] 7de45e6fe5 fix(deps): update turbo monorepo to ^2.6.2 (#4612)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-08 21:26:08 +00:00
homarr-renovate[bot] f6d16fe44b fix(deps): update react monorepo to v19.2.1 (#4608)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-08 16:36:42 +00:00
Releases Homarr 340f5606a9 chore(release): 1.45.3 [skip ci] 2025-12-08 16:11:13 +00:00
homarr-releases[bot] ca9478dce7 chore(release): automatic release v1.45.3 2025-12-08 16:10:20 +00:00
Meier Lukas 1bb3654bf7 Merge commit from fork 2025-12-08 16:54:15 +01:00
homarr-renovate[bot]andMeier Lukas d88c6027fe fix(deps): update dependency typescript-eslint to ^8.48.1 (#4438)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-12-08 16:52:25 +01:00
homarr-renovate[bot] 020cbd72d3 fix(deps): update dependency glob to v13 (#4584)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-08 15:50:54 +01:00
homarr-renovate[bot] aed2025b30 chore(deps): update vitest monorepo to ^4.0.15 (#4603)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-08 13:24:28 +00:00
homarr-renovate[bot]andMeier Lukas 54809823e9 fix(deps): update dependency prettier to ^3.7.4 (#4582)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-12-08 14:19:46 +01:00
homarr-renovate[bot] 4f92e5265a fix(deps): update dependency next-intl to v4.5.8 (#4606)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-08 11:38:56 +00:00
homarr-renovate[bot] 234266ebd9 chore(deps): update actions/checkout action to v6 (#4601)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-07 17:36:30 +01:00
homarr-renovate[bot] 6b28aa86d0 fix(deps): update dependency next-intl to v4.5.7 (#4598)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-07 01:59:48 +00:00
homarr-renovate[bot] ed638d4767 fix(deps): update dependency jotai to ^2.15.2 (#4597)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-07 01:59:14 +00:00
homarr-renovate[bot] a2f7d63d2a chore(deps): update dependency vite@&gt;&#x3d;5.0.0 &lt;&#x3d;5.4.18 to >=7.2.6 (#4595)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-07 01:58:21 +00:00
homarr-crowdin[bot] 9639f44560 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-12-07 00:59:37 +00:00
Releases Homarr bc087e6e60 chore(release): 1.45.2 [skip ci] 2025-12-05 19:17:05 +00:00
homarr-releases[bot] d347d4455b chore(release): automatic release v1.45.2 2025-12-05 19:16:11 +00:00
Meier Lukas 10572ca962 fix: disable react-compiler due to issue with mantine-react-table data updates (#4593) 2025-12-05 19:10:23 +01:00
homarr-update-contributors[bot] d350e48998 chore: update contributors (#4591)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2025-12-05 14:56:10 +00:00
DZ945brH 2c48b18968 fix: add ipv6 support for internal nginx proxy 2025-12-05 13:04:17 +01:00
Meier Lukas 069ed0a267 chore: update bug-report template 2025-12-03 23:42:06 +01:00
Releases Homarr a3e53ec291 chore(release): 1.45.1 [skip ci] 2025-12-03 17:43:49 +00:00
homarr-releases[bot] 38a1c9ca83 chore(release): automatic release v1.45.1 2025-12-03 17:42:39 +00:00
Meier Lukas b40deba9ff fix(security): rce vulnerability in nextjs 2025-12-03 18:33:01 +01:00
homarr-renovate[bot] d5d59dd256 fix(deps): update mantine monorepo to ^8.3.9 (#4576)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 13:44:52 +00:00
homarr-renovate[bot] 3453482ed4 fix(deps): update trpc monorepo to ^11.7.2 (#4579)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 12:57:36 +00:00
homarr-renovate[bot] d2c1dc3cc1 fix(deps): update dependency swagger-ui-react to ^5.30.3 (#4574)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 12:54:17 +00:00
homarr-renovate[bot] 59bcbc8b4d fix(deps): update tanstack-query monorepo (#4578)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 11:33:29 +00:00
homarr-renovate[bot] b2c46d33d6 fix(deps): update dependency chroma-js to ^3.2.0 (#4585)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 11:28:54 +00:00
homarr-renovate[bot] ca01a31fe5 fix(deps): update dependency isomorphic-dompurify to ^2.33.0 (#4581)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 10:53:13 +00:00
homarr-renovate[bot] deb5e9d960 fix(deps): update dependency superjson to v2.2.6 (#4573)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 10:48:15 +00:00
homarr-renovate[bot] 16f12d98e6 fix(deps): update nextjs monorepo to v16.0.5 (#4577)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 10:45:55 +00:00
homarr-renovate[bot] 10bdd74b2b fix(deps): update testcontainers-node monorepo to ^11.9.0 (#4583)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 10:35:20 +00:00
Meier Lukas a91955a35b chore(docs): update sitemap 2025-12-03 11:31:46 +01:00
Meier Lukas 87deba417f chore: update bug-report template 2025-12-03 11:30:41 +01:00
homarr-renovate[bot] 220f68ee04 chore(deps): update pnpm to v10.24.0 (#4580)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 10:27:50 +00:00
homarr-renovate[bot] d6fc46df3b fix(deps): update dependency chroma-js to ^3.1.4 (#4570)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 10:24:55 +00:00
homarr-renovate[bot] 7a8c50fa4c fix(deps): update dependency sass to ^1.94.2 (#4572)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 10:24:23 +00:00
homarr-renovate[bot] 59eecaf842 chore(deps): update vitest monorepo to ^4.0.14 (#4568)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 10:12:54 +00:00
homarr-renovate[bot] 879a77ff2a fix(deps): update dependency zod to ^4.1.13 (#4575)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 10:10:53 +00:00
homarr-renovate[bot] b53bcb77e9 chore(deps): update dependency prettier-plugin-packagejson to ^2.5.20 (#4566)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 10:06:51 +00:00
homarr-renovate[bot] 3062ddafbe fix(deps): update dependency next-intl to v4.5.6 (#4571)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 10:06:32 +00:00
homarr-renovate[bot] 7f782220a1 chore(deps): update dependency vite@&gt;&#x3d;5.0.0 &lt;&#x3d;5.4.18 to >=7.2.4 (#4567)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 10:06:17 +00:00
homarr-renovate[bot] aa2ec5d221 chore(deps): update dependency hono@&lt;4.6.5 to >=4.10.7 (#4565)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 10:00:23 +00:00
homarr-renovate[bot] e6db47aaab chore(deps): update dependency @types/react to v19.2.7 (#4564)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-12-03 09:59:46 +00:00
homarr-crowdin[bot] f3d2026746 chore(lang): update translations from crowdin (#4560)
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-12-01 10:43:38 +01:00
121 changed files with 2379 additions and 8738 deletions

No files matched your search

+2
View File
@@ -33,6 +33,8 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.45.1
- 1.45.0
- 1.44.0
- 1.43.1
- 1.43.0
@@ -0,0 +1,43 @@
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 create directories (e.g. amd64, 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: Start docker container for ${{ inputs.architecture }}
run: |
docker run --name homarr \
-e "SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000" \
--detach --rm ${{ inputs.digest }}
shell: bash
- name: Extract build from ${{ inputs.architecture }} container
run: |
docker exec homarr cp /etc/nginx/templates/nginx.conf /app && \
docker exec homarr tar -czf extraction.tar.gz -C /app . && \
mkdir -p ${{ runner.temp }}/extraction/${{ inputs.architecture }} && \
docker cp homarr:/app/extraction.tar.gz ${{ runner.temp }}/extraction/${{ inputs.architecture }}/build-${{ 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
env:
GH_TOKEN: ${{ inputs.token }}
run: gh release upload --repo ${{ inputs.repository }} ${{ inputs.release-tag }} ${{ runner.temp }}/extraction/${{ inputs.architecture }}/build-${{ inputs.architecture }}.tar.gz --clobber
shell: bash
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
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@v5
uses: actions/checkout@v6
- name: Obtain token
id: obtainToken
uses: actions/create-github-app-token@v2
+6 -6
View File
@@ -24,7 +24,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
@@ -39,7 +39,7 @@ jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
@@ -50,7 +50,7 @@ jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
@@ -61,7 +61,7 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
@@ -79,7 +79,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
- name: Build docker image
@@ -102,7 +102,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
- name: Copy env
@@ -14,7 +14,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Obtain token
id: obtainToken
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Upload Crowdin translations
uses: crowdin/github-action@v2
+40 -6
View File
@@ -37,7 +37,7 @@ jobs:
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'
@@ -55,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@v5
- uses: actions/checkout@v6
if: env.SKIP_RELEASE == 'false'
with:
persist-credentials: false
@@ -115,7 +115,7 @@ jobs:
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ needs.release.outputs.git_ref }}
@@ -146,7 +146,6 @@ 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
@@ -154,7 +153,7 @@ jobs:
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ needs.release.outputs.git_ref }}
@@ -185,10 +184,38 @@ 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
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
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]
needs: [release, build-amd64, build-arm64, extract-asset-amd64, extract-asset-arm64]
runs-on: ubuntu-latest
env:
NEXT_VERSION: ${{ needs.release.outputs.version }}
@@ -222,6 +249,13 @@ 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 }}
@@ -27,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@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get Next Version
@@ -12,7 +12,7 @@ jobs:
renovate-validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- run: |
npx --yes --package renovate -- \
renovate-config-validator --strict .github/renovate.json5
+1 -1
View File
@@ -49,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@v5
uses: actions/checkout@v6
with:
token: ${{ steps.obtainToken.outputs.token }}
- name: Setup
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
@@ -27,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@v5
uses: actions/checkout@v6
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
- name: Setup
+1
View File
@@ -44,6 +44,7 @@
{
"successComment": false,
"failComment": false,
"draftRelease": true,
"releaseBodyTemplate": "<%= _.truncate(nextRelease.notes, { 'length': 124000, 'omission': '' }) %>"
}
]
+64
View File
@@ -1,3 +1,67 @@
## [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
+2 -1
View File
@@ -20,7 +20,8 @@ const withNextIntl = createNextIntlPlugin({
const nextConfig: NextConfig = {
output: "standalone",
reactStrictMode: true,
reactCompiler: true,
// react compiler breaks mantine-react-table, so disabled for now
//reactCompiler: true,
/** We already do typechecking as separate tasks in CI */
typescript: { ignoreBuildErrors: true },
/**
+25 -25
View File
@@ -50,46 +50,46 @@
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"@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",
"@mantine/colors-generator": "^8.3.9",
"@mantine/core": "^8.3.9",
"@mantine/dropzone": "^8.3.9",
"@mantine/hooks": "^8.3.9",
"@mantine/modals": "^8.3.9",
"@mantine/tiptap": "^8.3.9",
"@million/lint": "1.0.14",
"@tabler/icons-react": "^3.35.0",
"@tanstack/react-query": "^5.90.10",
"@tanstack/react-query-devtools": "^5.90.2",
"@tanstack/react-query": "^5.90.12",
"@tanstack/react-query-devtools": "^5.91.1",
"@tanstack/react-query-next-experimental": "^5.91.0",
"@trpc/client": "^11.7.1",
"@trpc/next": "^11.7.1",
"@trpc/react-query": "^11.7.1",
"@trpc/server": "^11.7.1",
"@trpc/client": "^11.7.2",
"@trpc/next": "^11.7.2",
"@trpc/react-query": "^11.7.2",
"@trpc/server": "^11.7.2",
"@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.1.2",
"chroma-js": "^3.2.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.19",
"dotenv": "^17.2.3",
"flag-icons": "^7.5.0",
"glob": "^12.0.0",
"isomorphic-dompurify": "^2.32.0",
"jotai": "^2.15.1",
"glob": "^13.0.0",
"isomorphic-dompurify": "^2.33.0",
"jotai": "^2.15.2",
"mantine-react-table": "2.0.0-beta.9",
"next": "16.0.3",
"next": "16.0.10",
"postcss-preset-mantine": "^1.18.0",
"prismjs": "^1.30.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"react": "19.2.1",
"react-dom": "19.2.1",
"react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1",
"sass": "^1.94.1",
"superjson": "2.2.5",
"swagger-ui-react": "^5.30.2",
"sass": "^1.94.2",
"superjson": "2.2.6",
"swagger-ui-react": "^5.30.3",
"use-deep-compare-effect": "^1.8.1",
"zod": "^4.1.12"
"zod": "^4.1.13"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -98,13 +98,13 @@
"@types/chroma-js": "3.1.2",
"@types/node": "^24.10.1",
"@types/prismjs": "^1.26.5",
"@types/react": "19.2.6",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.2.1",
"eslint": "^9.39.1",
"node-loader": "^2.1.0",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"typescript": "^5.9.3"
}
}
@@ -64,16 +64,23 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
<Stack gap="xl">
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="lg">
<TextInput label={t("field.username.label")} autoComplete="off" {...form.getInputProps("username")} />
<TextInput
label={t("field.username.label")}
id="username"
autoComplete="username"
{...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,8 +116,18 @@ 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")} {...form.getInputProps("name")} />
<PasswordInput label={t("field.password.label")} {...form.getInputProps("password")} />
<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")}
/>
{providers.includes("credentials") && (
<Stack gap="sm">
@@ -17,7 +17,7 @@ import {
IconSettings,
} from "@tabler/icons-react";
import { applyAutoLayoutToBoard, clientApi } from "@homarr/api/client";
import { 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(applyAutoLayoutToBoard(board));
if (isEditMode) return saveBoard(board);
open();
}, [board, isEditMode, saveBoard, open]);
@@ -1,31 +1,11 @@
"use client";
import {
Badge,
Box,
Button,
Chip,
ChipGroup,
Fieldset,
Grid,
Group,
Input,
InputWrapper,
NumberInput,
RadioGroup,
SimpleGrid,
Slider,
Stack,
Text,
TextInput,
} from "@mantine/core";
import { Button, Fieldset, Grid, Group, Input, NumberInput, 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";
@@ -45,8 +25,6 @@ export const LayoutSettingsContent = ({ board }: Props) => {
const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), {
initialValues: {
layouts: board.layouts,
layoutMode: board.layoutMode,
baseLayoutId: board.baseLayoutId,
},
});
@@ -60,53 +38,6 @@ 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>
@@ -151,21 +82,18 @@ export const LayoutSettingsContent = ({ board }: Props) => {
/>
</Grid.Col>
</Grid>
{form.values.layouts.length >= 2 && form.values.baseLayoutId !== layout.id && (
{form.values.layouts.length >= 2 && (
<Group justify="end">
<Button
variant="subtle"
onClick={() => {
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),
};
});
form.setValues((previous) =>
previous.layouts !== undefined && previous.layouts.length >= 2
? {
layouts: form.values.layouts.filter((filteredLayout) => filteredLayout.id !== layout.id),
}
: previous,
);
}}
>
{t("common.action.remove")}
@@ -12,7 +12,11 @@ import { groupCreateSchema } from "@homarr/validation/group";
export const InitGroup = () => {
const t = useI18n();
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation();
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/init");
},
});
const form = useZodForm(groupCreateSchema, {
initialValues: {
name: "",
@@ -21,9 +25,6 @@ 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,7 +17,11 @@ 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();
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/init");
},
});
const form = useZodForm(settingsInitSchema, { initialValues: defaultServerSettings });
form.watch("analytics.enableGeneral", ({ value }) => {
@@ -31,11 +35,7 @@ export const InitSettings = () => {
});
const handleSubmitAsync = async (values: z.infer<typeof settingsInitSchema>) => {
await mutateAsync(values, {
async onSuccess() {
await revalidatePathActionAsync("/init");
},
});
await mutateAsync(values);
};
return (
@@ -26,19 +26,19 @@ export const InitUserForm = () => {
const handleSubmitAsync = async (values: FormType) => {
await mutateAsync(values, {
async onSuccess() {
onSuccess() {
showSuccessNotification({
title: tUser("notification.success.title"),
message: tUser("notification.success.message"),
});
await signIn("credentials", {
void signIn("credentials", {
name: values.username,
password: values.password,
redirect: false,
}).then(async () => {
await revalidatePathActionAsync("/init");
});
await revalidatePathActionAsync("/init");
},
onError: (error) => {
showErrorNotification({
@@ -84,9 +84,6 @@ export const BoardCardMenuDropdown = ({ board }: BoardCardMenuDropdownProps) =>
id: board.id,
name: board.name,
},
onSuccess: async () => {
await revalidatePathActionAsync("/manage/boards");
},
});
}, [board.id, board.name, openDuplicateModal]);
@@ -86,7 +86,11 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
},
});
const { mutateAsync: createIntegrationAsync, isPending } = clientApi.integration.create.useMutation();
const { mutateAsync: createIntegrationAsync, isPending } = clientApi.integration.create.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/manage/integrations");
},
});
const [error, setError] = useState<null | AnyMappedTestConnectionError>(null);
const handleSubmitAsync = async ({ appId, appHref, hasApp, ...values }: FormType) => {
@@ -116,7 +120,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
app,
},
{
async onSuccess(data) {
onSuccess(data) {
// We do it this way as we are unable to send a typesafe error through onError
if (data?.error) {
setError(data.error);
@@ -132,7 +136,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
message: t("integration.page.create.notification.success.message"),
});
await revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
router.push("/manage/integrations");
},
onError: () => {
showErrorNotification({
@@ -15,7 +15,11 @@ interface RemoveCertificateProps {
export const RemoveCertificate = ({ fileName }: RemoveCertificateProps) => {
const { openConfirmModal } = useConfirmModal();
const { mutateAsync } = clientApi.certificates.removeCertificate.useMutation();
const { mutateAsync } = clientApi.certificates.removeCertificate.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/manage/tools/certificates");
},
});
const t = useI18n();
const handleClick = () => {
@@ -27,12 +31,11 @@ export const RemoveCertificate = ({ fileName }: RemoveCertificateProps) => {
await mutateAsync(
{ fileName },
{
async onSuccess() {
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({
@@ -15,7 +15,11 @@ interface RemoveHostnameActionIconProps {
}
export const RemoveHostnameActionIcon = (input: RemoveHostnameActionIconProps) => {
const { mutateAsync } = clientApi.certificates.removeTrustedHostname.useMutation();
const { mutateAsync } = clientApi.certificates.removeTrustedHostname.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/manage/tools/certificates/hostnames");
},
});
const { openConfirmModal } = useConfirmModal();
const t = useI18n();
@@ -26,8 +30,7 @@ export const RemoveHostnameActionIcon = (input: RemoveHostnameActionIconProps) =
// eslint-disable-next-line no-restricted-syntax
async onConfirm() {
await mutateAsync(input, {
async onSuccess() {
await revalidatePathActionAsync("/manage/tools/certificates/hostnames");
onSuccess() {
showSuccessNotification({
title: t("certificate.action.removeHostname.notification.success.title"),
message: t("certificate.action.removeHostname.notification.success.message"),
@@ -206,15 +206,16 @@ const ContainerActionBarButton = (props: ContainerActionBarButtonProps) => {
const t = useScopedI18n("docker.action");
const utils = clientApi.useUtils();
const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation();
const { mutateAsync, isPending } = clientApi.docker[`${props.action}All`].useMutation({
async onSettled() {
await utils.docker.getContainers.invalidate();
},
});
const handleClickAsync = async () => {
await mutateAsync(
{ ids: props.selectedIds },
{
async onSettled() {
await utils.docker.getContainers.invalidate();
},
onSuccess() {
showSuccessNotification({
title: t(`${props.action}.notification.success.title`),
@@ -18,7 +18,12 @@ interface UserProfileAvatarForm {
}
export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
const { mutate } = clientApi.user.setProfileImage.useMutation();
const { mutate } = clientApi.user.setProfileImage.useMutation({
async onSuccess() {
// Revalidate all as the avatar is used in multiple places
await revalidatePathActionAsync("/");
},
});
const [opened, { toggle }] = useDisclosure(false);
const { openConfirmModal } = useConfirmModal();
const t = useI18n();
@@ -38,9 +43,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
image: base64Url,
},
{
async onSuccess() {
// Revalidate all as the avatar is used in multiple places
await revalidatePathActionAsync("/");
onSuccess() {
showSuccessNotification({
message: tManageAvatar("changeImage.notification.success.message"),
});
@@ -74,9 +77,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
image: null,
},
{
async onSuccess() {
// Revalidate all as the avatar is used in multiple places
await revalidatePathActionAsync("/");
onSuccess() {
showSuccessNotification({
message: tManageAvatar("removeImage.notification.success.message"),
});
@@ -25,7 +25,11 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
const [innerOwnerId, setInnerOwnerId] = useState(group.ownerId);
const { openModal } = useModalAction(UserSelectModal);
const { openConfirmModal } = useConfirmModal();
const { mutateAsync } = clientApi.group.transferOwnership.useMutation();
const { mutateAsync } = clientApi.group.transferOwnership.useMutation({
async onSuccess() {
await revalidatePathActionAsync(`/manage/users/groups/${group.id}`);
},
});
const handleTransfer = useCallback(() => {
openModal(
@@ -47,7 +51,7 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
userId: id,
},
{
async onSuccess() {
onSuccess() {
setInnerOwnerId(id);
showSuccessNotification({
title: tRoot("common.notification.transfer.success"),
@@ -56,7 +60,6 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
user: name,
}),
});
await revalidatePathActionAsync(`/manage/users/groups/${group.id}`);
},
onError() {
showErrorNotification({
@@ -56,16 +56,19 @@ export const GroupsTable = ({ groups, initialGroupIds, hasFilter }: GroupsTableP
() => initialGroupIds.some((groupId, index) => groupIds.indexOf(groupId) !== index),
[groupIds, initialGroupIds],
);
const { mutateAsync, isPending } = clientApi.group.savePositions.useMutation();
const { mutateAsync, isPending } = clientApi.group.savePositions.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/manage/users/groups");
},
});
const handleSavePositionsAsync = async () => {
await mutateAsync(
{ positions: groupIds },
{
async onSuccess() {
onSuccess() {
showSuccessNotification({
message: t("group.action.changePosition.notification.success.message"),
});
await revalidatePathActionAsync("/manage/users/groups");
},
onError() {
showSuccessNotification({
@@ -10,7 +10,6 @@ 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",
@@ -42,14 +41,12 @@ export class BoardMockBuilder {
items: [],
layouts: [
{
id: baseLayoutId,
id: createId(),
name: "Base",
columnCount: 12,
breakpoint: 0,
},
],
baseLayoutId,
layoutMode: "auto",
...board,
};
}
+3 -3
View File
@@ -40,7 +40,7 @@
"dayjs": "^1.11.19",
"dotenv": "^17.2.3",
"fastify": "^5.6.2",
"superjson": "2.2.5",
"superjson": "2.2.6",
"undici": "7.16.0"
},
"devDependencies": {
@@ -49,9 +49,9 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^24.10.1",
"dotenv-cli": "^11.0.0",
"esbuild": "^0.27.0",
"esbuild": "^0.27.1",
"eslint": "^9.39.1",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"tsx": "4.20.4",
"typescript": "^5.9.3"
}
+2 -2
View File
@@ -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.0",
"esbuild": "^0.27.1",
"eslint": "^9.39.1",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"typescript": "^5.9.3"
}
}
+2 -1
View File
@@ -5,6 +5,7 @@ events {
http {
server {
listen 7575;
listen [::]:7575;
# Route websockets traffic to port 3001
location /websockets {
@@ -25,4 +26,4 @@ http {
client_max_body_size 32M;
}
}
}
}
+15 -15
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.45.0",
"version": "1.46.0",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -42,27 +42,27 @@
"@semantic-release/github": "^12.0.2",
"@semantic-release/npm": "^13.1.2",
"@semantic-release/release-notes-generator": "^14.1.0",
"@testcontainers/redis": "^11.8.1",
"@turbo/gen": "^2.6.1",
"@testcontainers/redis": "^11.9.0",
"@turbo/gen": "^2.6.3",
"@vitejs/plugin-react": "^5.1.1",
"@vitest/coverage-v8": "^4.0.10",
"@vitest/ui": "^4.0.10",
"@vitest/coverage-v8": "^4.0.15",
"@vitest/ui": "^4.0.15",
"conventional-changelog-conventionalcommits": "^9.1.0",
"cross-env": "^10.1.0",
"jsdom": "^27.2.0",
"json5": "^2.2.3",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"semantic-release": "^25.0.2",
"testcontainers": "^11.8.1",
"turbo": "^2.6.1",
"testcontainers": "^11.9.0",
"turbo": "^2.6.3",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.0.10"
"vitest": "^4.0.15"
},
"packageManager": "pnpm@10.22.0",
"packageManager": "pnpm@10.24.0",
"engines": {
"node": ">=24.11.1",
"pnpm": ">=10.22.0"
"pnpm": ">=10.24.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.0",
"esbuild@<=0.24.2": ">=0.27.1",
"form-data@>=4.0.0 <4.0.4": ">=4.0.5",
"hono@<4.6.5": ">=4.10.6",
"hono@<4.6.5": ">=4.10.7",
"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.0",
"react-is": "^19.2.1",
"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.2"
"vite@>=5.0.0 <=5.4.18": ">=7.2.6"
},
"patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch",
+1 -1
View File
@@ -26,7 +26,7 @@
"@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@umami/node": "^0.4.0",
"superjson": "2.2.5"
"superjson": "2.2.6"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+11 -11
View File
@@ -43,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.10",
"@trpc/client": "^11.7.1",
"@trpc/react-query": "^11.7.1",
"@trpc/server": "^11.7.1",
"@trpc/tanstack-react-query": "^11.7.1",
"@tanstack/react-query": "^5.90.12",
"@trpc/client": "^11.7.2",
"@trpc/react-query": "^11.7.2",
"@trpc/server": "^11.7.2",
"@trpc/tanstack-react-query": "^11.7.2",
"lodash.clonedeep": "^4.5.0",
"next": "16.0.3",
"react": "19.2.0",
"react-dom": "19.2.0",
"superjson": "2.2.5",
"next": "16.0.10",
"react": "19.2.1",
"react-dom": "19.2.1",
"superjson": "2.2.6",
"trpc-to-openapi": "^3.1.0",
"zod": "^4.1.12"
"zod": "^4.1.13"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.39.1",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"typescript": "^5.9.3"
}
}
-2
View File
@@ -17,5 +17,3 @@ export const fetchApi = createTRPCClient<AppRouter>({
}),
],
});
export { applyAutoLayoutToBoard } from "./router/board/board-layout";
+249 -41
View File
@@ -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 } from "@homarr/db";
import type { Database, InferInsertModel, InferSelectModel, SQL } 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,11 +27,13 @@ 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";
@@ -48,11 +50,13 @@ 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 { applyAutoLayoutToBoard, getUpdatedBoardLayout } from "./board/board-layout";
import { getFullBoardWithWhereAsync } from "./board/full-board-query";
import { generateResponsiveGridFor } from "./board/grid-algorithm";
export const boardRouter = createTRPCRouter({
exists: permissionRequiredProcedure
@@ -275,7 +279,6 @@ export const boardRouter = createTRPCRouter({
name: input.name,
isPublic: input.isPublic,
creatorId: ctx.session.user.id,
layoutMode: input.layoutMode,
});
createBoardCollection.sections.push({
id: createId(),
@@ -284,9 +287,8 @@ export const boardRouter = createTRPCRouter({
yOffset: 0,
boardId,
});
const baseLayoutId = createId();
createBoardCollection.layouts.push({
id: baseLayoutId,
id: createId(),
name: "Base",
columnCount: input.columnCount,
breakpoint: 0,
@@ -294,10 +296,6 @@ 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));
@@ -584,15 +582,13 @@ export const boardRouter = createTRPCRouter({
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
const board = await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
return applyAutoLayoutToBoard(board);
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
}),
getBoardByName: publicProcedure.input(boardByNameSchema).query(async ({ input, ctx }) => {
const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase());
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
const board = await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
return applyAutoLayoutToBoard(board);
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
}),
saveLayouts: protectedProcedure.input(boardSaveLayoutsSchema).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
@@ -600,32 +596,13 @@ 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,
@@ -732,18 +709,11 @@ 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() })))
@@ -1438,6 +1408,244 @@ 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));
@@ -1,186 +0,0 @@
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,
),
};
};
@@ -1,133 +0,0 @@
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,7 +56,8 @@ export const generateResponsiveGridFor = ({
dynamicSectionHeightMap.set(dynamicSection.id, result.height);
}
if (width === previousWidth) {
// Return same positions for items in the current section
if (width >= previousWidth) {
return {
height: Math.max(...itemsOfCurrentSection.map((item) => item.yOffset + item.height)),
items: newItems.concat(normalizedItems),
@@ -99,12 +99,11 @@ qlllmmmmmnnrrrst
ulllmmmmmnnrrrvw
xyz äö`;
const eighteenColumns = `
abbccccddddeeefghi
jbbccccddddeeeklll
mmmmmnnopqseeetlll
mmmmmnnrrruvwxylll
mmmmmnnrrrzäö `;
// Just add two empty columns to the right
const eighteenColumns = sixteenColumns
.split("\n")
.map((line, index) => (index === 0 ? line : `${line} `))
.join("\n");
const tenColumns = `
abbcccceee
+6 -6
View File
@@ -34,12 +34,12 @@
"@homarr/validation": "workspace:^0.1.0",
"bcrypt": "^6.0.0",
"cookies": "^0.9.1",
"ldapts": "8.0.9",
"next": "16.0.3",
"ldapts": "8.0.14",
"next": "16.0.10",
"next-auth": "5.0.0-beta.30",
"react": "19.2.0",
"react-dom": "19.2.0",
"zod": "^4.1.12"
"react": "19.2.1",
"react-dom": "19.2.1",
"zod": "^4.1.13"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -48,7 +48,7 @@
"@types/bcrypt": "6.0.0",
"@types/cookies": "0.9.2",
"eslint": "^9.39.1",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"typescript": "^5.9.3"
}
}
@@ -6,14 +6,14 @@ import type { Database, InferInsertModel } 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";
import type { ldapSignInSchema } from "@homarr/validation/user";
import { env } from "../../../env";
import { LdapClient } from "../ldap-client";
export const authorizeWithLdapCredentialsAsync = async (
db: Database,
credentials: z.infer<typeof userSignInSchema>,
credentials: z.infer<typeof ldapSignInSchema>,
) => {
logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`);
const client = new LdapClient();
@@ -38,7 +38,14 @@ export const authorizeWithLdapCredentialsAsync = async (
attributes: [env.AUTH_LDAP_USERNAME_ATTRIBUTE, env.AUTH_LDAP_USER_MAIL_ATTRIBUTE],
},
})
.then((entries) => entries.at(0));
.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);
});
if (!ldapUser) {
logger.warn(`User ${credentials.name} not found in LDAP`);
@@ -1,7 +1,7 @@
import type Credentials from "@auth/core/providers/credentials";
import type { Database } from "@homarr/db";
import { userSignInSchema } from "@homarr/validation/user";
import { ldapSignInSchema, 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 userSignInSchema.parseAsync(credentials);
const data = await ldapSignInSchema.parseAsync(credentials);
return await authorizeWithLdapCredentialsAsync(db, data).catch(() => null);
},
}) satisfies CredentialsConfiguration;
+2 -2
View File
@@ -25,8 +25,8 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/api": "workspace:^0.1.0",
"react": "19.2.0",
"react-dom": "19.2.0"
"react": "19.2.1",
"react-dom": "19.2.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+1 -8
View File
@@ -7,7 +7,6 @@ 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<{
@@ -84,12 +83,10 @@ 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, isEditMode]);
}, [board]);
useEffect(() => {
if (typeof window === "undefined") return;
@@ -100,10 +97,6 @@ export const useCurrentLayout = () => {
};
}, [onResize]);
if (board.layoutMode === "auto" && isEditMode && board.baseLayoutId !== null) {
return board.baseLayoutId;
}
return currentLayout;
};
+1 -1
View File
@@ -34,7 +34,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"esbuild": "^0.27.0",
"esbuild": "^0.27.1",
"eslint": "^9.39.1",
"typescript": "^5.9.3"
}
+4 -4
View File
@@ -32,12 +32,12 @@
"@paralleldrive/cuid2": "^3.1.0",
"dayjs": "^1.11.19",
"dns-caching": "^0.2.7",
"next": "16.0.3",
"next": "16.0.10",
"octokit": "^5.0.5",
"react": "19.2.0",
"react-dom": "19.2.0",
"react": "19.2.1",
"react-dom": "19.2.1",
"undici": "7.16.0",
"zod": "^4.1.12",
"zod": "^4.1.13",
"zod-validation-error": "^5.0.0"
},
"devDependencies": {
+1 -1
View File
@@ -26,7 +26,7 @@
"dependencies": {
"@t3-oss/env-nextjs": "^0.13.8",
"ioredis": "5.8.2",
"zod": "^4.1.12"
"zod": "^4.1.13"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+7 -7
View File
@@ -29,20 +29,20 @@
"@homarr/core": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@tanstack/react-query": "^5.90.10",
"@trpc/client": "^11.7.1",
"@trpc/server": "^11.7.1",
"@trpc/tanstack-react-query": "^11.7.1",
"@tanstack/react-query": "^5.90.12",
"@trpc/client": "^11.7.2",
"@trpc/server": "^11.7.2",
"@trpc/tanstack-react-query": "^11.7.2",
"node-cron": "^4.2.1",
"react": "19.2.0",
"zod": "^4.1.12"
"react": "19.2.1",
"zod": "^4.1.13"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11",
"@types/react": "19.2.6",
"@types/react": "19.2.7",
"eslint": "^9.39.1",
"typescript": "^5.9.3"
}
@@ -1,3 +0,0 @@
ALTER TABLE `board` ADD `layout_mode` varchar(32) DEFAULT 'custom' NOT NULL;--> statement-breakpoint
ALTER TABLE `board` ADD `base_layout_id` varchar(64);--> statement-breakpoint
ALTER TABLE `board` ADD CONSTRAINT `board_base_layout_id_layout_id_fk` FOREIGN KEY (`base_layout_id`) REFERENCES `layout`(`id`) ON DELETE set null ON UPDATE no action;
File diff suppressed because it is too large. Load diff
@@ -260,13 +260,6 @@
"when": 1760968518445,
"tag": "0036_add_app_reference_to_integration",
"breakpoints": true
},
{
"idx": 37,
"version": "5",
"when": 1764190331048,
"tag": "0037_add_auto_layout_mode",
"breakpoints": true
}
]
}
@@ -1,3 +0,0 @@
ALTER TABLE "board" ADD COLUMN "layout_mode" varchar(32) DEFAULT 'custom' NOT NULL;--> statement-breakpoint
ALTER TABLE "board" ADD COLUMN "base_layout_id" varchar(64);--> statement-breakpoint
ALTER TABLE "board" ADD CONSTRAINT "board_base_layout_id_layout_id_fk" FOREIGN KEY ("base_layout_id") REFERENCES "public"."layout"("id") ON DELETE set null ON UPDATE no action;
File diff suppressed because it is too large. Load diff
@@ -22,13 +22,6 @@
"when": 1760968530084,
"tag": "0002_add_app_reference_to_integration",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1764190350662,
"tag": "0003_add_auto_layout_mode",
"breakpoints": true
}
]
}
@@ -1,2 +0,0 @@
ALTER TABLE `board` ADD `layout_mode` text DEFAULT 'custom' NOT NULL;--> statement-breakpoint
ALTER TABLE `board` ADD `base_layout_id` text REFERENCES layout(id);
File diff suppressed because it is too large. Load diff
@@ -246,13 +246,6 @@
"when": 1760968503571,
"tag": "0034_add_app_reference_to_integration",
"breakpoints": true
},
{
"idx": 35,
"version": "6",
"when": 1764190313293,
"tag": "0035_add_auto_layout_mode",
"breakpoints": true
}
]
}
+9 -9
View File
@@ -49,18 +49,18 @@
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^8.3.8",
"@mantine/core": "^8.3.9",
"@paralleldrive/cuid2": "^3.1.0",
"@testcontainers/mysql": "^11.8.1",
"@testcontainers/postgresql": "^11.8.1",
"better-sqlite3": "^12.4.1",
"@testcontainers/mysql": "^11.9.0",
"@testcontainers/postgresql": "^11.9.0",
"better-sqlite3": "^12.5.0",
"dotenv": "^17.2.3",
"drizzle-kit": "^0.31.7",
"drizzle-orm": "^0.44.7",
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.0",
"drizzle-zod": "^0.8.3",
"mysql2": "3.15.3",
"pg": "^8.16.3",
"superjson": "2.2.5"
"superjson": "2.2.6"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -69,9 +69,9 @@
"@types/better-sqlite3": "7.6.13",
"@types/pg": "^8.15.6",
"dotenv-cli": "^11.0.0",
"esbuild": "^0.27.0",
"esbuild": "^0.27.1",
"eslint": "^9.39.1",
"prettier": "^3.6.2",
"prettier": "^3.7.4",
"tsx": "4.20.4",
"typescript": "^5.9.3"
}
+6 -16
View File
@@ -17,11 +17,16 @@ import {
varchar,
} from "drizzle-orm/mysql-core";
import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
emptySuperJSON,
} from "@homarr/definitions";
import type {
BackgroundImageAttachment,
BackgroundImageRepeat,
BackgroundImageSize,
BoardLayoutMode,
BoardPermission,
ColorScheme,
GroupPermissionKey,
@@ -34,12 +39,6 @@ import type {
SupportedAuthProvider,
WidgetKind,
} from "@homarr/definitions";
import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
emptySuperJSON,
} from "@homarr/definitions";
const customBlob = customType<{ data: Buffer }>({
dataType() {
@@ -290,11 +289,6 @@ export const boards = mysqlTable("board", {
iconColor: text(),
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
disableStatus: boolean().default(false).notNull(),
// This has to be custom as the previous behaviour was custom
layoutMode: varchar({ length: 32 }).$type<BoardLayoutMode>().default("custom").notNull(),
baseLayoutId: varchar({ length: 64 }).references((): AnyMySqlColumn => layouts.id, {
onDelete: "set null",
}),
});
export const boardUserPermissions = mysqlTable(
@@ -684,10 +678,6 @@ export const boardRelations = relations(boards, ({ many, one }) => ({
userPermissions: many(boardUserPermissions),
groupPermissions: many(boardGroupPermissions),
layouts: many(layouts),
baseLayout: one(layouts, {
fields: [boards.baseLayoutId],
references: [layouts.id],
}),
groupHomes: many(groups, {
relationName: "groupRelations__board__homeBoardId",
}),
+6 -16
View File
@@ -16,11 +16,16 @@ import {
varchar,
} from "drizzle-orm/pg-core";
import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
emptySuperJSON,
} from "@homarr/definitions";
import type {
BackgroundImageAttachment,
BackgroundImageRepeat,
BackgroundImageSize,
BoardLayoutMode,
BoardPermission,
ColorScheme,
GroupPermissionKey,
@@ -33,12 +38,6 @@ import type {
SupportedAuthProvider,
WidgetKind,
} from "@homarr/definitions";
import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
emptySuperJSON,
} from "@homarr/definitions";
const customBlob = customType<{ data: Buffer }>({
dataType() {
@@ -289,11 +288,6 @@ export const boards = pgTable("board", {
iconColor: text(),
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
disableStatus: boolean().default(false).notNull(),
// This has to be custom as the previous behaviour was custom
layoutMode: varchar({ length: 32 }).$type<BoardLayoutMode>().default("custom").notNull(),
baseLayoutId: varchar({ length: 64 }).references((): AnyPgColumn => layouts.id, {
onDelete: "set null",
}),
});
export const boardUserPermissions = pgTable(
@@ -683,10 +677,6 @@ export const boardRelations = relations(boards, ({ many, one }) => ({
userPermissions: many(boardUserPermissions),
groupPermissions: many(boardGroupPermissions),
layouts: many(layouts),
baseLayout: one(layouts, {
fields: [boards.baseLayoutId],
references: [layouts.id],
}),
groupHomes: many(groups, {
relationName: "groupRelations__board__homeBoardId",
}),
+6 -16
View File
@@ -5,11 +5,16 @@ import { relations, sql } from "drizzle-orm";
import type { AnySQLiteColumn } from "drizzle-orm/sqlite-core";
import { blob, index, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
emptySuperJSON,
} from "@homarr/definitions";
import type {
BackgroundImageAttachment,
BackgroundImageRepeat,
BackgroundImageSize,
BoardLayoutMode,
BoardPermission,
ColorScheme,
GroupPermissionKey,
@@ -22,12 +27,6 @@ import type {
SupportedAuthProvider,
WidgetKind,
} from "@homarr/definitions";
import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
emptySuperJSON,
} from "@homarr/definitions";
export const apiKeys = sqliteTable("apiKey", {
id: text().notNull().primaryKey(),
@@ -275,11 +274,6 @@ export const boards = sqliteTable("board", {
iconColor: text(),
itemRadius: text().$type<MantineSize>().default("lg").notNull(),
disableStatus: int({ mode: "boolean" }).default(false).notNull(),
// This has to be custom as the previous behaviour was custom
layoutMode: text().$type<BoardLayoutMode>().default("custom").notNull(),
baseLayoutId: text().references((): AnySQLiteColumn => layouts.id, {
onDelete: "set null",
}),
});
export const boardUserPermissions = sqliteTable(
@@ -669,10 +663,6 @@ export const boardRelations = relations(boards, ({ many, one }) => ({
userPermissions: many(boardUserPermissions),
groupPermissions: many(boardGroupPermissions),
layouts: many(layouts),
baseLayout: one(layouts, {
fields: [boards.baseLayoutId],
references: [layouts.id],
}),
groupHomes: many(groups, {
relationName: "groupRelations__board__homeBoardId",
}),
+1 -1
View File
@@ -25,7 +25,7 @@
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "^5.3.2",
"zod": "^4.1.12"
"zod": "^4.1.13"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
-2
View File
@@ -8,9 +8,7 @@ export const backgroundImageRepeats = createDefinition(["repeat", "repeat-x", "r
export const backgroundImageSizes = createDefinition(["cover", "contain"], {
defaultValue: "cover",
});
export const boardLayoutModes = createDefinition(["auto", "custom"], { defaultValue: "auto" });
export type BackgroundImageAttachment = inferDefinitionType<typeof backgroundImageAttachments>;
export type BackgroundImageRepeat = inferDefinitionType<typeof backgroundImageRepeats>;
export type BackgroundImageSize = inferDefinitionType<typeof backgroundImageSizes>;
export type BoardLayoutMode = inferDefinitionType<typeof boardLayoutModes>;
@@ -144,6 +144,7 @@ export type HomarrDocumentationPath =
| "/docs/getting-started/installation/proxmox"
| "/docs/getting-started/installation/qnap"
| "/docs/getting-started/installation/railway"
| "/docs/getting-started/installation/runtipi"
| "/docs/getting-started/installation/saltbox"
| "/docs/getting-started/installation/source"
| "/docs/getting-started/installation/synology"
+2 -2
View File
@@ -26,9 +26,9 @@
"@homarr/common": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^8.3.8",
"@mantine/form": "^8.3.9",
"mantine-form-zod-resolver": "^1.3.0",
"zod": "^4.1.12"
"zod": "^4.1.13"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+4 -2
View File
@@ -1,7 +1,9 @@
import type { ChangeEvent, FocusEvent } from "react";
export interface InputPropsFor<T, TOnChangeArg, TComponent extends HTMLElement = HTMLInputElement>
extends BasePropsFor<TOnChangeArg, TComponent> {
export interface InputPropsFor<T, TOnChangeArg, TComponent extends HTMLElement = HTMLInputElement> extends BasePropsFor<
TOnChangeArg,
TComponent
> {
value?: T;
defaultValue?: T;
}
+3 -3
View File
@@ -30,9 +30,9 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.3.8",
"react": "19.2.0",
"zod": "^4.1.12"
"@mantine/core": "^8.3.9",
"react": "19.2.1",
"zod": "^4.1.13"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -16,7 +16,19 @@ interface UploadMediaProps {
export const UploadMedia = ({ children, onSettled, onSuccess, multiple = false }: UploadMediaProps) => {
const t = useI18n();
const { mutateAsync, isPending } = clientApi.media.uploadMedia.useMutation();
const { mutateAsync, isPending } = clientApi.media.uploadMedia.useMutation({
async onSuccess(mediaIds) {
await onSuccess?.(
mediaIds.map((id) => ({
id,
url: `/api/user-medias/${id}`,
})),
);
},
async onSettled() {
await onSettled?.();
},
});
const handleFileUploadAsync = async (files: File[] | File | null) => {
if (!files || (Array.isArray(files) && files.length === 0)) return;
@@ -24,25 +36,16 @@ export const UploadMedia = ({ children, onSettled, onSuccess, multiple = false }
const formData = new FormData();
filesArray.forEach((file) => formData.append("files", file));
await mutateAsync(formData, {
async onSuccess(mediaIds) {
onSuccess() {
showSuccessNotification({
message: t("media.action.upload.notification.success.message"),
});
await onSuccess?.(
mediaIds.map((id) => ({
id,
url: `/api/user-medias/${id}`,
})),
);
},
onError() {
showErrorNotification({
message: t("media.action.upload.notification.error.message"),
});
},
async onSettled() {
await onSettled?.();
},
});
};
+1 -1
View File
@@ -49,7 +49,7 @@
"tsdav": "^2.1.6",
"undici": "7.16.0",
"xml2js": "^0.6.2",
"zod": "^4.1.12"
"zod": "^4.1.13"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -46,6 +46,7 @@ export class TransmissionIntegration extends Integration implements IDownloadCli
name: torrent.name,
size: torrent.totalSize,
sent: torrent.uploadedEver,
received: torrent.downloadedEver,
downSpeed: torrent.percentDone !== 1 ? torrent.rateDownload : undefined,
upSpeed: torrent.rateUpload,
time:
@@ -26,6 +26,8 @@ export const downloadClientItemSchema = z.object({
size: z.number(),
/** Total uploaded in Bytes, only required for Torrent items */
sent: z.number().optional(),
/** Total downloaded in Bytes, only required for Torrent items */
received: z.number().optional(),
/** Download speed in Bytes/s, only required if not complete
* (Says 0 only if it should be downloading but isn't) */
downSpeed: z.number().optional(),
@@ -169,17 +169,15 @@ export class PlexIntegration extends Integration implements IMediaServerIntegrat
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const token = super.getSecretValue("apiKey");
const response = await input.fetchAsync(this.url("/"), {
const response = await input.fetchAsync(super.url("/prefs"), {
headers: {
"X-Plex-Token": token,
Accept: "application/json",
},
});
if (!response.ok) return TestConnectionError.StatusResult(response);
const result = await response.text();
await PlexIntegration.parseXmlAsync<PlexResponse>(result);
return { success: true };
}
+3 -3
View File
@@ -25,9 +25,9 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/core": "workspace:^0.1.0",
"superjson": "2.2.5",
"winston": "3.18.3",
"zod": "^4.1.12"
"superjson": "2.2.6",
"winston": "3.19.0",
"zod": "^4.1.13"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+5 -6
View File
@@ -24,7 +24,6 @@
"dependencies": {
"@homarr/api": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/form": "workspace:^0.1.0",
"@homarr/forms-collection": "workspace:^0.1.0",
"@homarr/modals": "workspace:^0.1.0",
@@ -34,13 +33,13 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.3.8",
"@mantine/core": "^8.3.9",
"@tabler/icons-react": "^3.35.0",
"dayjs": "^1.11.19",
"next": "16.0.3",
"react": "19.2.0",
"react-dom": "19.2.0",
"zod": "^4.1.12"
"next": "16.0.10",
"react": "19.2.1",
"react-dom": "19.2.1",
"zod": "^4.1.13"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -2,7 +2,6 @@ import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import type { MaybePromise } from "@homarr/common/types";
import { AppForm } from "@homarr/forms-collection";
import { createModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
@@ -10,7 +9,7 @@ import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { appManageSchema } from "@homarr/validation/app";
interface QuickAddAppModalProps {
onClose: (createdApp: Omit<RouterOutputs["app"]["create"], "appId">) => MaybePromise<void>;
onClose: (createdApp: Omit<RouterOutputs["app"]["create"], "appId">) => void;
}
export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, innerProps }) => {
@@ -28,13 +27,13 @@ export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, i
const handleSubmit = (values: z.infer<typeof appManageSchema>) => {
mutate(values, {
async onSuccess(app) {
onSuccess(app) {
showSuccessNotification({
title: tScoped("success.title"),
message: tScoped("success.message"),
});
await innerProps.onClose(app);
innerProps.onClose(app);
actions.closeModal();
},
});
@@ -1,28 +1,13 @@
import {
Badge,
Box,
Button,
Fieldset,
Group,
InputWrapper,
RadioGroup,
SimpleGrid,
Slider,
Stack,
Switch,
TextInput,
} from "@mantine/core";
import { Button, Group, InputWrapper, Slider, Stack, Switch, TextInput } from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import { IconAlertTriangle, IconCircleCheck } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { boardLayoutModes } from "@homarr/definitions";
import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { CustomRadioCard } from "@homarr/ui";
import { boardColumnCountSchema, boardCreateSchema, boardNameSchema } from "@homarr/validation/board";
export const AddBoardModal = createModal(({ actions }) => {
@@ -33,7 +18,6 @@ export const AddBoardModal = createModal(({ actions }) => {
name: "",
columnCount: 10,
isPublic: false,
layoutMode: boardLayoutModes.defaultValue,
},
});
const { mutate, isPending } = clientApi.board.createBoard.useMutation({
@@ -80,38 +64,14 @@ export const AddBoardModal = createModal(({ actions }) => {
) : null
}
/>
<Fieldset legend={t("board.setting.section.layout.title")}>
<Stack gap="sm">
<RadioGroup {...form.getInputProps("layoutMode")}>
<SimpleGrid cols={{ sm: 2, xs: 1 }} spacing="md">
{boardLayoutModes.values.map((mode) => (
<Box w="100%" key={mode} pos="relative">
<CustomRadioCard
value={mode}
label={t(`board.field.layoutMode.option.${mode}.label`)}
description={t(`board.field.layoutMode.option.${mode}.description`)}
/>
{mode === boardLayoutModes.defaultValue && (
<Badge pos="absolute" top="8px" right="8px" variant="light" size="sm">
{t("common.select.badge.recommended")}
</Badge>
)}
</Box>
))}
</SimpleGrid>
</RadioGroup>
<InputWrapper label={t("board.field.columnCount.label")} {...form.getInputProps("columnCount")}>
<Slider
min={boardColumnCountSchema.minValue ?? undefined}
max={boardColumnCountSchema.maxValue ?? undefined}
step={1}
{...form.getInputProps("columnCount")}
/>
</InputWrapper>
</Stack>
</Fieldset>
<InputWrapper label={t("board.field.columnCount.label")} {...form.getInputProps("columnCount")}>
<Slider
min={boardColumnCountSchema.minValue ?? undefined}
max={boardColumnCountSchema.maxValue ?? undefined}
step={1}
{...form.getInputProps("columnCount")}
/>
</InputWrapper>
<Switch
label={t("board.field.isPublic.label")}
@@ -132,7 +92,6 @@ export const AddBoardModal = createModal(({ actions }) => {
);
}).withOptions({
defaultTitle: (t) => t("management.page.board.action.new.label"),
size: "lg",
});
export const useBoardNameStatus = (name: string) => {
@@ -1,7 +1,7 @@
import { Button, Group, Stack, Text, TextInput } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import type { MaybePromise } from "@homarr/common/types";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
@@ -15,7 +15,6 @@ interface InnerProps {
id: string;
name: string;
};
onSuccess: () => MaybePromise<void>;
}
export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProps }) => {
@@ -27,7 +26,11 @@ export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProp
},
});
const boardNameStatus = useBoardNameStatus(form.values.name);
const { mutateAsync, isPending } = clientApi.board.duplicateBoard.useMutation();
const { mutateAsync, isPending } = clientApi.board.duplicateBoard.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/manage/boards");
},
});
return (
<form
@@ -40,13 +43,12 @@ export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProp
id: innerProps.board.id,
},
{
async onSuccess() {
onSuccess() {
actions.closeModal();
showSuccessNotification({
title: t("board.action.duplicate.notification.success.title"),
message: t("board.action.duplicate.notification.success.message"),
});
await innerProps.onSuccess();
},
onError() {
showErrorNotification({
@@ -65,7 +65,11 @@ export const ImportBoardModal = createModal(({ actions }) => {
},
);
const { mutateAsync, isPending } = clientApi.board.importOldmarrConfig.useMutation();
const { mutateAsync, isPending } = clientApi.board.importOldmarrConfig.useMutation({
async onSuccess() {
await revalidatePathActionAsync("/manage/boards");
},
});
const boardNameStatus = useBoardNameStatus(form.values.configuration.name);
const handleSubmitAsync = async (values: { file: File; configuration: OldmarrImportConfiguration }) => {
@@ -74,9 +78,8 @@ export const ImportBoardModal = createModal(({ actions }) => {
formData.set("configuration", JSON.stringify(values.configuration));
await mutateAsync(formData, {
async onSuccess() {
onSuccess() {
actions.closeModal();
await revalidatePathActionAsync("/manage/boards");
showSuccessNotification({
title: tOldImport("notification.success.title"),
message: tOldImport("notification.success.message"),
@@ -27,7 +27,11 @@ export const AddCertificateModal = createModal<InnerProps>(({ actions, innerProp
},
},
);
const { mutateAsync } = clientApi.certificates.addCertificate.useMutation();
const { mutateAsync } = clientApi.certificates.addCertificate.useMutation({
async onSuccess() {
await innerProps.onSuccess?.();
},
});
return (
<form
@@ -35,12 +39,11 @@ export const AddCertificateModal = createModal<InnerProps>(({ actions, innerProp
const formData = new FormData();
formData.set("file", values.file);
await mutateAsync(formData, {
async onSuccess() {
onSuccess() {
showSuccessNotification({
title: t("certificate.action.create.notification.success.title"),
message: t("certificate.action.create.notification.success.message"),
});
await innerProps.onSuccess?.();
actions.closeModal();
},
onError() {
+3 -3
View File
@@ -24,9 +24,9 @@
"dependencies": {
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^8.3.8",
"@mantine/hooks": "^8.3.8",
"react": "19.2.0"
"@mantine/core": "^8.3.9",
"@mantine/hooks": "^8.3.9",
"react": "19.2.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+1 -1
View File
@@ -24,7 +24,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/ui": "workspace:^0.1.0",
"@mantine/notifications": "^8.3.8",
"@mantine/notifications": "^8.3.9",
"@tabler/icons-react": "^3.35.0"
},
"devDependencies": {
+7 -7
View File
@@ -37,14 +37,14 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.3.8",
"@mantine/hooks": "^8.3.8",
"@mantine/core": "^8.3.9",
"@mantine/hooks": "^8.3.9",
"adm-zip": "0.5.16",
"next": "16.0.3",
"react": "19.2.0",
"react-dom": "19.2.0",
"superjson": "2.2.5",
"zod": "^4.1.12",
"next": "16.0.10",
"react": "19.2.1",
"react-dom": "19.2.1",
"superjson": "2.2.6",
"zod": "^4.1.13",
"zod-form-data": "^3.0.1"
},
"devDependencies": {
@@ -29,10 +29,10 @@ export const ImportTokenModal = createModal<InnerProps>(({ actions, innerProps }
mutate(
{ checksum: innerProps.checksum, token: values.token },
{
async onSuccess(isValid) {
onSuccess(isValid) {
if (isValid) {
actions.closeModal();
await innerProps.onSuccessAsync(values.token);
void innerProps.onSuccessAsync(values.token);
} else {
showErrorNotification({
title: tTokenModal("notification.error.title"),
+1 -1
View File
@@ -23,7 +23,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"zod": "^4.1.12"
"zod": "^4.1.13"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+1 -1
View File
@@ -28,7 +28,7 @@
"@homarr/definitions": "workspace:^",
"@homarr/log": "workspace:^",
"ioredis": "5.8.2",
"superjson": "2.2.5"
"superjson": "2.2.6"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+2 -2
View File
@@ -32,9 +32,9 @@
"@homarr/redis": "workspace:^0.1.0",
"dayjs": "^1.11.19",
"octokit": "^5.0.5",
"superjson": "2.2.5",
"superjson": "2.2.6",
"undici": "7.16.0",
"zod": "^4.1.12"
"zod": "^4.1.13"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+4 -4
View File
@@ -26,10 +26,10 @@
"@homarr/api": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/dates": "^8.3.8",
"next": "16.0.3",
"react": "19.2.0",
"react-dom": "19.2.0"
"@mantine/dates": "^8.3.9",
"next": "16.0.10",
"react": "19.2.1",
"react-dom": "19.2.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+7 -7
View File
@@ -33,14 +33,14 @@
"@homarr/settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^8.3.8",
"@mantine/hooks": "^8.3.8",
"@mantine/spotlight": "^8.3.8",
"@mantine/core": "^8.3.9",
"@mantine/hooks": "^8.3.9",
"@mantine/spotlight": "^8.3.9",
"@tabler/icons-react": "^3.35.0",
"jotai": "^2.15.1",
"next": "16.0.3",
"react": "19.2.0",
"react-dom": "19.2.0",
"jotai": "^2.15.2",
"next": "16.0.10",
"react": "19.2.1",
"react-dom": "19.2.1",
"use-deep-compare-effect": "^1.8.1"
},
"devDependencies": {
+4 -4
View File
@@ -32,10 +32,10 @@
"dayjs": "^1.11.19",
"deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.9",
"next": "16.0.3",
"next-intl": "4.5.5",
"react": "19.2.0",
"react-dom": "19.2.0"
"next": "16.0.10",
"next-intl": "4.5.8",
"react": "19.2.1",
"react-dom": "19.2.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+1 -1
View File
@@ -2232,7 +2232,7 @@
"unknown": "Ukendt",
"pending": "Afventende",
"processing": "Behandler",
"requested": "",
"requested": "Anmodet",
"partiallyAvailable": "Delvis",
"available": "Tilgængelig",
"blacklisted": "Blacklistet",
+17 -18
View File
@@ -1939,7 +1939,23 @@
"dockerContainers": {
"name": "Docker stats",
"description": "Stats of your containers (This widget can only be added with administrator privileges)",
"option": {},
"option": {
"enableRowSorting": {
"label": "Enable items sorting"
},
"defaultSort": {
"label": "Column used for sorting by default",
"option": {
"name": "Name",
"state": "State",
"cpuUsage": "CPU usage",
"memoryUsage": "Memory usage"
}
},
"descendingDefaultSort": {
"label": "Invert sorting"
}
},
"error": {
"internalServerError": "Failed to fetch containers stats"
}
@@ -2793,23 +2809,6 @@
"label": "Disable app status",
"description": "Disables the status check for all apps on this board"
},
"layoutMode": {
"label": "Layout mode",
"option": {
"auto": {
"label": "Auto",
"description": "Position and size of all layouts are calculated from the base layout."
},
"custom": {
"label": "Custom",
"description": "Position and size must be configured for all layouts separately."
}
}
},
"baseLayout": {
"label": "Base layout",
"description": "The layout will be used to calculate positions and sizes for other layouts."
},
"columnCount": {
"label": "Column count"
},
+1 -1
View File
@@ -2232,7 +2232,7 @@
"unknown": "Desconocido",
"pending": "Pendiente",
"processing": "Procesando",
"requested": "",
"requested": "Solicitado",
"partiallyAvailable": "Parcial",
"available": "Disponible",
"blacklisted": "Lista negra",
+2 -2
View File
@@ -747,7 +747,7 @@
},
"statusCode": {
"title": "Antwoord fout",
"description": "",
"description": "Onverwachte {statusCode} ({reason}) reactie van <url></url>. Controleer of de URL wijst naar de basis-URL van de integratie.",
"otherDescription": "",
"reason": {
"badRequest": "Onjuist verzoek",
@@ -2232,7 +2232,7 @@
"unknown": "Onbekend",
"pending": "In afwachting",
"processing": "Verwerken",
"requested": "",
"requested": "Aangevraagd",
"partiallyAvailable": "Gedeeltelijk",
"available": "Beschikbaar",
"blacklisted": "Geblacklist",
+1 -1
View File
@@ -341,7 +341,7 @@
"description": ""
},
"full-all": {
"label": "",
"label": "Acesso completo a aplicativos",
"description": ""
}
}
+6 -6
View File
@@ -30,14 +30,14 @@
"@homarr/log": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.3.8",
"@mantine/dates": "^8.3.8",
"@mantine/hooks": "^8.3.8",
"@mantine/core": "^8.3.9",
"@mantine/dates": "^8.3.9",
"@mantine/hooks": "^8.3.9",
"@tabler/icons-react": "^3.35.0",
"mantine-react-table": "2.0.0-beta.9",
"next": "16.0.3",
"react": "19.2.0",
"react-dom": "19.2.0",
"next": "16.0.10",
"react": "19.2.1",
"react-dom": "19.2.1",
"svgson": "^5.3.1"
},
"devDependencies": {
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff. Show more