Compare commits

..
Author SHA1 Message Date
copilot-swe-agent[bot] 4607a3ec33 Initial plan 2025-10-29 17:12:43 +00:00
homarr-renovate[bot] ad88c908f0 chore(deps): update dependency node to v24 2025-10-29 10:29:45 +00:00
196 changed files with 4043 additions and 11519 deletions

No files matched your search

-2
View File
@@ -33,8 +33,6 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.44.0
- 1.43.1
- 1.43.0
- 1.42.1
- 1.42.0
-15
View File
@@ -15,21 +15,6 @@
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
automerge: true,
},
{
// disallow automerge for onlyBuiltDependencies
matchPackageNames: [
"@tree-sitter-grammars/tree-sitter-yaml",
"bcrypt",
"better-sqlite3",
"cpu-features",
"esbuild",
"sharp",
"ssh2",
"tree-sitter",
"tree-sitter-json",
],
automerge: false,
},
],
updateInternalDeps: true,
rangeStrategy: "bump",
-2
View File
@@ -1,6 +1,4 @@
name: "[Quality] Code Analysis"
permissions:
contents: read
on:
pull_request:
-2
View File
@@ -1,6 +1,4 @@
name: "[Crowdin] Upload translations"
permissions:
contents: read
on:
workflow_dispatch:
@@ -61,10 +61,10 @@ jobs:
persist-credentials: false
- uses: pnpm/action-setup@v4
if: env.SKIP_RELEASE == 'false'
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
if: env.SKIP_RELEASE == 'false'
with:
node-version: 24.11.1
node-version: 24.10.0
cache: "pnpm"
- run: npm i -g pnpm
if: env.SKIP_RELEASE == 'false'
+1 -3
View File
@@ -9,8 +9,6 @@ env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
CROWDIN_TOKEN: "${{ secrets.CROWDIN_UPDATE_CONTRIBUTORS_TOKEN }}"
permissions:
contents: read
jobs:
update-contributors:
runs-on: ubuntu-latest
@@ -34,7 +32,7 @@ jobs:
- uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
-1
View File
@@ -3,4 +3,3 @@
# @link https://docs.expo.dev/guides/monorepos/#common-issues
node-linker=hoisted
strict-peer-dependencies=false
engine-strict=true
+1 -1
View File
@@ -1 +1 @@
24.11.1
22.21.0
-104
View File
@@ -1,107 +1,3 @@
## [1.45.0](https://github.com/homarr-labs/homarr/compare/v1.44.0...v1.45.0) (2025-11-28)
### Features
* **docker:** hide containers with label homarr.hide ([#4506](https://github.com/homarr-labs/homarr/issues/4506)) ([60ab98a](https://github.com/homarr-labs/homarr/commit/60ab98ae36abe712c825f7828394d3e3e98c888a))
### Bug Fixes
* Add fallback when no icon in Minecraft Server widget ([#4544](https://github.com/homarr-labs/homarr/issues/4544)) ([#4545](https://github.com/homarr-labs/homarr/issues/4545)) ([99949fe](https://github.com/homarr-labs/homarr/commit/99949fe095ac5a3616c8e9c1964d7eb253c9f826))
* **deps:** update dependency @tanstack/react-query to ^5.90.10 ([#4523](https://github.com/homarr-labs/homarr/issues/4523)) ([e87170a](https://github.com/homarr-labs/homarr/commit/e87170ae6e56cdbeef52e943353060329c39bdc9))
* **deps:** update dependency glob to ^11.1.0 ([#4529](https://github.com/homarr-labs/homarr/issues/4529)) ([24c9854](https://github.com/homarr-labs/homarr/commit/24c985424ecc795eb96b56881a9b00c0c37386c1))
* **deps:** update dependency glob to v12 ([#4530](https://github.com/homarr-labs/homarr/issues/4530)) ([18a6bfd](https://github.com/homarr-labs/homarr/commit/18a6bfd69d86de8c58dbfe6dc8b8fab78b2b5ee6))
* **deps:** update dependency next-intl to v4.5.4 ([#4540](https://github.com/homarr-labs/homarr/issues/4540)) ([77a86ea](https://github.com/homarr-labs/homarr/commit/77a86ead41c0d098987561249203667ac4d88177))
* **deps:** update dependency next-intl to v4.5.5 ([#4542](https://github.com/homarr-labs/homarr/issues/4542)) ([2bc6964](https://github.com/homarr-labs/homarr/commit/2bc6964f7efb5203f626d2f07b847dffd53191f0))
* **deps:** update dependency sass to ^1.94.1 ([#4531](https://github.com/homarr-labs/homarr/issues/4531)) ([292e36f](https://github.com/homarr-labs/homarr/commit/292e36fec5acff9b1d143f19e18ea170c87fa0d0))
* **deps:** update dependency zod-validation-error to v5 ([#4437](https://github.com/homarr-labs/homarr/issues/4437)) ([9a3feb4](https://github.com/homarr-labs/homarr/commit/9a3feb404c0c284b4f33fce12d87a469741a6ebd))
* **security:** restrict modification of entrypoint script ([#4555](https://github.com/homarr-labs/homarr/issues/4555)) ([524e1a4](https://github.com/homarr-labs/homarr/commit/524e1a485be56dbebf8f71d4546bc65672737f30))
## [1.44.0](https://github.com/homarr-labs/homarr/compare/v1.43.3...v1.44.0) (2025-11-21)
### Features
* **pi-hole:** support no-auth ([#4480](https://github.com/homarr-labs/homarr/issues/4480)) ([c89e35e](https://github.com/homarr-labs/homarr/commit/c89e35e13c794bbdf98748c00644b86f7f5dbe68))
### Bug Fixes
* **deps:** update dependency @tanstack/react-query to ^5.90.8 ([#4494](https://github.com/homarr-labs/homarr/issues/4494)) ([a5b6203](https://github.com/homarr-labs/homarr/commit/a5b6203be6ac3c2308614f10ef6a17590917e2c9))
* **deps:** update dependency @tanstack/react-query to ^5.90.9 ([#4508](https://github.com/homarr-labs/homarr/issues/4508)) ([b3db5d5](https://github.com/homarr-labs/homarr/commit/b3db5d5f7bc2d146f0b2c230a52e7bd4efad3bcb))
* **deps:** update dependency @tanstack/react-query-next-experimental to ^5.91.0 ([#4505](https://github.com/homarr-labs/homarr/issues/4505)) ([8ea6cff](https://github.com/homarr-labs/homarr/commit/8ea6cffb537ca54a413c583ab8b8fd24a52185be))
* **deps:** update dependency drizzle-kit to ^0.31.7 ([#4507](https://github.com/homarr-labs/homarr/issues/4507)) ([b4f9fb8](https://github.com/homarr-labs/homarr/commit/b4f9fb8b46a8f74e2d7896dfe98e80618cfbcde4))
* **deps:** update dependency fast-xml-parser to ^5.3.2 ([#4510](https://github.com/homarr-labs/homarr/issues/4510)) ([5e57a49](https://github.com/homarr-labs/homarr/commit/5e57a4906bcedc53d220e983827293f4a699c425))
* **deps:** update dependency next-intl to v4.5.1 ([#4484](https://github.com/homarr-labs/homarr/issues/4484)) ([06942ec](https://github.com/homarr-labs/homarr/commit/06942ecbcd245f5540f2960f437ca2955a5ed0c3))
* **deps:** update dependency next-intl to v4.5.3 ([#4503](https://github.com/homarr-labs/homarr/issues/4503)) ([0642773](https://github.com/homarr-labs/homarr/commit/064277373cacd3309d8da3779d5451d195da7f81))
* **deps:** update dependency sass to ^1.94.0 ([#4482](https://github.com/homarr-labs/homarr/issues/4482)) ([19ecede](https://github.com/homarr-labs/homarr/commit/19ecedea4370924d47545ca8d9d46a0b7c016cc9))
* **deps:** update mantine monorepo to ^8.3.8 ([#4516](https://github.com/homarr-labs/homarr/issues/4516)) ([ded4e72](https://github.com/homarr-labs/homarr/commit/ded4e72e5407b0076495a06111f3b04445f27276))
* **deps:** update nextjs monorepo to v16.0.3 ([#4504](https://github.com/homarr-labs/homarr/issues/4504)) ([7d0c1a1](https://github.com/homarr-labs/homarr/commit/7d0c1a14fdf1731ff44e3d00793cf4cd1c3e9b1f))
* **deps:** update testcontainers-node monorepo to ^11.8.1 ([#4486](https://github.com/homarr-labs/homarr/issues/4486)) ([64657d8](https://github.com/homarr-labs/homarr/commit/64657d831cbe28a59708b280b07889b9f0232743))
* **deps:** update turbo monorepo to ^2.6.1 ([#4483](https://github.com/homarr-labs/homarr/issues/4483)) ([cd124d6](https://github.com/homarr-labs/homarr/commit/cd124d6e8392eef5df0092481ec3cc26599f2902))
* **iframe:** allow clicking links within ([#4521](https://github.com/homarr-labs/homarr/issues/4521)) ([5a418d7](https://github.com/homarr-labs/homarr/commit/5a418d73bdb07cf3d3270983b6881f2de8d58ad7))
* **media-requests:** incorrect availability mapping ([#4520](https://github.com/homarr-labs/homarr/issues/4520)) ([d9cc35b](https://github.com/homarr-labs/homarr/commit/d9cc35b9859a14f44c4e4b13c6518287f8d18e54))
## [1.43.3](https://github.com/homarr-labs/homarr/compare/v1.43.2...v1.43.3) (2025-11-14)
### Bug Fixes
* **deps:** update dependency @ctrl/qbittorrent to ^9.11.0 ([#4456](https://github.com/homarr-labs/homarr/issues/4456)) ([ddfed18](https://github.com/homarr-labs/homarr/commit/ddfed18acf44596dba15ea0ef56233c4936f9775))
* **deps:** update dependency @tanstack/react-query to ^5.90.7 ([#4451](https://github.com/homarr-labs/homarr/issues/4451)) ([7bc2f87](https://github.com/homarr-labs/homarr/commit/7bc2f87f31f1babe691fa4a086a3897977f2d871))
* **deps:** update dependency fast-xml-parser to ^5.3.1 ([#4435](https://github.com/homarr-labs/homarr/issues/4435)) ([65f2e82](https://github.com/homarr-labs/homarr/commit/65f2e825fdb9e5e79b814c9b49f663d1afac784d))
* **deps:** update dependency fastify to ^5.6.2 ([#4471](https://github.com/homarr-labs/homarr/issues/4471)) ([e7071bf](https://github.com/homarr-labs/homarr/commit/e7071bf2bf6a45ff760cd3e7bd06382d2ed66cdc))
* **deps:** update dependency next-intl to v4.5.0 ([#4462](https://github.com/homarr-labs/homarr/issues/4462)) ([51ce5da](https://github.com/homarr-labs/homarr/commit/51ce5da45bdccf061e593097ed751cf09d663437))
* **deps:** update dependency swagger-ui-react to ^5.30.2 ([#4442](https://github.com/homarr-labs/homarr/issues/4442)) ([d3edfcf](https://github.com/homarr-labs/homarr/commit/d3edfcf3dca3d34afd5ef8ec21599cd08fde5092))
* **deps:** update mantine monorepo to ^8.3.7 ([#4472](https://github.com/homarr-labs/homarr/issues/4472)) ([8a87dba](https://github.com/homarr-labs/homarr/commit/8a87dbad59786fa9789dd3efb8d9424bb950e977))
* **deps:** update testcontainers-node monorepo to ^11.8.0 ([#4450](https://github.com/homarr-labs/homarr/issues/4450)) ([f648af0](https://github.com/homarr-labs/homarr/commit/f648af0e2b2b5d5f8180402c2734ea0c04559daa))
* **stock-price:** ignore missing data points in price history ([#4429](https://github.com/homarr-labs/homarr/issues/4429)) ([c30c98b](https://github.com/homarr-labs/homarr/commit/c30c98b153fbec21551e45aedf83e0ae29d6e773))
* **users:** next button in create form always disabled ([#4443](https://github.com/homarr-labs/homarr/issues/4443)) ([1c1f8a4](https://github.com/homarr-labs/homarr/commit/1c1f8a48d259c65ebeeec6856e1fe339defb4aaf))
## [1.43.2](https://github.com/homarr-labs/homarr/compare/v1.43.1...v1.43.2) (2025-11-07)
### Bug Fixes
* **deps:** update dependency @ctrl/deluge to ^7.5.0 ([#4407](https://github.com/homarr-labs/homarr/issues/4407)) ([57d1a91](https://github.com/homarr-labs/homarr/commit/57d1a9186653dd4c327b360a3bdaa78fd8e1187b))
* **deps:** update dependency @gitbeaker/rest to ^43.7.0 ([#4386](https://github.com/homarr-labs/homarr/issues/4386)) ([dcf960f](https://github.com/homarr-labs/homarr/commit/dcf960fa97011c24475f303d587152b42eabec45))
* **deps:** update dependency @gitbeaker/rest to ^43.8.0 ([#4425](https://github.com/homarr-labs/homarr/issues/4425)) ([4516f27](https://github.com/homarr-labs/homarr/commit/4516f27fa69445e5aec16760e1f6ed8080bc8fff))
* **deps:** update dependency @jellyfin/sdk to ^0.13.0 ([#4399](https://github.com/homarr-labs/homarr/issues/4399)) ([4c4e901](https://github.com/homarr-labs/homarr/commit/4c4e901e7d38faf83a4725b7cee5526b0b223dc3))
* **deps:** update dependency @octokit/auth-app to ^8.1.2 ([#4412](https://github.com/homarr-labs/homarr/issues/4412)) ([018a33b](https://github.com/homarr-labs/homarr/commit/018a33b07812fa59f1469ce8b4baa7b1d55c6103))
* **deps:** update dependency @tanstack/react-query to ^5.90.6 ([#4427](https://github.com/homarr-labs/homarr/issues/4427)) ([5f9054b](https://github.com/homarr-labs/homarr/commit/5f9054b162daf974baafdbee9f844b3a860cbdd0))
* **deps:** update dependency dayjs to ^1.11.19 ([#4414](https://github.com/homarr-labs/homarr/issues/4414)) ([97cbade](https://github.com/homarr-labs/homarr/commit/97cbade7135cb06e0780dd1b448c78c8802794ea))
* **deps:** update dependency drizzle-kit to ^0.31.6 ([#4397](https://github.com/homarr-labs/homarr/issues/4397)) ([59434ee](https://github.com/homarr-labs/homarr/commit/59434ee33ae16e06f96e2f07cf85337c313c73f6))
* **deps:** update dependency jotai to ^2.15.1 ([#4426](https://github.com/homarr-labs/homarr/issues/4426)) ([07af9d8](https://github.com/homarr-labs/homarr/commit/07af9d8673ea9573bc436626cd84d11980d023bc))
* **deps:** update dependency octokit to ^5.0.5 ([#4413](https://github.com/homarr-labs/homarr/issues/4413)) ([186e3e4](https://github.com/homarr-labs/homarr/commit/186e3e445dfe6b4fb1aeda3830ae1f260b605882))
* **deps:** update dependency sass to ^1.93.3 ([#4421](https://github.com/homarr-labs/homarr/issues/4421)) ([f216427](https://github.com/homarr-labs/homarr/commit/f2164272a5ca9b51bdf9110664ed5a40b87b60f6))
* **deps:** update dependency superjson to v2.2.5 ([#4398](https://github.com/homarr-labs/homarr/issues/4398)) ([19e25a0](https://github.com/homarr-labs/homarr/commit/19e25a095fb0252e8f2a459e0a97d44a37d2188a))
* **deps:** update dependency swagger-ui-react to ^5.30.0 ([#4391](https://github.com/homarr-labs/homarr/issues/4391)) ([b51e285](https://github.com/homarr-labs/homarr/commit/b51e285c92ef707076f321f0ce1a15d5cbd5de7d))
* **deps:** update dependency swagger-ui-react to ^5.30.1 ([#4418](https://github.com/homarr-labs/homarr/issues/4418)) ([d9b14db](https://github.com/homarr-labs/homarr/commit/d9b14db6a1af515796951a1db670431d10f81eaf))
* **deps:** update dependency tsdav to ^2.1.6 ([#4417](https://github.com/homarr-labs/homarr/issues/4417)) ([160c20a](https://github.com/homarr-labs/homarr/commit/160c20a9a6f850f75537e246f42e93f9a2ed79a0))
* **deps:** update mantine monorepo to ^8.3.6 ([#4403](https://github.com/homarr-labs/homarr/issues/4403)) ([9e2bbb8](https://github.com/homarr-labs/homarr/commit/9e2bbb8a3f2db95c5598e5dca993bf6cd4ebc127))
* **deps:** update nextjs monorepo to v16 (major) ([#4363](https://github.com/homarr-labs/homarr/issues/4363)) ([6ce23a6](https://github.com/homarr-labs/homarr/commit/6ce23a6e971b064292970fc4df90cc69cdb59a95))
* **deps:** update tiptap monorepo to v2.27.0 ([#4404](https://github.com/homarr-labs/homarr/issues/4404)) ([f4caddf](https://github.com/homarr-labs/homarr/commit/f4caddfd564553014854347eb6dd14cc800eb027))
* **deps:** update tiptap monorepo to v2.27.1 ([#4405](https://github.com/homarr-labs/homarr/issues/4405)) ([d34f9e5](https://github.com/homarr-labs/homarr/commit/d34f9e5b3ef999559a4c5f1165488fe4f8828a83))
* **deps:** update trpc monorepo to ^11.7.1 ([#4396](https://github.com/homarr-labs/homarr/issues/4396)) ([5aca5d1](https://github.com/homarr-labs/homarr/commit/5aca5d1b0a76ab4cf0ed27dac62183c766ad398a))
* **deps:** update turbo monorepo to ^2.6.0 ([#4422](https://github.com/homarr-labs/homarr/issues/4422)) ([742ef66](https://github.com/homarr-labs/homarr/commit/742ef66e3cf56fd351c8dee2d3dd05cd639e51fa))
## [1.43.1](https://github.com/homarr-labs/homarr/compare/v1.43.0...v1.43.1) (2025-10-31)
### Bug Fixes
* **deps:** update dependency @auth/core to ^0.41.1 ([#4387](https://github.com/homarr-labs/homarr/issues/4387)) ([747bb92](https://github.com/homarr-labs/homarr/commit/747bb922ccf0508bd00c37faeab29e28876577b1))
* **deps:** update dependency @auth/drizzle-adapter to ^1.11.1 ([#4388](https://github.com/homarr-labs/homarr/issues/4388)) ([2fe3656](https://github.com/homarr-labs/homarr/commit/2fe36564c26c33ae2625eb22ef4503c6dd8ebcf9))
* **deps:** update dependency @jellyfin/sdk to ^0.12.0 ([#4359](https://github.com/homarr-labs/homarr/issues/4359)) ([554d19a](https://github.com/homarr-labs/homarr/commit/554d19a458e856141f90e795ecebabf205e945e9))
* **deps:** update dependency @paralleldrive/cuid2 to v3 ([#4343](https://github.com/homarr-labs/homarr/issues/4343)) ([074d27e](https://github.com/homarr-labs/homarr/commit/074d27e37e3430e7e98cf28e2876afe009811027))
* **deps:** update dependency drizzle-orm to ^0.44.7 ([#4372](https://github.com/homarr-labs/homarr/issues/4372)) ([8242bfc](https://github.com/homarr-labs/homarr/commit/8242bfc95cd0e67397b1bfc46d65aec3f568c304))
* **deps:** update dependency ioredis to v5.8.2 ([#4357](https://github.com/homarr-labs/homarr/issues/4357)) ([3c4c26e](https://github.com/homarr-labs/homarr/commit/3c4c26ee214a37193aea31b69ce75322862fc534))
* **deps:** update dependency mysql2 to v3.15.3 ([#4360](https://github.com/homarr-labs/homarr/issues/4360)) ([6e6c912](https://github.com/homarr-labs/homarr/commit/6e6c9122257934402848d6fdc51e6ccdfc2dcca0))
* **deps:** update dependency next-auth to v5.0.0-beta.30 [security] ([#4379](https://github.com/homarr-labs/homarr/issues/4379)) ([c544d6e](https://github.com/homarr-labs/homarr/commit/c544d6ec2e426bc4c82db547d7c4a5dc4d6e2a27))
* **deps:** update dependency next-intl to v4.4.0 ([#4365](https://github.com/homarr-labs/homarr/issues/4365)) ([ce63388](https://github.com/homarr-labs/homarr/commit/ce6338840c20fc46048687ed5566ff12fa4b2502))
* **deps:** update dependency node-ical to ^0.22.1 ([#4346](https://github.com/homarr-labs/homarr/issues/4346)) ([ef18c58](https://github.com/homarr-labs/homarr/commit/ef18c58f5b09bafd697365a0182ab7bc8719db2b))
* **deps:** update dependency superjson to v2.2.3 ([#4364](https://github.com/homarr-labs/homarr/issues/4364)) ([18cc04e](https://github.com/homarr-labs/homarr/commit/18cc04e7f2740226f7bc7c75c0b0cdaec7cda6f8))
* **deps:** update dependency typescript-eslint to ^8.46.2 ([#4352](https://github.com/homarr-labs/homarr/issues/4352)) ([f96cbe0](https://github.com/homarr-labs/homarr/commit/f96cbe06525515112fad466cb8a5f33d74517aba))
* **deps:** update testcontainers-node monorepo to ^11.7.2 ([#4366](https://github.com/homarr-labs/homarr/issues/4366)) ([7329135](https://github.com/homarr-labs/homarr/commit/7329135fd85906602a16a9bdc16a1ef1cc4607c5))
* **deps:** update tiptap monorepo to v2.26.4 ([#4371](https://github.com/homarr-labs/homarr/issues/4371)) ([136ed8e](https://github.com/homarr-labs/homarr/commit/136ed8e8b401b801ba16f2079bdfb5aeebe9f57f))
* **deps:** update trpc monorepo to ^11.7.0 ([#4385](https://github.com/homarr-labs/homarr/issues/4385)) ([24def99](https://github.com/homarr-labs/homarr/commit/24def9982709cd6de1765a7235169fe1ceb3cbea))
* **media-releases:** support more media type mappings for jellyfin and emby ([#4382](https://github.com/homarr-labs/homarr/issues/4382)) ([6f3b7ec](https://github.com/homarr-labs/homarr/commit/6f3b7ec4915754e183300a915baebc11f264df62))
* **releases-widget:** Search multiple docker hub pages for releases ([#4158](https://github.com/homarr-labs/homarr/issues/4158)) ([e0526c5](https://github.com/homarr-labs/homarr/commit/e0526c5ea7ee83adcd22d78fbf63334cb59bcf79))
## [1.43.0](https://github.com/homarr-labs/homarr/compare/v1.42.1...v1.43.0) (2025-10-24)
### Features
+2 -2
View File
@@ -1,4 +1,4 @@
FROM node:24.11.1-alpine AS base
FROM node:24.10.0-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
@@ -55,7 +55,7 @@ COPY --from=builder /app/apps/nextjs/.next/standalone ./
COPY --from=builder /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
COPY --from=builder /app/apps/nextjs/public ./apps/nextjs/public
COPY scripts/run.sh ./run.sh
COPY --chmod=755 scripts/entrypoint.sh ./entrypoint.sh
COPY --chmod=777 scripts/entrypoint.sh ./entrypoint.sh
COPY packages/redis/redis.conf /app/redis.conf
COPY nginx.conf /etc/nginx/templates/nginx.conf
+4 -11
View File
@@ -20,26 +20,19 @@ const withNextIntl = createNextIntlPlugin({
const nextConfig: NextConfig = {
output: "standalone",
reactStrictMode: true,
reactCompiler: true,
/** We already do typechecking as separate tasks in CI */
/** We already do linting and typechecking as separate tasks in CI */
eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
/**
* dockerode is required in the external server packages because of https://github.com/homarr-labs/homarr/issues/612
* isomorphic-dompurify and jsdom are required, see https://github.com/kkomelin/isomorphic-dompurify/issues/356
*/
serverExternalPackages: ["dockerode", "isomorphic-dompurify", "jsdom"],
serverExternalPackages: ["dockerode"],
experimental: {
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
turbopackFileSystemCacheForDev: true,
},
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
images: {
localPatterns: [
{
pathname: "/**",
search: "",
},
],
domains: ["cdn.jsdelivr.net"],
},
// eslint-disable-next-line @typescript-eslint/require-await,no-restricted-syntax
async headers() {
+24 -26
View File
@@ -50,44 +50,42 @@
"@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.5",
"@mantine/core": "^8.3.5",
"@mantine/dropzone": "^8.3.5",
"@mantine/hooks": "^8.3.5",
"@mantine/modals": "^8.3.5",
"@mantine/tiptap": "^8.3.5",
"@million/lint": "1.0.14",
"@tabler/icons-react": "^3.35.0",
"@tanstack/react-query": "^5.90.10",
"@tanstack/react-query": "^5.90.5",
"@tanstack/react-query-devtools": "^5.90.2",
"@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",
"@tanstack/react-query-next-experimental": "^5.90.2",
"@trpc/client": "^11.6.0",
"@trpc/next": "^11.6.0",
"@trpc/react-query": "^11.6.0",
"@trpc/server": "^11.6.0",
"@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",
"clsx": "^2.1.1",
"dayjs": "^1.11.19",
"dayjs": "^1.11.18",
"dotenv": "^17.2.3",
"flag-icons": "^7.5.0",
"glob": "^12.0.0",
"isomorphic-dompurify": "^2.32.0",
"jotai": "^2.15.1",
"glob": "^11.0.3",
"jotai": "^2.15.0",
"mantine-react-table": "2.0.0-beta.9",
"next": "16.0.3",
"next": "15.5.6",
"postcss-preset-mantine": "^1.18.0",
"prismjs": "^1.30.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"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.93.2",
"superjson": "2.2.3",
"swagger-ui-react": "^5.29.5",
"use-deep-compare-effect": "^1.8.1",
"zod": "^4.1.12"
},
@@ -95,14 +93,14 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.2",
"@types/node": "^24.10.1",
"@types/chroma-js": "3.1.1",
"@types/node": "^24.9.1",
"@types/prismjs": "^1.26.5",
"@types/react": "19.2.6",
"@types/react-dom": "19.2.3",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.2.1",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"node-loader": "^2.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3"
@@ -1,6 +1,7 @@
"use client";
import { useCallback, useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Group, Menu, ScrollArea } from "@mantine/core";
import { useHotkeys } from "@mantine/hooks";
@@ -17,7 +18,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";
@@ -28,7 +29,6 @@ import { useConfirmModal, useModalAction } from "@homarr/modals";
import { AppSelectModal } from "@homarr/modals-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
import { useItemActions } from "~/components/board/items/item-actions";
import { ItemSelectModal } from "~/components/board/items/item-select-modal";
@@ -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")}
@@ -1,3 +1,4 @@
import Link from "next/link";
import type { MantineColor } from "@mantine/core";
import { Button, Card, Stack, Text } from "@mantine/core";
import { IconBook2, IconCategoryPlus, IconLayoutDashboard, IconMailForward } from "@tabler/icons-react";
@@ -7,7 +8,6 @@ import { getMantineColor } from "@homarr/common";
import { db } from "@homarr/db";
import { createDocumentationLink } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { Link } from "@homarr/ui";
import type { TablerIcon } from "@homarr/ui";
export const InitFinish = async () => {
@@ -1,4 +1,5 @@
import { Fragment } from "react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
import { IconBox, IconPencil } from "@tabler/icons-react";
@@ -9,7 +10,7 @@ import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { Link, SearchInput, TablePagination } from "@homarr/ui";
import { SearchInput, TablePagination } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
@@ -1,6 +1,7 @@
"use client";
import { useCallback } from "react";
import Link from "next/link";
import { Menu } from "@mantine/core";
import { IconCopy, IconDeviceMobile, IconHome, IconSettings, IconTrash } from "@tabler/icons-react";
@@ -11,7 +12,6 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useConfirmModal, useModalAction } from "@homarr/modals";
import { DuplicateBoardModal } from "@homarr/modals-collection";
import { useScopedI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
import { useBoardPermissions } from "~/components/board/permissions/client";
@@ -1,3 +1,4 @@
import Link from "next/link";
import {
ActionIcon,
Badge,
@@ -20,7 +21,7 @@ import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { getScopedI18n } from "@homarr/translation/server";
import { Link, UserAvatar } from "@homarr/ui";
import { UserAvatar } from "@homarr/ui";
import { getBoardPermissionsAsync } from "~/components/board/permissions/server";
import { ManageContainer } from "~/components/manage/manage-container";
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Alert, Anchor, Button, ButtonGroup, Fieldset, Group, Stack, Text, TextInput } from "@mantine/core";
import { IconInfoCircle, IconPencil, IconPlus, IconUnlink } from "@tabler/icons-react";
@@ -16,7 +17,6 @@ import { useConfirmModal, useModalAction } from "@homarr/modals";
import { AppSelectModal } from "@homarr/modals-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
import { integrationUpdateSchema } from "@homarr/validation/integration";
import { SecretCard } from "../../_components/secrets/integration-secret-card";
@@ -2,12 +2,13 @@
import type { ChangeEvent } from "react";
import React, { useMemo, useState } from "react";
import Link from "next/link";
import { Flex, Group, Menu, ScrollArea, Text, TextInput } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
import { getIntegrationName, integrationKinds } from "@homarr/definitions";
import { useI18n } from "@homarr/translation/client";
import { IntegrationAvatar, Link } from "@homarr/ui";
import { IntegrationAvatar } from "@homarr/ui";
interface IntegrationCreateDropdownContentProps {
enableMockIntegration: boolean;
@@ -1,6 +1,7 @@
"use client";
import { startTransition, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
Alert,
@@ -35,7 +36,6 @@ import type { GetInputPropsReturnType, UseFormReturnType } from "@homarr/form";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
import { appHrefSchema } from "@homarr/validation/app";
import { integrationCreateSchema } from "@homarr/validation/integration";
@@ -1,5 +1,6 @@
import { Fragment } from "react";
import type { PropsWithChildren } from "react";
import Link from "next/link";
import { redirect } from "next/navigation";
import {
AccordionControl,
@@ -35,7 +36,7 @@ import { objectEntries } from "@homarr/common";
import type { IntegrationKind } from "@homarr/definitions";
import { getIntegrationName } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { CountBadge, IntegrationAvatar, Link } from "@homarr/ui";
import { CountBadge, IntegrationAvatar } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
@@ -1,3 +1,4 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import {
ActionIcon,
@@ -24,7 +25,7 @@ import { humanFileSize } from "@homarr/common";
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { createLocalImageUrl } from "@homarr/icons/local";
import { getI18n } from "@homarr/translation/server";
import { Link, SearchInput, TablePagination, UserAvatar } from "@homarr/ui";
import { SearchInput, TablePagination, UserAvatar } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
+1 -1
View File
@@ -1,10 +1,10 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Card, Group, SimpleGrid, Space, Stack, Text } from "@mantine/core";
import { IconArrowRight } from "@tabler/icons-react";
import { api } from "@homarr/api/server";
import { getScopedI18n } from "@homarr/translation/server";
import { Link } from "@homarr/ui";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { createMetaTitle } from "~/metadata";
@@ -1,5 +1,6 @@
"use client";
import Link from "next/link";
import type { SegmentedControlItem } from "@mantine/core";
import { Button, Fieldset, Grid, Group, SegmentedControl, Stack, Textarea, TextInput } from "@mantine/core";
import { WidgetIntegrationSelect } from "node_modules/@homarr/widgets/src/widget-integration-select";
@@ -11,7 +12,6 @@ import { useZodForm } from "@homarr/form";
import { IconPicker } from "@homarr/forms-collection";
import type { TranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
import { searchEngineManageSchema } from "@homarr/validation/search-engine";
type FormType = z.infer<typeof searchEngineManageSchema>;
@@ -1,3 +1,4 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
import { IconPencil, IconSearch } from "@tabler/icons-react";
@@ -8,7 +9,7 @@ import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { Link, SearchInput, TablePagination } from "@homarr/ui";
import { SearchInput, TablePagination } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
@@ -1,4 +1,5 @@
import { X509Certificate } from "node:crypto";
import Link from "next/link";
import { notFound } from "next/navigation";
import {
Button,
@@ -18,7 +19,6 @@ import { IconCertificateOff } from "@tabler/icons-react";
import { auth } from "@homarr/auth/next";
import { getTrustedCertificateHostnamesAsync } from "@homarr/certificates/server";
import { getI18n } from "@homarr/translation/server";
import { Link } from "@homarr/ui";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NoResults } from "~/components/no-results";
@@ -1,4 +1,5 @@
import { X509Certificate } from "node:crypto";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Button, Card, Group, SimpleGrid, Stack, Text, Title } from "@mantine/core";
import { IconAlertTriangle, IconCertificate, IconCertificateOff } from "@tabler/icons-react";
@@ -9,7 +10,6 @@ import { loadCustomRootCertificatesAsync } from "@homarr/certificates/server";
import { getMantineColor } from "@homarr/common";
import type { SupportedLanguage } from "@homarr/translation";
import { getI18n } from "@homarr/translation/server";
import { Link } from "@homarr/ui";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NoResults } from "~/components/no-results";
@@ -1,10 +1,10 @@
"use client";
import Link from "next/link";
import { Anchor, Center, Stack, Text } from "@mantine/core";
import { IconShipOff } from "@tabler/icons-react";
import { useI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
export default function DockerErrorPage() {
const t = useI18n();
@@ -1,10 +1,10 @@
"use client";
import Link from "next/link";
import { Anchor, Center, Stack, Text } from "@mantine/core";
import { IconCubeOff } from "@tabler/icons-react";
import { useI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
export default function KubernetesErrorPage() {
const t = useI18n();
@@ -1,11 +1,11 @@
import React from "react";
import Image from "next/image";
import Link from "next/link";
import { Card, Group, Text } from "@mantine/core";
import { IconArrowRight } from "@tabler/icons-react";
import type { KubernetesLabelResourceType } from "@homarr/definitions";
import { useI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
import classes from "./resource-tile.module.css";
@@ -1,6 +1,7 @@
"use client";
import { useMemo } from "react";
import Link from "next/link";
import { Anchor, Button, Group, Text, Title, Tooltip } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
import type { MRT_ColumnDef } from "mantine-react-table";
@@ -9,7 +10,7 @@ import { MantineReactTable } from "mantine-react-table";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { Link, UserAvatar } from "@homarr/ui";
import { UserAvatar } from "@homarr/ui";
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
interface UserListComponentProps {
@@ -1,6 +1,6 @@
"use client";
import { startTransition, useCallback, useState } from "react";
import { startTransition, useCallback, useMemo, useState } from "react";
import {
Badge,
Button,
@@ -20,9 +20,8 @@ import { IconPlus, IconUserCheck } from "@tabler/icons-react";
import { z } from "zod/v4";
import { clientApi } from "@homarr/api/client";
import type { GroupPermissionKey } from "@homarr/definitions";
import { everyoneGroup, groupPermissions } from "@homarr/definitions";
import type { IsValid } from "@homarr/form";
import type { GroupPermissionKey } from "@homarr/definitions";
import { useZodForm } from "@homarr/form";
import { useModalAction } from "@homarr/modals";
import { showErrorNotification } from "@homarr/notifications";
@@ -114,9 +113,11 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
},
);
const allForms = [generalForm, securityForm, groupsForm];
const isValidCallback: IsValid<unknown> | undefined = allForms[active]?.isValid;
const currentFormValid = isValidCallback?.() ?? true;
const allForms = useMemo(() => [generalForm, securityForm, groupsForm], [generalForm, securityForm, groupsForm]);
const activeForm = allForms[active];
const isCurrentFormValid = activeForm ? activeForm.isValid : () => true;
const canNavigateToNextStep = isCurrentFormValid();
const controlledGoToNextStep = useCallback(async () => {
if (active + 1 === stepperMax) {
@@ -217,7 +218,7 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
</Stepper.Completed>
</Stepper>
<StepperNavigationComponent
hasNext={hasNext && currentFormValid}
hasNext={hasNext && canNavigateToNextStep}
hasPrevious={hasPrevious}
isComplete={active === stepperMax}
isLoadingNextStep={isPending}
@@ -1,8 +1,8 @@
import Link from "next/link";
import { Button, Card, Group } from "@mantine/core";
import { IconArrowBackUp, IconArrowLeft, IconArrowRight, IconRotate } from "@tabler/icons-react";
import { useI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
interface StepperNavigationComponentProps {
hasPrevious: boolean;
@@ -1,11 +1,10 @@
"use client";
import type { ReactNode } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { NavLink } from "@mantine/core";
import { Link } from "@homarr/ui";
interface NavigationLinkProps {
href: string;
label: string;
@@ -1,11 +1,11 @@
"use client";
import Link from "next/link";
import { Alert, Anchor } from "@mantine/core";
import { IconExclamationCircle } from "@tabler/icons-react";
import { createDocumentationLink } from "@homarr/definitions";
import { useI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
export const ReservedGroupAlert = () => {
const t = useI18n();
@@ -1,10 +1,10 @@
import type { PropsWithChildren } from "react";
import Link from "next/link";
import { Button, Grid, GridCol, Group, Stack, Text, Title } from "@mantine/core";
import { IconId, IconLock, IconSettings, IconUsersGroup } from "@tabler/icons-react";
import { api } from "@homarr/api/server";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { Link } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { NavigationLink } from "./_navigation";
@@ -1,3 +1,4 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { Alert, Anchor, Center, Group, Stack, Table, TableTbody, TableTd, TableTr, Text, Title } from "@mantine/core";
import { IconExclamationCircle } from "@tabler/icons-react";
@@ -9,7 +10,7 @@ import { auth } from "@homarr/auth/next";
import { isProviderEnabled } from "@homarr/auth/server";
import { everyoneGroup } from "@homarr/definitions";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { Link, SearchInput, UserAvatar } from "@homarr/ui";
import { SearchInput, UserAvatar } from "@homarr/ui";
import { ReservedGroupAlert } from "../_reserved-group-alert";
import { AddGroupMember } from "./_add-group-member";
@@ -2,6 +2,7 @@
import type { ReactNode } from "react";
import { useMemo, useState } from "react";
import Link from "next/link";
import type { DragEndEvent, DraggableAttributes, DragStartEvent } from "@dnd-kit/core";
import {
closestCenter,
@@ -40,7 +41,7 @@ import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { Link, UserAvatarGroup } from "@homarr/ui";
import { UserAvatarGroup } from "@homarr/ui";
interface GroupsTableProps {
initialGroupIds: string[];
@@ -1,3 +1,4 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { Card, Group, Stack, Text, ThemeIcon, Title, UnstyledButton } from "@mantine/core";
import { IconChevronRight, IconUsersGroup } from "@tabler/icons-react";
@@ -6,7 +7,6 @@ import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { everyoneGroup } from "@homarr/definitions";
import { getI18n } from "@homarr/translation/server";
import { Link } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
@@ -1,7 +1,6 @@
import { notFound } from "next/navigation";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import DOMPurify from "isomorphic-dompurify";
import { db, eq } from "@homarr/db";
import { medias } from "@homarr/db/schema";
@@ -20,24 +19,11 @@ export async function GET(_req: NextRequest, props: { params: Promise<{ id: stri
notFound();
}
let content = new Uint8Array(image.content);
// Sanitize SVG content to prevent XSS attacks
if (image.contentType === "image/svg+xml" || image.contentType === "image/svg") {
const svgText = new TextDecoder().decode(content);
const sanitized = DOMPurify.sanitize(svgText, {
USE_PROFILES: { svg: true, svgFilters: true },
});
content = new TextEncoder().encode(sanitized);
}
const headers = new Headers();
headers.set("Content-Type", image.contentType);
headers.set("Content-Length", content.length.toString());
headers.set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox");
headers.set("X-Content-Type-Options", "nosniff");
headers.set("Content-Length", image.content.length.toString());
return new NextResponse(content, {
return new NextResponse(new Uint8Array(image.content), {
status: 200,
headers,
});
@@ -1,10 +1,10 @@
import { useState } from "react";
import Link from "next/link";
import { Anchor, Button, Group, Stack, Table, TableTbody, TableTh, TableThead, TableTr } from "@mantine/core";
import { IconPlus } from "@tabler/icons-react";
import { useModalAction } from "@homarr/modals";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
import type { AccessQueryData } from "./access-settings";
import { AccessSelectRow } from "./access-table-rows";
@@ -1,10 +1,11 @@
import { useState } from "react";
import Link from "next/link";
import { Anchor, Box, Button, Group, Stack, Table, TableTbody, TableTh, TableThead, TableTr } from "@mantine/core";
import { IconPlus } from "@tabler/icons-react";
import { useModalAction } from "@homarr/modals";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { Link, UserAvatar } from "@homarr/ui";
import { UserAvatar } from "@homarr/ui";
import type { AccessQueryData } from "./access-settings";
import { AccessDisplayRow, AccessSelectRow } from "./access-table-rows";
@@ -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,
};
}
+1 -1
View File
@@ -1,8 +1,8 @@
import type { ReactNode } from "react";
import Link from "next/link";
import { AppShellHeader, Group, UnstyledButton } from "@mantine/core";
import { Spotlight } from "@homarr/spotlight";
import { Link } from "@homarr/ui";
import { ClientBurger } from "./header/burger";
import { DesktopSearchInput, MobileSearchButton } from "./header/search";
@@ -1,10 +1,9 @@
import type { ForwardedRef, ReactNode } from "react";
import { forwardRef } from "react";
import Link from "next/link";
import type { ActionIconProps } from "@mantine/core";
import { ActionIcon } from "@mantine/core";
import { Link } from "@homarr/ui";
type HeaderButtonProps = (
| {
onClick?: () => void;
@@ -1,11 +1,10 @@
"use client";
import type { ReactNode } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { NavLink } from "@mantine/core";
import { Link } from "@homarr/ui";
export const CommonNavLink = (props: ClientNavigationLink) =>
"href" in props ? <NavLinkHref {...props} /> : <NavLinkWithItems {...props} />;
@@ -2,6 +2,7 @@
import type { ReactNode } from "react";
import { useCallback, useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Center, Menu, Stack, Text, useMantineColorScheme } from "@mantine/core";
import { useHotkeys, useTimeout } from "@mantine/hooks";
@@ -21,7 +22,6 @@ import { signOut, useSession } from "@homarr/auth/client";
import { hotkeys } from "@homarr/definitions";
import { createModal, useModalAction } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
import { useAuthContext } from "~/app/[locale]/_client-providers/session";
import { CurrentLanguageCombobox } from "./language/current-language-combobox";
@@ -1,7 +1,10 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { createTRPCClient, httpLink } from "@trpc/client";
import SuperJSON from "superjson";
import { api } from "@homarr/api/server";
import type { AppRouter } from "@homarr/api";
import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
import { localeCookieKey } from "@homarr/definitions";
import type { SupportedLanguage } from "@homarr/translation";
import { supportedLanguages } from "@homarr/translation";
@@ -9,12 +12,12 @@ import { createI18nMiddleware } from "@homarr/translation/middleware";
let isOnboardingFinished = false;
export async function proxy(request: NextRequest) {
export async function middleware(request: NextRequest) {
// Redirect to onboarding if it's not finished yet
const pathname = request.nextUrl.pathname;
if (!isOnboardingFinished && !pathname.endsWith("/init")) {
const currentOnboardingStep = await api.onboard.currentStep();
const currentOnboardingStep = await serverFetchApi.onboard.currentStep.query();
if (currentOnboardingStep.current !== "finish") {
return NextResponse.redirect(new URL("/init", request.url));
}
@@ -26,12 +29,11 @@ export async function proxy(request: NextRequest) {
const currentLocale = request.cookies.get(localeCookieKey)?.value;
let defaultLocale: SupportedLanguage = "en";
if (!currentLocale || !supportedLanguages.includes(currentLocale as SupportedLanguage)) {
defaultLocale = await api.serverSettings.getCulture().then((culture) => culture.defaultLocale);
defaultLocale = await serverFetchApi.serverSettings.getCulture.query().then((culture) => culture.defaultLocale);
}
// We don't want to fallback to accept-language header so we clear it
request.headers.set("accept-language", "");
const next = createI18nMiddleware(defaultLocale);
return next(request);
}
@@ -39,3 +41,13 @@ export async function proxy(request: NextRequest) {
export const config = {
matcher: ["/((?!api|static|.*\\..*|_next|favicon.ico|robots.txt).*)"],
};
export const serverFetchApi = createTRPCClient<AppRouter>({
links: [
httpLink({
url: getTrpcUrl(),
transformer: SuperJSON,
headers: createHeadersCallbackForSource("server-fetch"),
}),
],
});
+7 -7
View File
@@ -37,20 +37,20 @@
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"dayjs": "^1.11.19",
"dayjs": "^1.11.18",
"dotenv": "^17.2.3",
"fastify": "^5.6.2",
"superjson": "2.2.5",
"fastify": "^5.6.1",
"superjson": "2.2.3",
"undici": "7.16.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^24.10.1",
"dotenv-cli": "^11.0.0",
"esbuild": "^0.27.0",
"eslint": "^9.39.1",
"@types/node": "^24.9.1",
"dotenv-cli": "^10.0.0",
"esbuild": "^0.25.11",
"eslint": "^9.38.0",
"prettier": "^3.6.2",
"tsx": "4.20.4",
"typescript": "^5.9.3"
+2 -2
View File
@@ -34,8 +34,8 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.1",
"esbuild": "^0.27.0",
"eslint": "^9.39.1",
"esbuild": "^0.25.11",
"eslint": "^9.38.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3"
}
+21 -23
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.45.0",
"version": "1.43.0",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -39,30 +39,28 @@
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^12.0.2",
"@semantic-release/npm": "^13.1.2",
"@semantic-release/github": "^12.0.0",
"@semantic-release/npm": "^13.1.1",
"@semantic-release/release-notes-generator": "^14.1.0",
"@testcontainers/redis": "^11.8.1",
"@turbo/gen": "^2.6.1",
"@vitejs/plugin-react": "^5.1.1",
"@vitest/coverage-v8": "^4.0.10",
"@vitest/ui": "^4.0.10",
"@testcontainers/redis": "^11.7.2",
"@turbo/gen": "^2.5.8",
"@vitejs/plugin-react": "^5.1.0",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"conventional-changelog-conventionalcommits": "^9.1.0",
"cross-env": "^10.1.0",
"jsdom": "^27.2.0",
"json5": "^2.2.3",
"jsdom": "^27.0.1",
"prettier": "^3.6.2",
"semantic-release": "^25.0.2",
"testcontainers": "^11.8.1",
"turbo": "^2.6.1",
"semantic-release": "^25.0.1",
"testcontainers": "^11.7.2",
"turbo": "^2.5.8",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.0.10"
"vitest": "^3.2.4"
},
"packageManager": "pnpm@10.22.0",
"packageManager": "pnpm@10.19.0",
"engines": {
"node": ">=24.11.1",
"pnpm": ">=10.22.0"
"node": ">=22.21.0"
},
"pnpm": {
"onlyBuiltDependencies": [
@@ -79,23 +77,23 @@
"overrides": {
"@babel/helpers@<7.26.10": ">=7.28.4",
"@babel/runtime@<7.26.10": ">=7.28.4",
"axios@>=1.0.0 <1.8.2": ">=1.13.2",
"axios@>=1.0.0 <1.8.2": ">=1.12.2",
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
"esbuild@<=0.24.2": ">=0.27.0",
"form-data@>=4.0.0 <4.0.4": ">=4.0.5",
"hono@<4.6.5": ">=4.10.6",
"esbuild@<=0.24.2": ">=0.25.11",
"form-data@>=4.0.0 <4.0.4": ">=4.0.4",
"hono@<4.6.5": ">=4.10.3",
"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",
"rollup@>=4.0.0 <4.22.4": ">=4.53.3",
"rollup@>=4.0.0 <4.22.4": ">=4.52.5",
"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.1.12"
},
"patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch",
+2 -2
View File
@@ -26,13 +26,13 @@
"@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.3"
},
"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",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}
}
+8 -8
View File
@@ -43,16 +43,16 @@
"@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.5",
"@trpc/client": "^11.6.0",
"@trpc/react-query": "^11.6.0",
"@trpc/server": "^11.6.0",
"@trpc/tanstack-react-query": "^11.6.0",
"lodash.clonedeep": "^4.5.0",
"next": "16.0.3",
"next": "15.5.6",
"react": "19.2.0",
"react-dom": "19.2.0",
"superjson": "2.2.5",
"superjson": "2.2.3",
"trpc-to-openapi": "^3.1.0",
"zod": "^4.1.12"
},
@@ -60,7 +60,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"prettier": "^3.6.2",
"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
@@ -3,7 +3,6 @@ import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
import { mediaRequestStatusConfiguration } from "@homarr/integrations/types";
import type { MediaRequest } from "@homarr/integrations/types";
import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list";
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
@@ -36,10 +35,7 @@ export const mediaRequestsRouter = createTRPCRouter({
return dataB.createdAt.getTime() - dataA.createdAt.getTime();
}
return (
mediaRequestStatusConfiguration[dataA.status].position -
mediaRequestStatusConfiguration[dataB.status].position
);
return dataA.status - dataB.status;
});
}),
subscribeToLatestRequests: publicProcedure
+8 -15
View File
@@ -40,22 +40,15 @@ export const releasesRouter = createTRPCRouter({
.query(async ({ ctx, input }) => {
return await Promise.all(
input.repositories.map(async (repository) => {
const response = await releasesRequestHandler
.handler(ctx.integration, {
id: repository.id,
identifier: repository.identifier,
versionRegex: formatVersionFilterRegex(repository.versionFilter),
})
.getCachedOrUpdatedDataAsync({
forceUpdate: false,
});
return {
const innerHandler = releasesRequestHandler.handler(ctx.integration, {
id: repository.id,
integration: { name: ctx.integration.name, kind: ctx.integration.kind },
timestamp: response.timestamp,
...response.data,
};
identifier: repository.identifier,
versionRegex: formatVersionFilterRegex(repository.versionFilter),
});
return await innerHandler.getCachedOrUpdatedDataAsync({
forceUpdate: false,
});
}),
);
}),
+5 -5
View File
@@ -23,8 +23,8 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@auth/core": "^0.41.1",
"@auth/drizzle-adapter": "^1.11.1",
"@auth/core": "^0.41.0",
"@auth/drizzle-adapter": "^1.11.0",
"@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
@@ -35,8 +35,8 @@
"bcrypt": "^6.0.0",
"cookies": "^0.9.1",
"ldapts": "8.0.9",
"next": "16.0.3",
"next-auth": "5.0.0-beta.30",
"next": "15.5.6",
"next-auth": "5.0.0-beta.29",
"react": "19.2.0",
"react-dom": "19.2.0",
"zod": "^4.1.12"
@@ -47,7 +47,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "6.0.0",
"@types/cookies": "0.9.2",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3"
}
@@ -23,11 +23,12 @@ describe("authorizeWithLdapCredentials", () => {
test("should fail when wrong ldap base credentials", async () => {
// Arrange
const spy = vi.spyOn(ldapClient, "LdapClient");
spy.mockImplementation(function () {
return {
bindAsync: vi.fn(() => Promise.reject(new Error("bindAsync"))),
} as unknown as ldapClient.LdapClient;
});
spy.mockImplementation(
() =>
({
bindAsync: vi.fn(() => Promise.reject(new Error("bindAsync"))),
}) as unknown as ldapClient.LdapClient,
);
// Act
const act = () =>
@@ -43,12 +44,13 @@ describe("authorizeWithLdapCredentials", () => {
test("should fail when user not found", async () => {
// Arrange
const spy = vi.spyOn(ldapClient, "LdapClient");
spy.mockImplementation(function () {
return {
bindAsync: vi.fn(() => Promise.resolve()),
searchAsync: vi.fn(() => Promise.resolve([])),
} as unknown as ldapClient.LdapClient;
});
spy.mockImplementation(
() =>
({
bindAsync: vi.fn(() => Promise.resolve()),
searchAsync: vi.fn(() => Promise.resolve([])),
}) as unknown as ldapClient.LdapClient,
);
// Act
const act = () =>
@@ -64,19 +66,20 @@ describe("authorizeWithLdapCredentials", () => {
test("should fail when user has invalid email", async () => {
// Arrange
const spy = vi.spyOn(ldapClient, "LdapClient");
spy.mockImplementation(function () {
return {
bindAsync: vi.fn(() => Promise.resolve()),
searchAsync: vi.fn(() =>
Promise.resolve([
{
dn: "test",
mail: "test",
},
]),
),
} as unknown as ldapClient.LdapClient;
});
spy.mockImplementation(
() =>
({
bindAsync: vi.fn(() => Promise.resolve()),
searchAsync: vi.fn(() =>
Promise.resolve([
{
dn: "test",
mail: "test",
},
]),
),
}) as unknown as ldapClient.LdapClient,
);
// Act
const act = () =>
@@ -100,14 +103,15 @@ describe("authorizeWithLdapCredentials", () => {
]),
);
const spy = vi.spyOn(ldapClient, "LdapClient");
spy.mockImplementation(function () {
return {
bindAsync: vi.fn((props: ldapClient.BindOptions) =>
props.distinguishedName === "test" ? Promise.reject(new Error("bindAsync")) : Promise.resolve(),
),
searchAsync: searchSpy,
} as unknown as ldapClient.LdapClient;
});
spy.mockImplementation(
() =>
({
bindAsync: vi.fn((props: ldapClient.BindOptions) =>
props.distinguishedName === "test" ? Promise.reject(new Error("bindAsync")) : Promise.resolve(),
),
searchAsync: searchSpy,
}) as unknown as ldapClient.LdapClient,
);
// Act
const act = () =>
@@ -125,20 +129,21 @@ describe("authorizeWithLdapCredentials", () => {
// Arrange
const db = createDb();
const spy = vi.spyOn(ldapClient, "LdapClient");
spy.mockImplementation(function () {
return {
bindAsync: vi.fn(() => Promise.resolve()),
searchAsync: vi.fn(() =>
Promise.resolve([
{
dn: "test",
mail: "test@gmail.com",
},
]),
),
disconnectAsync: vi.fn(),
} as unknown as ldapClient.LdapClient;
});
spy.mockImplementation(
() =>
({
bindAsync: vi.fn(() => Promise.resolve()),
searchAsync: vi.fn(() =>
Promise.resolve([
{
dn: "test",
mail: "test@gmail.com",
},
]),
),
disconnectAsync: vi.fn(),
}) as unknown as ldapClient.LdapClient,
);
// Act
const result = await authorizeWithLdapCredentialsAsync(db, {
@@ -163,20 +168,21 @@ describe("authorizeWithLdapCredentials", () => {
// Arrange
const db = createDb();
const spy = vi.spyOn(ldapClient, "LdapClient");
spy.mockImplementation(function () {
return {
bindAsync: vi.fn(() => Promise.resolve()),
searchAsync: vi.fn(() =>
Promise.resolve([
{
dn: "test",
mail: "test@gmail.com",
},
]),
),
disconnectAsync: vi.fn(),
} as unknown as ldapClient.LdapClient;
});
spy.mockImplementation(
() =>
({
bindAsync: vi.fn(() => Promise.resolve()),
searchAsync: vi.fn(() =>
Promise.resolve([
{
dn: "test",
mail: "test@gmail.com",
},
]),
),
disconnectAsync: vi.fn(),
}) as unknown as ldapClient.LdapClient,
);
await db.insert(users).values({
id: createId(),
name: "test",
@@ -214,20 +220,21 @@ describe("authorizeWithLdapCredentials", () => {
test("should authorize user with correct credentials and return updated name", async () => {
// Arrange
const spy = vi.spyOn(ldapClient, "LdapClient");
spy.mockImplementation(function () {
return {
bindAsync: vi.fn(() => Promise.resolve()),
searchAsync: vi.fn(() =>
Promise.resolve([
{
dn: "test55",
mail: "test@gmail.com",
},
]),
),
disconnectAsync: vi.fn(),
} as unknown as ldapClient.LdapClient;
});
spy.mockImplementation(
() =>
({
bindAsync: vi.fn(() => Promise.resolve()),
searchAsync: vi.fn(() =>
Promise.resolve([
{
dn: "test55",
mail: "test@gmail.com",
},
]),
),
disconnectAsync: vi.fn(),
}) as unknown as ldapClient.LdapClient,
);
const userId = createId();
const db = createDb();
@@ -261,26 +268,27 @@ describe("authorizeWithLdapCredentials", () => {
test("should authorize user with correct credentials and return his groups", async () => {
// Arrange
const spy = vi.spyOn(ldapClient, "LdapClient");
spy.mockImplementation(function () {
return {
bindAsync: vi.fn(() => Promise.resolve()),
searchAsync: vi.fn((argument: { options: { filter: string } }) =>
argument.options.filter.includes("group")
? Promise.resolve([
{
cn: "homarr_example",
},
])
: Promise.resolve([
{
dn: "test55",
mail: "test@gmail.com",
},
]),
),
disconnectAsync: vi.fn(),
} as unknown as ldapClient.LdapClient;
});
spy.mockImplementation(
() =>
({
bindAsync: vi.fn(() => Promise.resolve()),
searchAsync: vi.fn((argument: { options: { filter: string } }) =>
argument.options.filter.includes("group")
? Promise.resolve([
{
cn: "homarr_example",
},
])
: Promise.resolve([
{
dn: "test55",
mail: "test@gmail.com",
},
]),
),
disconnectAsync: vi.fn(),
}) as unknown as ldapClient.LdapClient,
);
const db = createDb();
const userId = createId();
await db.insert(users).values({
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}
}
+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
@@ -30,7 +30,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}
}
+2 -2
View File
@@ -34,8 +34,8 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"esbuild": "^0.27.0",
"eslint": "^9.39.1",
"esbuild": "^0.25.11",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}
}
+5 -5
View File
@@ -30,21 +30,21 @@
"@homarr/core": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@paralleldrive/cuid2": "^3.1.0",
"dayjs": "^1.11.19",
"dayjs": "^1.11.18",
"dns-caching": "^0.2.7",
"next": "16.0.3",
"octokit": "^5.0.5",
"next": "15.5.6",
"octokit": "^5.0.4",
"react": "19.2.0",
"react-dom": "19.2.0",
"undici": "7.16.0",
"zod": "^4.1.12",
"zod-validation-error": "^5.0.0"
"zod-validation-error": "^4.0.2"
},
"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",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}
}
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}
}
+6 -6
View File
@@ -29,10 +29,10 @@
"@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.5",
"@trpc/client": "^11.6.0",
"@trpc/server": "^11.6.0",
"@trpc/tanstack-react-query": "^11.6.0",
"node-cron": "^4.2.1",
"react": "19.2.0",
"zod": "^4.1.12"
@@ -42,8 +42,8 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11",
"@types/react": "19.2.6",
"eslint": "^9.39.1",
"@types/react": "19.2.2",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}
}
+1 -1
View File
@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}
}
+1 -1
View File
@@ -33,7 +33,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}
}
+1 -1
View File
@@ -44,7 +44,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"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
}
]
}
+10 -10
View File
@@ -43,34 +43,34 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@auth/core": "^0.41.1",
"@auth/core": "^0.41.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"@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.5",
"@paralleldrive/cuid2": "^3.1.0",
"@testcontainers/mysql": "^11.8.1",
"@testcontainers/postgresql": "^11.8.1",
"@testcontainers/mysql": "^11.7.2",
"@testcontainers/postgresql": "^11.7.2",
"better-sqlite3": "^12.4.1",
"dotenv": "^17.2.3",
"drizzle-kit": "^0.31.7",
"drizzle-kit": "^0.31.5",
"drizzle-orm": "^0.44.7",
"drizzle-zod": "^0.8.3",
"mysql2": "3.15.3",
"pg": "^8.16.3",
"superjson": "2.2.5"
"superjson": "2.2.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "7.6.13",
"@types/pg": "^8.15.6",
"dotenv-cli": "^11.0.0",
"esbuild": "^0.27.0",
"eslint": "^9.39.1",
"@types/pg": "^8.15.5",
"dotenv-cli": "^10.0.0",
"esbuild": "^0.25.11",
"eslint": "^9.38.0",
"prettier": "^3.6.2",
"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",
}),
+2 -2
View File
@@ -24,14 +24,14 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "^5.3.2",
"fast-xml-parser": "^5.3.0",
"zod": "^4.1.12"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"tsx": "4.20.4",
"typescript": "^5.9.3"
}
-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>;
+1 -1
View File
@@ -145,7 +145,7 @@ export const integrationDefs = {
},
piHole: {
name: "Pi-hole",
secretKinds: [["apiKey"], []],
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/pi-hole.svg",
category: ["dnsHole"],
documentationUrl: createDocumentationLink("/docs/integrations/pi-hole"),
+2 -2
View File
@@ -32,8 +32,8 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.47",
"eslint": "^9.39.1",
"@types/dockerode": "^3.3.45",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}
}
-1
View File
@@ -8,4 +8,3 @@ export type { Docker };
export const containerStates = ["created", "running", "paused", "restarting", "exited", "removing", "dead"] as const;
export type ContainerState = (typeof containerStates)[number];
export * from "./labels";
-4
View File
@@ -1,4 +0,0 @@
export const dockerLabels = {
// Label to hide a container from Homarrs docker integration
hide: "homarr.hide",
} as const;
+2 -2
View File
@@ -26,7 +26,7 @@
"@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.5",
"mantine-form-zod-resolver": "^1.3.0",
"zod": "^4.1.12"
},
@@ -34,7 +34,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}
}
+2 -3
View File
@@ -28,9 +28,8 @@
"@homarr/form": "workspace:^0.1.0",
"@homarr/notifications": "workspace:^0.1.0",
"@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.5",
"react": "19.2.0",
"zod": "^4.1.12"
},
@@ -38,7 +37,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}
}
@@ -2,6 +2,7 @@
import type { ChangeEventHandler } from "react";
import { useEffect, useRef } from "react";
import Link from "next/link";
import { Button, Checkbox, Collapse, Group, Stack, Textarea, TextInput } from "@mantine/core";
import { useDebouncedValue, useDisclosure } from "@mantine/hooks";
import type { z } from "zod/v4";
@@ -9,7 +10,6 @@ import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client";
import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
import { appManageSchema } from "@homarr/validation/app";
import { IconPicker } from "../icon-picker/icon-picker";
+1 -1
View File
@@ -31,7 +31,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}
}
+1 -1
View File
@@ -33,7 +33,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "6.0.0",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}
}
+8 -8
View File
@@ -25,10 +25,10 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@ctrl/deluge": "^7.5.0",
"@ctrl/qbittorrent": "^9.11.0",
"@ctrl/deluge": "^7.4.0",
"@ctrl/qbittorrent": "^9.10.0",
"@ctrl/transmission": "^7.4.0",
"@gitbeaker/rest": "^43.8.0",
"@gitbeaker/rest": "^43.5.0",
"@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
@@ -39,14 +39,14 @@
"@homarr/redis": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@jellyfin/sdk": "^0.13.0",
"@octokit/auth-app": "^8.1.2",
"@jellyfin/sdk": "^0.12.0",
"@octokit/auth-app": "^8.1.1",
"ical.js": "^2.2.1",
"maria2": "^0.4.1",
"node-ical": "^0.22.1",
"octokit": "^5.0.5",
"octokit": "^5.0.4",
"proxmox-api": "1.1.1",
"tsdav": "^2.1.6",
"tsdav": "^2.1.5",
"undici": "7.16.0",
"xml2js": "^0.6.2",
"zod": "^4.1.12"
@@ -57,7 +57,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-unifi": "^2.5.1",
"@types/xml2js": "^0.4.14",
"eslint": "^9.39.1",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}
}
@@ -11,7 +11,8 @@ import type { ReleasesProviderIntegration } from "../interfaces/releases-provide
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
import type {
DetailsProviderResponse,
ReleaseResponse,
ReleasesRepository,
ReleasesResponse,
} from "../interfaces/releases-providers/releases-providers-types";
import { detailsResponseSchema, releasesResponseSchema } from "./codeberg-schemas";
@@ -42,23 +43,22 @@ export class CodebergIntegration extends Integration implements ReleasesProvider
};
}
private parseIdentifier(identifier: string) {
const [owner, name] = identifier.split("/");
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
const [owner, name] = repository.identifier.split("/");
if (!owner || !name) {
localLogger.warn(
`Invalid identifier format. Expected 'owner/name', for ${identifier} with Codeberg integration`,
{ identifier },
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with Codeberg integration`,
{
identifier: repository.identifier,
},
);
return null;
return {
id: repository.id,
error: { code: "invalidIdentifier" },
};
}
return { owner, name };
}
public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse> {
const parsedIdentifier = this.parseIdentifier(identifier);
if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } };
const { owner, name } = parsedIdentifier;
const details = await this.getDetailsAsync(owner, name);
const releasesResponse = await this.withHeadersAsync(async (headers) => {
return await fetchWithTrustedCertificatesAsync(
@@ -66,36 +66,34 @@ export class CodebergIntegration extends Integration implements ReleasesProvider
{ headers },
);
});
if (!releasesResponse.ok) {
return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } };
return {
id: repository.id,
error: { message: releasesResponse.statusText },
};
}
const releasesResponseJson: unknown = await releasesResponse.json();
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
if (!success) {
return {
success: false,
id: repository.id,
error: {
code: "unexpected",
message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : error.message,
},
};
} else {
const formattedReleases = data.map((tag) => ({
latestRelease: tag.tag_name,
latestReleaseAt: tag.published_at,
releaseUrl: tag.url,
releaseDescription: tag.body,
isPreRelease: tag.prerelease,
}));
return getLatestRelease(formattedReleases, repository, details);
}
const formattedReleases = data.map((tag) => ({
latestRelease: tag.tag_name,
latestReleaseAt: tag.published_at,
releaseUrl: tag.url,
releaseDescription: tag.body,
isPreRelease: tag.prerelease,
}));
const latestRelease = getLatestRelease(formattedReleases, versionRegex);
if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } };
const details = await this.getDetailsAsync(owner, name);
return { success: true, data: { ...details, ...latestRelease } };
}
protected async getDetailsAsync(owner: string, name: string): Promise<DetailsProviderResponse | undefined> {
Loaded 100 of 196 files, more files were not shown because too many files have changed in this diff. Show more